Skip to content

Commit 44d4e98

Browse files
committed
fix: dedupe concurrent joins before auth resolution
1 parent 0a0792b commit 44d4e98

File tree

2 files changed

+47
-11
lines changed

2 files changed

+47
-11
lines changed

packages/loro-websocket/src/client/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,21 +1029,22 @@ export class LoroWebsocketClient {
10291029
return room;
10301030
});
10311031

1032+
// Register pending room immediately so concurrent join calls dedupe
1033+
this.pendingRooms.set(id, {
1034+
room,
1035+
resolve: resolve!,
1036+
reject: reject!,
1037+
adaptor: crdtAdaptor,
1038+
roomId,
1039+
auth: undefined,
1040+
});
10321041
this.roomAuth.set(id, auth);
10331042

1034-
// Resolve auth before registering pending room to avoid race condition
1035-
// where JoinError retry might use undefined auth
10361043
void this.resolveAuth(auth)
10371044
.then(authValue => {
1038-
// Register pending room only after auth is resolved
1039-
this.pendingRooms.set(id, {
1040-
room,
1041-
resolve: resolve!,
1042-
reject: reject!,
1043-
adaptor: crdtAdaptor,
1044-
roomId,
1045-
auth: authValue,
1046-
});
1045+
const currentPending = this.pendingRooms.get(id);
1046+
if (!currentPending) return;
1047+
currentPending.auth = authValue;
10471048

10481049
const joinPayload = encode({
10491050
type: MessageType.JoinRequest,

packages/loro-websocket/tests/e2e.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,41 @@ describe("E2E: Client-Server Sync", () => {
618618
}
619619
}, 15000);
620620

621+
it("dedupes concurrent join calls even before auth resolves", async () => {
622+
const port = await getPort();
623+
const tokens: string[] = [];
624+
625+
const server = new SimpleServer({
626+
port,
627+
authenticate: async (_roomId, _crdt, auth) => {
628+
tokens.push(new TextDecoder().decode(auth));
629+
return "write";
630+
},
631+
});
632+
await server.start();
633+
634+
const client = new LoroWebsocketClient({ url: `ws://localhost:${port}` });
635+
await client.waitConnected();
636+
637+
const adaptor = new LoroAdaptor();
638+
const auth = () => new TextEncoder().encode("token-once");
639+
640+
const joinPromise1 = client.join({ roomId: "dedupe", crdtAdaptor: adaptor, auth });
641+
const joinPromise2 = client.join({ roomId: "dedupe", crdtAdaptor: adaptor, auth });
642+
643+
expect(joinPromise1).toBe(joinPromise2);
644+
645+
const [room1, room2] = await Promise.all([joinPromise1, joinPromise2]);
646+
expect(room1).toBe(room2);
647+
648+
await waitUntil(() => tokens.length >= 1, 5000, 25);
649+
expect(tokens).toHaveLength(1);
650+
651+
await room1.destroy();
652+
client.destroy();
653+
await server.stop();
654+
}, 15000);
655+
621656
it("destroy rejects pending ping waiters", async () => {
622657
const client = new LoroWebsocketClient({ url: `ws://localhost:${port}` });
623658
await client.waitConnected();

0 commit comments

Comments
 (0)