fix: make server authoritative on initial sync to prevent stale IndexedDB from hiding R2 data
CI/CD / test (push) Successful in 1m45s Details
CI/CD / deploy (push) Has been skipped Details

- Server always overwrites local IndexedDB on initial page load (was only when server had more shapes)
- Prune local-only shapes not on server (stale deletions stuck in IndexedDB)
- Increase sync timeout from 5s to 15s (DO cold starts can exceed 5s)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-13 15:51:58 -04:00
parent 3f0b6c7d6c
commit e342100d5a
1 changed files with 26 additions and 23 deletions

View File

@ -445,7 +445,7 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
// Wait for network adapter with a timeout // Wait for network adapter with a timeout
const networkReadyPromise = adapter.whenReady() const networkReadyPromise = adapter.whenReady()
const timeoutPromise = new Promise<'timeout'>((resolve) => const timeoutPromise = new Promise<'timeout'>((resolve) =>
setTimeout(() => resolve('timeout'), 5000) setTimeout(() => resolve('timeout'), 15000)
) )
const result = await Promise.race([networkReadyPromise, timeoutPromise]) const result = await Promise.race([networkReadyPromise, timeoutPromise])
@ -500,39 +500,42 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
const localShapeCount = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length const localShapeCount = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length
const localIsEmpty = Object.keys(doc.store).length === 0 const localIsEmpty = Object.keys(doc.store).length === 0
// IMPROVED: Server is source of truth on initial load // Server is ALWAYS authoritative on initial page load.
// Prefer server if: // Previous logic only preferred server when it had more shapes,
// - Local is empty (first load or cleared cache) // but that's flawed: local IndexedDB can accumulate stale/deleted
// - Server has more shapes (local is likely stale/incomplete) // shapes, keeping its count artificially high and preventing
// - Local has shapes but server has different/more content // server data from ever overwriting the stale cache.
const serverHasMoreContent = serverShapeCount > localShapeCount // After initial sync, ongoing CRDT WebSocket sync handles changes.
const shouldPreferServer = localIsEmpty || localShapeCount === 0 || serverHasMoreContent
let addedFromServer = 0 let addedFromServer = 0
let updatedFromServer = 0 let updatedFromServer = 0
let keptLocal = 0
// Apply all server records (add new, overwrite existing)
Object.entries(serverDoc.store).forEach(([id, record]) => { Object.entries(serverDoc.store).forEach(([id, record]) => {
const existsLocally = !!doc.store[id] const existsLocally = !!doc.store[id]
doc.store[id] = record
if (!existsLocally) { if (existsLocally) {
// Record doesn't exist locally - add from server
doc.store[id] = record
addedFromServer++
} else if (shouldPreferServer) {
// Record exists locally but server has more content - update with server version
// This handles stale IndexedDB cache scenarios
doc.store[id] = record
updatedFromServer++ updatedFromServer++
} else { } else {
// Local has equal or more content - keep local version addedFromServer++
// Local changes will sync to server via normal CRDT mechanism
keptLocal++
} }
}) })
totalMerged = addedFromServer + updatedFromServer // Remove local-only shapes that don't exist on server
console.log(`🔄 Server sync: added=${addedFromServer}, updated=${updatedFromServer}, keptLocal=${keptLocal}, serverShapes=${serverShapeCount}, localShapes=${localShapeCount}, preferServer=${shouldPreferServer}`) // (they were deleted on server but still in stale IndexedDB)
let removedStale = 0
if (!localIsEmpty) {
const serverIds = new Set(Object.keys(serverDoc.store))
for (const id of Object.keys(doc.store)) {
if (!serverIds.has(id) && doc.store[id]?.typeName === 'shape') {
delete doc.store[id]
removedStale++
}
}
}
totalMerged = addedFromServer + updatedFromServer + removedStale
console.log(`🔄 Server sync: added=${addedFromServer}, updated=${updatedFromServer}, removedStale=${removedStale}, serverShapes=${serverShapeCount}, localShapes=${localShapeCount}`)
}) })
const finalDoc = handle.doc() const finalDoc = handle.doc()