diff --git a/src/client/lib/signaling.ts b/src/client/lib/signaling.ts index 8977025..cf1ef1e 100644 --- a/src/client/lib/signaling.ts +++ b/src/client/lib/signaling.ts @@ -203,7 +203,9 @@ export class SignalingClient { this.events.onTerminated( event.reason === "Session replaced" ? "此页面的会话已被另一个标签页接管" - : "信令会话已终止,请刷新后重试", + : event.code === INVALID_MESSAGE_CLOSE_CODE + ? PROTOCOL_REFRESH_MESSAGE + : "信令会话已终止,请刷新后重试", ); } }); diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 3585df5..bbb0742 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -503,12 +503,12 @@ export const serverMessageSchema = z.union([ ...viewerQualityEvidenceWindowShape, }) .strict() - .refine(freezeFitsEvidenceWindow, { - message: "Viewer freeze duration exceeds its evidence window", - }), - z - .object({ - type: z.literal("host-status"), + .refine(freezeFitsEvidenceWindow, { + message: "Viewer freeze duration exceeds its evidence window", + }), + z + .object({ + type: z.literal("host-status"), online: z.boolean(), }) .strict(), diff --git a/tests/client-state.test.ts b/tests/client-state.test.ts index 0902a27..bb9aa6c 100644 --- a/tests/client-state.test.ts +++ b/tests/client-state.test.ts @@ -224,57 +224,68 @@ describe("client signaling recovery policy", () => { viewerPeerIds: [], iceConfig: { iceServers: [] }, }), - ])("terminates once when an old server uses an incompatible protocol", (payload) => { - const sockets: FakeWebSocket[] = []; - class FakeWebSocket extends EventTarget { - static readonly CLOSING = 2; - readyState = 1; - readonly send = vi.fn(); - readonly close = vi.fn(); + ])( + "terminates once when an old server uses an incompatible protocol", + (payload) => { + const sockets: FakeWebSocket[] = []; + class FakeWebSocket extends EventTarget { + static readonly CLOSING = 2; + readyState = 1; + readonly send = vi.fn(); + readonly close = vi.fn(); - constructor(readonly url: string) { - super(); - sockets.push(this); + constructor(readonly url: string) { + super(); + sockets.push(this); + } } - } - vi.stubGlobal("WebSocket", FakeWebSocket); - vi.stubGlobal("window", { - location: new URL("https://share.test/r/123456789012"), - setTimeout, - clearTimeout, - }); - const onTerminated = vi.fn(); - const signal = new SignalingClient( - { - roomId: "123456789012", - role: "viewer", - clientId: "viewer-client", - }, - { - onMessage: () => undefined, - onStatus: () => undefined, - onTerminated, - onAccessRequired: () => undefined, - }, - ); + vi.stubGlobal("WebSocket", FakeWebSocket); + vi.stubGlobal("window", { + location: new URL("https://share.test/r/123456789012"), + setTimeout, + clearTimeout, + }); + const onTerminated = vi.fn(); + const signal = new SignalingClient( + { + roomId: "123456789012", + role: "viewer", + clientId: "viewer-client", + }, + { + onMessage: () => undefined, + onStatus: () => undefined, + onTerminated, + onAccessRequired: () => undefined, + }, + ); - signal.start(); - sockets[0]!.dispatchEvent(new Event("open")); - expect(JSON.parse(String(sockets[0]!.send.mock.calls[0]![0]))).toMatchObject({ - type: "authenticate", - protocol: "screener-v1", - }); - const message = new Event("message"); - Object.defineProperty(message, "data", { value: payload }); - sockets[0]!.dispatchEvent(message); + signal.start(); + sockets[0]!.dispatchEvent(new Event("open")); + expect( + JSON.parse(String(sockets[0]!.send.mock.calls[0]![0])), + ).toMatchObject({ + type: "authenticate", + protocol: "screener-v1", + }); + const message = new Event("message"); + Object.defineProperty(message, "data", { value: payload }); + sockets[0]!.dispatchEvent(message); + const close = new Event("close"); + Object.defineProperties(close, { + code: { value: 1008 }, + reason: { value: "Invalid message" }, + }); + sockets[0]!.dispatchEvent(close); - expect(onTerminated).toHaveBeenCalledOnce(); - expect(onTerminated).toHaveBeenCalledWith( - "页面版本已更新,请刷新后重试", - ); - expect(sockets[0]!.close).toHaveBeenCalledOnce(); - expect(sockets).toHaveLength(1); - }); + expect(onTerminated).toHaveBeenCalledOnce(); + expect(onTerminated).toHaveBeenCalledWith( + "页面版本已更新,请刷新后重试", + ); + expect(sockets[0]!.close).toHaveBeenCalledOnce(); + expect(sockets).toHaveLength(1); + }, + ); it("returns to the access gate after an unauthorized upgrade", async () => { const sockets: FakeWebSocket[] = [];