feat: implement daemon connectivity updates and relay protocol versioning

This commit is contained in:
Mohamed Boudra
2026-02-20 19:27:56 +07:00
parent 6dad3212da
commit 0350f00840
49 changed files with 4146 additions and 1351 deletions
+167 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { RelayDurableObject } from "./cloudflare-adapter.js";
import relayWorker, { RelayDurableObject } from "./cloudflare-adapter.js";
type MockSocket = WebSocket & {
send: ReturnType<typeof vi.fn>;
@@ -42,6 +42,57 @@ function createMockState() {
};
}
async function withMockWebSocketPair(
run: (sockets: { clientWs: MockSocket; serverWs: MockSocket }) => Promise<void> | void
): Promise<void> {
const serverWs = createMockSocket();
const clientWs = createMockSocket();
const WebSocketPairMock = class {
[index: number]: WebSocket;
constructor() {
this[0] = clientWs as unknown as WebSocket;
this[1] = serverWs as unknown as WebSocket;
}
};
const previousPair = (globalThis as unknown as { WebSocketPair?: unknown }).WebSocketPair;
(globalThis as unknown as { WebSocketPair: unknown }).WebSocketPair = WebSocketPairMock;
try {
await run({ clientWs, serverWs });
} finally {
if (previousPair === undefined) {
delete (globalThis as unknown as { WebSocketPair?: unknown }).WebSocketPair;
} else {
(globalThis as unknown as { WebSocketPair: unknown }).WebSocketPair = previousPair;
}
}
}
describe("RelayDurableObject versioning", () => {
it("accepts legacy v1 client sockets without clientId", async () => {
const { state } = createMockState();
await withMockWebSocketPair(async () => {
const relay = new RelayDurableObject(state as any);
const req = new Request("https://relay.test/ws?role=client&serverId=srv_test&v=1", {
headers: {
Upgrade: "websocket",
},
});
await relay.fetch(req).catch(() => undefined);
expect(state.acceptWebSocket).toHaveBeenCalled();
});
});
it("rejects v2 client sockets when clientId is missing", async () => {
const { state } = createMockState();
const relay = new RelayDurableObject(state as any);
const req = new Request("https://relay.test/ws?role=client&serverId=srv_test&v=2");
const response = await relay.fetch(req);
expect(response.status).toBe(400);
await expect(response.text()).resolves.toBe("Missing clientId parameter");
});
});
describe("RelayDurableObject control nudge/reset behavior", () => {
afterEach(() => {
vi.useRealTimers();
@@ -93,4 +144,119 @@ describe("RelayDurableObject control nudge/reset behavior", () => {
vi.advanceTimersByTime(5_000);
expect(control.close).toHaveBeenCalledWith(1011, "Control unresponsive");
});
it("does not replace existing client sockets for the same clientId", async () => {
const existingClient = createMockSocket({
version: "2",
role: "client",
clientId: "clt_same_session",
serverId: "srv_test",
createdAt: Date.now(),
});
const { state, setTagSockets } = createMockState();
setTagSockets("client:clt_same_session", [existingClient]);
setTagSockets("client", [existingClient]);
await withMockWebSocketPair(async () => {
const relay = new RelayDurableObject(state as any);
const req = new Request(
"https://relay.test/ws?role=client&serverId=srv_test&clientId=clt_same_session&v=2",
{
headers: {
Upgrade: "websocket",
},
}
);
await relay.fetch(req).catch(() => undefined);
expect(existingClient.close).not.toHaveBeenCalled();
});
});
it("keeps server data socket alive while at least one client socket remains", () => {
const clientId = "clt_multi";
const disconnectedClient = createMockSocket({
version: "2",
role: "client",
clientId,
serverId: "srv_test",
createdAt: Date.now(),
});
const stillConnectedClient = createMockSocket({
version: "2",
role: "client",
clientId,
serverId: "srv_test",
createdAt: Date.now(),
});
const serverData = createMockSocket();
const control = createMockSocket();
const { state, setTagSockets } = createMockState();
setTagSockets("server-control", [control]);
setTagSockets(`server:${clientId}`, [serverData]);
setTagSockets("client", [stillConnectedClient]);
setTagSockets(`client:${clientId}`, [stillConnectedClient]);
const relay = new RelayDurableObject(state as any);
relay.webSocketClose(
disconnectedClient as unknown as WebSocket,
1001,
"Client disconnected",
true
);
expect(serverData.close).not.toHaveBeenCalled();
expect(control.send).not.toHaveBeenCalledWith(
JSON.stringify({ type: "client_disconnected", clientId })
);
});
});
describe("relay worker endpoint routing", () => {
it("routes missing v to legacy v1 isolated DO ids", async () => {
const fetch = vi.fn(async (request: Request) => new Response(`ok:${new URL(request.url).searchParams.get("v")}`));
const get = vi.fn(() => ({ fetch }));
const idFromName = vi.fn(() => ({ toString: () => "id" }));
const response = await relayWorker.fetch(
new Request("https://relay.test/ws?serverId=srv_test&role=server"),
{ RELAY: { idFromName, get } } as any
);
expect(idFromName).toHaveBeenCalledWith("relay-v1:srv_test");
expect(fetch).toHaveBeenCalledTimes(1);
await expect(response.text()).resolves.toBe("ok:1");
});
it("routes v=2 to v2 isolated DO ids", async () => {
const fetch = vi.fn(async (request: Request) => new Response(`ok:${new URL(request.url).searchParams.get("v")}`));
const get = vi.fn(() => ({ fetch }));
const idFromName = vi.fn(() => ({ toString: () => "id" }));
const response = await relayWorker.fetch(
new Request("https://relay.test/ws?serverId=srv_test&role=server&v=2"),
{ RELAY: { idFromName, get } } as any
);
expect(idFromName).toHaveBeenCalledWith("relay-v2:srv_test");
expect(fetch).toHaveBeenCalledTimes(1);
await expect(response.text()).resolves.toBe("ok:2");
});
it("rejects invalid v values", async () => {
const fetch = vi.fn();
const get = vi.fn(() => ({ fetch }));
const idFromName = vi.fn(() => ({ toString: () => "id" }));
const response = await relayWorker.fetch(
new Request("https://relay.test/ws?serverId=srv_test&role=server&v=nope"),
{ RELAY: { idFromName, get } } as any
);
expect(response.status).toBe(400);
await expect(response.text()).resolves.toBe("Invalid v parameter (expected 1 or 2)");
expect(idFromName).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});
});