From 97f88c6e422a08b22a261841e8c27fd0f24246f6 Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Mon, 7 Sep 2026 10:50:18 +1200 Subject: [PATCH] SF-3913 Pause sending of ops to ShareDB if offline --- .../src/app/shared/text/text.component.ts | 18 ++++++++++++++++++ .../ClientApp/src/typings/sharedb.d.ts | 2 ++ .../memory-realtime-remote-store.ts | 9 +++++++++ .../src/xforge-common/realtime-remote-store.ts | 2 ++ .../sharedb-realtime-remote-store.ts | 8 ++++++++ 5 files changed, 39 insertions(+) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.ts index e358077af14..9a50bb822c7 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.ts @@ -154,6 +154,7 @@ export class TextComponent implements AfterViewInit, OnDestroy { private activePresenceSubscription?: Subscription; private onDeleteSub?: Subscription; private localSystemChangesSub?: Subscription; + private onlineSubscription?: Subscription; private readonly DEFAULT_MODULES: any = { toolbar: false, keyboard: { @@ -1213,6 +1214,23 @@ export class TextComponent implements AfterViewInit, OnDestroy { // But if getText does not return, then we are showing a good message. this.loadingState = 'offline-or-loading'; const textDoc = await this.projectService.getText(this._id); + + // When the user appears offline, ensure that the user's ops are not sent to ShareDB by pausing sending. + // This will prevent the ViewModel's fixSegment offline-specific logic causing issues with a flaky connection. + this.onlineSubscription?.unsubscribe(); + this.onlineSubscription = this.onlineStatusService.onlineStatus$ + .pipe(quietTakeUntilDestroyed(this.destroyRef)) + .subscribe(isOnline => { + if (isOnline) { + textDoc.adapter.resume(); + } else { + textDoc.adapter.pause(); + } + }); + if (!this.onlineStatusService.isOnline) { + textDoc.adapter.pause(); + } + this.loadingState = 'loading'; this.viewModel.bind(this._id, textDoc, this.subscribeToUpdates); if (this.viewModel.isEmpty) this.loadingState = 'empty-viewModel'; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/typings/sharedb.d.ts b/src/SIL.XForge.Scripture/ClientApp/src/typings/sharedb.d.ts index 17ccbe3238c..d995bfa2316 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/typings/sharedb.d.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/typings/sharedb.d.ts @@ -123,6 +123,8 @@ declare module 'sharedb/lib/client' { whenNothingPending(callback: Callback): void; hasWritePending(): boolean; flush(): void; + pause(): void; + resume(): void; previousSnapshot(): Snapshot; } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts index c8e20d6b22f..e1169f7d2d0 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/memory-realtime-remote-store.ts @@ -91,6 +91,7 @@ export class MemoryRealtimeDocAdapter implements RealtimeDocAdapter { on: (_event: string, _handler: Function) => {}, off: (_event: string, _handler: Function) => {} } as Presence; + paused: boolean = false; submitSource: boolean = false; subscribed: boolean = false; version: number = -1; @@ -127,6 +128,14 @@ export class MemoryRealtimeDocAdapter implements RealtimeDocAdapter { return Promise.resolve(); } + pause(): void { + this.paused = true; + } + + resume(): void { + this.paused = false; + } + subscribe(): Promise { this.subscribed = true; return Promise.resolve(); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/realtime-remote-store.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/realtime-remote-store.ts index b5ca3721516..34856dce2fc 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/realtime-remote-store.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/realtime-remote-store.ts @@ -50,6 +50,8 @@ export interface RealtimeDocAdapter { exists(): Promise; delete(): Promise; updatePendingOps(ops: any[]): void; + pause(): void; + resume(): void; previousSnapshot(): Promise; destroy(): Promise; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/sharedb-realtime-remote-store.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/sharedb-realtime-remote-store.ts index 3d590bebef7..af351655cee 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/sharedb-realtime-remote-store.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/sharedb-realtime-remote-store.ts @@ -420,6 +420,14 @@ export class SharedbRealtimeDocAdapter implements RealtimeDocAdapter { this.doc.flush(); } + pause(): void { + this.doc.pause(); + } + + resume(): void { + this.doc.resume(); + } + previousSnapshot(): Promise { return new Promise((resolve, reject) => { this.doc.connection.fetchSnapshot(