From 5a8e4e6dcbc6f65908555eb515f67a30a6e23fda Mon Sep 17 00:00:00 2001 From: Michael Grinshpon Date: Mon, 6 Apr 2026 09:37:48 +0200 Subject: [PATCH 1/2] Reproduce error with LoroWebsocketClient when mounting in effect in react strict mode via unit test; fix the error --- packages/loro-websocket/src/client/index.ts | 1 + packages/loro-websocket/tests/e2e.test.ts | 41 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/packages/loro-websocket/src/client/index.ts b/packages/loro-websocket/src/client/index.ts index de9f11e..923d15e 100644 --- a/packages/loro-websocket/src/client/index.ts +++ b/packages/loro-websocket/src/client/index.ts @@ -1544,6 +1544,7 @@ export class LoroWebsocketClient { private sendJoinPayload(payload: Uint8Array) { if (this.safeSend(this.ws, payload, "join")) return; + if (!this.shouldReconnect) return; this.enqueueJoin(payload); void this.connect(); } diff --git a/packages/loro-websocket/tests/e2e.test.ts b/packages/loro-websocket/tests/e2e.test.ts index 5e17c1d..560f8cf 100644 --- a/packages/loro-websocket/tests/e2e.test.ts +++ b/packages/loro-websocket/tests/e2e.test.ts @@ -1072,6 +1072,47 @@ describe("E2E: RoomError rejoin policy", () => { }, 8000); }); +describe("React strict-mode: join + immediate close", () => { + let server: SimpleServer; + let port: number; + + beforeAll(async () => { + port = await getPort(); + server = new SimpleServer({ port }); + await server.start(); + }); + + afterAll(async () => { + await server.stop(); + }, 15000); + + it("close() before auth microtask settles does not resurrect the client", async () => { + const statuses: ClientStatus[] = []; + const client = new LoroWebsocketClient({ + url: `ws://localhost:${port}`, + }); + await client.waitConnected(); + + client.onStatusChange(s => statuses.push(s)); + + // Simulate React strict-mode: effect fires join(), cleanup fires close() + const adaptor = new LoroAdaptor(); + client.join({ roomId: "strict-mode-zombie", crdtAdaptor: adaptor }); + client.close(); // shouldReconnect = false + + // Let the resolveAuth().then(sendJoinPayload) microtask fire + await new Promise(r => setTimeout(r, 500)); + + // The client must stay dead — no Connecting/Connected after Disconnected + expect(client.getStatus()).toBe(ClientStatus.Disconnected); + expect( + statuses.filter(s => s === ClientStatus.Connecting) + ).toHaveLength(0); + + client.destroy(); + }, 5000); +}); + function installMockWindow(initialOnline = true) { const originalWindowDescriptor = Object.getOwnPropertyDescriptor( globalThis, From 13ee30c83bb9b93827fe1621562d2988714239bf Mon Sep 17 00:00:00 2001 From: Michael Grinshpon Date: Mon, 6 Apr 2026 09:51:19 +0200 Subject: [PATCH 2/2] Fix: uncaught type error in react websocket test --- packages/loro-websocket/tests/e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loro-websocket/tests/e2e.test.ts b/packages/loro-websocket/tests/e2e.test.ts index 560f8cf..243f278 100644 --- a/packages/loro-websocket/tests/e2e.test.ts +++ b/packages/loro-websocket/tests/e2e.test.ts @@ -1087,7 +1087,7 @@ describe("React strict-mode: join + immediate close", () => { }, 15000); it("close() before auth microtask settles does not resurrect the client", async () => { - const statuses: ClientStatus[] = []; + const statuses: string[] = []; const client = new LoroWebsocketClient({ url: `ws://localhost:${port}`, });