fix(app): keep browser relay WebSockets protocol-neutral (#2976)

Use the standard browser WebSocket constructor without the Node/React Native options object, while retaining the options-capable factory for supported runtimes and the desktop bridge.
This commit is contained in:
Mohamed Boudra
2026-08-07 17:30:43 +08:00
committed by GitHub
parent 75a5065bb3
commit a5184fd08a
9 changed files with 169 additions and 36 deletions
@@ -9,7 +9,6 @@ import {
encodeUtf8String,
extractRelayMessage,
} from "./daemon-client-transport.js";
import { defaultWebSocketFactory } from "./daemon-client-websocket-transport.js";
const createClientChannelMock = vi.hoisted(() => vi.fn());
@@ -18,28 +17,6 @@ vi.mock("@getpaseo/relay/e2ee", () => ({
}));
describe("daemon-client transport helpers", () => {
test("defaultWebSocketFactory passes custom headers to native WebSocket options", () => {
const constructorSpy = vi.fn();
function MockWebSocket(
url: string,
protocols?: string | string[],
options?: { headers?: Record<string, string> },
) {
constructorSpy(url, protocols, options);
}
vi.stubGlobal("WebSocket", MockWebSocket);
defaultWebSocketFactory("ws://example.test", {
protocols: ["paseo.test"],
headers: { "X-Tenant": "acme" },
});
expect(constructorSpy).toHaveBeenCalledWith("ws://example.test", ["paseo.test"], {
headers: { "X-Tenant": "acme" },
});
vi.unstubAllGlobals();
});
test("createEncryptedTransport closes handshake failures with browser-safe code", async () => {
createClientChannelMock.mockReset();
createClientChannelMock.mockRejectedValueOnce(new Error("handshake failed"));
@@ -5,23 +5,38 @@ import type {
} from "./daemon-client-transport-types.js";
import { extractRelayMessage } from "./daemon-client-transport-utils.js";
type GlobalWebSocketConstructor = new (
url: string,
protocols?: string | string[],
options?: { headers?: Record<string, string> },
) => WebSocketLike;
function getGlobalWebSocket(): GlobalWebSocketConstructor {
const globalWs = (globalThis as { WebSocket?: GlobalWebSocketConstructor }).WebSocket;
if (!globalWs) {
throw new Error("WebSocket is not available in this runtime");
}
return globalWs;
}
export function defaultWebSocketFactory(
url: string,
options?: { headers?: Record<string, string>; protocols?: string[] },
): WebSocketLike {
const globalWs = (
globalThis as {
WebSocket?: new (
url: string,
protocols?: string | string[],
options?: { headers?: Record<string, string> },
) => WebSocketLike;
}
).WebSocket;
if (!globalWs) {
throw new Error("WebSocket is not available in this runtime");
const globalWs = getGlobalWebSocket();
if (options?.protocols === undefined) {
return new globalWs(url);
}
return new globalWs(url, options?.protocols, { headers: options?.headers });
return new globalWs(url, options.protocols);
}
// React Native and Node-compatible WebSocket implementations accept this extra options object.
// Keep it out of the standard browser factory above.
export function nativeWebSocketFactory(
url: string,
options?: { headers?: Record<string, string>; protocols?: string[] },
): WebSocketLike {
return new (getGlobalWebSocket())(url, options?.protocols, { headers: options?.headers });
}
export function createWebSocketTransportFactory(factory: WebSocketFactory): DaemonTransportFactory {