Skip to content

Commit e75769e

Browse files
committed
fix: rm isOnline to make retry connecting work
1 parent 1980301 commit e75769e

File tree

2 files changed

+107
-26
lines changed

2 files changed

+107
-26
lines changed

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

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,6 @@ type NodeProcessLike = {
5959
removeListener?: (event: string, listener: () => void) => unknown;
6060
};
6161

62-
const readInitialOnlineState = (): boolean => {
63-
try {
64-
if (typeof navigator !== "undefined") {
65-
const navOnline = (navigator as { onLine?: unknown }).onLine;
66-
if (typeof navOnline === "boolean") {
67-
return navOnline;
68-
}
69-
}
70-
} catch {}
71-
72-
const globalScope = globalThis as {
73-
navigator?: { onLine?: unknown };
74-
};
75-
const maybe = globalScope.navigator?.onLine;
76-
return typeof maybe === "boolean" ? maybe : true;
77-
};
78-
7962
/**
8063
* The websocket client's high-level connection status.
8164
* - `Connecting`: initial connect or a manual `connect()` in progress.
@@ -154,7 +137,6 @@ export class LoroWebsocketClient {
154137
private shouldReconnect = true;
155138
private reconnectAttempts = 0;
156139
private reconnectTimer?: ReturnType<typeof setTimeout>;
157-
private isOnline = readInitialOnlineState();
158140
private removeNetworkListeners?: () => void;
159141

160142
constructor(private ops: LoroWebsocketClientOptions) {
@@ -302,11 +284,6 @@ export class LoroWebsocketClient {
302284
// Ensure there's a pending promise for this attempt
303285
this.ensureConnectedPromise();
304286

305-
if (!this.isOnline) {
306-
this.setStatus(ClientStatus.Disconnected);
307-
return this.connectedPromise;
308-
}
309-
310287
this.setStatus(ClientStatus.Connecting);
311288

312289
const ws = new WebSocket(this.ops.url);
@@ -435,7 +412,6 @@ export class LoroWebsocketClient {
435412

436413
private scheduleReconnect(immediate = false) {
437414
if (this.reconnectTimer) return;
438-
if (!this.isOnline) return; // pause while offline
439415
const attempt = ++this.reconnectAttempts;
440416
const base = 500; // ms
441417
const max = 15_000; // ms
@@ -452,15 +428,13 @@ export class LoroWebsocketClient {
452428
}
453429

454430
private handleOnline = () => {
455-
this.isOnline = true;
456431
if (!this.shouldReconnect) return;
457432
if (this.status === ClientStatus.Connected) return;
458433
this.clearReconnectTimer();
459434
this.scheduleReconnect(true);
460435
};
461436

462437
private handleOffline = () => {
463-
this.isOnline = false;
464438
// Pause scheduled retries until online
465439
this.clearReconnectTimer();
466440
if (this.shouldReconnect) {

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import getPort from "get-port";
44
import { SimpleServer } from "../src/server/simple-server";
55
import { LoroWebsocketClient } from "../src/client";
66
import { ClientStatus } from "../src/client";
7+
import type { LoroWebsocketClientRoom } from "../src/client";
78
import { createLoroAdaptor } from "loro-adaptors";
89

910
// Make WebSocket available globally for the client
@@ -308,6 +309,12 @@ describe("E2E: Client-Server Sync", () => {
308309
unsubscribe2 = client2.onStatusChange(s => statuses2.push(s));
309310

310311
await Promise.all([client1.waitConnected(), client2.waitConnected()]);
312+
const initialConnectedCount1 = statuses1.filter(
313+
s => s === ClientStatus.Connected
314+
).length;
315+
const initialConnectedCount2 = statuses2.filter(
316+
s => s === ClientStatus.Connected
317+
).length;
311318

312319
const adaptor1 = createLoroAdaptor({ peerId: 31 });
313320
const adaptor2 = createLoroAdaptor({ peerId: 32 });
@@ -350,6 +357,16 @@ describe("E2E: Client-Server Sync", () => {
350357
50
351358
);
352359

360+
await waitUntil(
361+
() =>
362+
statuses1.filter(s => s === ClientStatus.Connected).length >
363+
initialConnectedCount1 &&
364+
statuses2.filter(s => s === ClientStatus.Connected).length >
365+
initialConnectedCount2,
366+
5000,
367+
25
368+
);
369+
353370
await waitUntil(() => text2.toString() === expected, 5000, 50);
354371

355372
await Promise.all([room1.destroy(), room2.destroy()]);
@@ -362,6 +379,96 @@ describe("E2E: Client-Server Sync", () => {
362379
}
363380
}, 20000);
364381

382+
it("reconnects even when the online event never fires", async () => {
383+
const env = installMockWindow();
384+
let client1: LoroWebsocketClient | undefined;
385+
let client2: LoroWebsocketClient | undefined;
386+
let unsubscribe1: (() => void) | undefined;
387+
let unsubscribe2: (() => void) | undefined;
388+
let room1: LoroWebsocketClientRoom | undefined;
389+
let room2: LoroWebsocketClientRoom | undefined;
390+
try {
391+
client1 = new LoroWebsocketClient({ url: `ws://localhost:${port}` });
392+
client2 = new LoroWebsocketClient({ url: `ws://localhost:${port}` });
393+
394+
const statuses1: string[] = [];
395+
const statuses2: string[] = [];
396+
unsubscribe1 = client1.onStatusChange(s => statuses1.push(s));
397+
unsubscribe2 = client2.onStatusChange(s => statuses2.push(s));
398+
399+
await Promise.all([client1.waitConnected(), client2.waitConnected()]);
400+
401+
const initialConnectedCount1 = statuses1.filter(
402+
s => s === ClientStatus.Connected
403+
).length;
404+
const initialConnectedCount2 = statuses2.filter(
405+
s => s === ClientStatus.Connected
406+
).length;
407+
408+
const adaptor1 = createLoroAdaptor({ peerId: 41 });
409+
const adaptor2 = createLoroAdaptor({ peerId: 42 });
410+
411+
[room1, room2] = await Promise.all([
412+
client1.join({ roomId: "offline-no-online", crdtAdaptor: adaptor1 }),
413+
client2.join({ roomId: "offline-no-online", crdtAdaptor: adaptor2 }),
414+
]);
415+
416+
const text1 = adaptor1.getDoc().getText("shared");
417+
const text2 = adaptor2.getDoc().getText("shared");
418+
419+
text1.insert(0, "seed");
420+
adaptor1.getDoc().commit();
421+
await waitUntil(() => text2.toString() === "seed", 3000, 50);
422+
423+
env.goOffline();
424+
await waitUntil(
425+
() =>
426+
client1!.getStatus() === ClientStatus.Disconnected &&
427+
client2!.getStatus() === ClientStatus.Disconnected,
428+
5000,
429+
25
430+
);
431+
await server.stop();
432+
433+
expect((navigator as { onLine?: boolean }).onLine).toBe(false);
434+
435+
// No env.goOnline() here – navigator stays offline
436+
await new Promise(resolve => setTimeout(resolve, 200));
437+
await server.start();
438+
439+
await waitUntil(
440+
() =>
441+
client1!.getStatus() === ClientStatus.Connected &&
442+
client2!.getStatus() === ClientStatus.Connected,
443+
10000,
444+
50
445+
);
446+
447+
expect((navigator as { onLine?: boolean }).onLine).toBe(false);
448+
449+
await waitUntil(
450+
() =>
451+
statuses1.filter(s => s === ClientStatus.Connected).length >
452+
initialConnectedCount1 &&
453+
statuses2.filter(s => s === ClientStatus.Connected).length >
454+
initialConnectedCount2,
455+
5000,
456+
25
457+
);
458+
459+
text1.insert(text1.length, " rebound");
460+
adaptor1.getDoc().commit();
461+
await waitUntil(() => text2.toString() === "seed rebound", 5000, 50);
462+
} finally {
463+
unsubscribe1?.();
464+
unsubscribe2?.();
465+
await Promise.all([room1?.destroy(), room2?.destroy()]);
466+
client1?.destroy();
467+
client2?.destroy();
468+
env.restore();
469+
}
470+
}, 20000);
471+
365472
it("destroy rejects pending ping waiters", async () => {
366473
const client = new LoroWebsocketClient({ url: `ws://localhost:${port}` });
367474
await client.waitConnected();

0 commit comments

Comments
 (0)