Merge pull request #35855 from overleaf/ds-unable-to-sync-modal-improved

[WEB] Adding improved version of the unable to sync modal

GitOrigin-RevId: 1cfe2b3a2e8417028339efddb18385a5224e50c3
This commit is contained in:
Davinder Singh
2026-09-17 11:43:20 +00:00
committed by Copybot
parent 469f5629d2
commit 36d0a5c227
11 changed files with 241 additions and 72 deletions
@@ -2,7 +2,7 @@
// Migrated from services/web/frontend/js/ide/editor/Document.js
import RangesTracker from '@overleaf/ranges-tracker'
import { OTType, ShareJsDoc } from './share-js-doc'
import { OTType, ShareJsDoc, applyOpsToSnapshot } from './share-js-doc'
import { debugConsole } from '@/utils/debugging'
import { Socket } from '@/features/ide-react/connection/types/socket'
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
@@ -638,10 +638,22 @@ export class DocumentContainer extends EventEmitter {
debugConsole.error('[recovery] failed, loading without recovery', error)
this.releaseTrackChangesPin()
this.needsRangesRefetchAfterRecovery = false
const editorContent = this.doc?.getSnapshot() || record.snapshot
// Compute the user's local version so the unable-to-sync modal can
// show a diff of offline changes. If the ops can't be applied (e.g.
// corrupt backup), fall back to the raw snapshot.
let targetContent = record.snapshot
try {
targetContent = applyOpsToSnapshot(record)
} catch (err) {
debugConsole.warn(
'[recovery] failed to apply ops to snapshot for diff',
err
)
}
this.ideEventEmitter.emit('ide:unableToSyncOfflineChanges', {
docId: record.docId,
editorContent,
editorContent: targetContent,
baseContent: record.snapshot,
docName: this.docName,
})
OfflineDocBackup.remove(record.projectId, record.docId)
@@ -36,6 +36,20 @@ const intermittentConnectionImprovementsEnabled = isSplitTestEnabled(
'intermittent-connection-improvements'
)
// Apply buffered ops (inflight + pending) to a backup snapshot to reconstruct
// the user's local document state. Used by both the recovery path and the
// unable-to-sync modal diff.
export function applyOpsToSnapshot(backup: OfflineDocBackupRecord): string {
let snapshot = backup.snapshot
if (backup.inflightOp) {
snapshot = sharejs.types.text.apply(snapshot, backup.inflightOp)
}
if (backup.pendingOp) {
snapshot = sharejs.types.text.apply(snapshot, backup.pendingOp)
}
return snapshot
}
// All times below are in milliseconds
const SINGLE_USER_FLUSH_DELAY = 2000
const MULTI_USER_FLUSH_DELAY = 500
@@ -360,18 +374,9 @@ export class ShareJsDoc extends EventEmitter {
// constructs a doc at the baseline version, restores this, then catches the
// doc up to the current server version.
restoreFromOfflineBackup(backup: OfflineDocBackupRecord) {
// The snapshot must be the local view (baseline with the buffered ops
// applied), matching ShareJS's invariant that snapshot = acked version +
// inflight + pending. Seeding only the baseline would show the server text
// without the recovered edits.
let snapshot = backup.snapshot
if (backup.inflightOp) {
snapshot = sharejs.types.text.apply(snapshot, backup.inflightOp)
}
if (backup.pendingOp) {
snapshot = sharejs.types.text.apply(snapshot, backup.pendingOp)
}
this._doc.snapshot = snapshot
// The snapshot must be the local view (baseline + buffered ops),
// matching ShareJS's invariant: snapshot = acked version + inflight + pending.
this._doc.snapshot = applyOpsToSnapshot(backup)
this._doc.inflightOp = backup.inflightOp
this._doc.pendingOp = backup.pendingOp
this._doc.inflightSubmittedIds = [...backup.inflightSubmittedIds]