fix: make server authoritative on initial sync to prevent stale IndexedDB from hiding R2 data
- 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:
parent
3f0b6c7d6c
commit
e342100d5a
|
|
@ -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]
|
||||||
|
|
||||||
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
|
doc.store[id] = record
|
||||||
|
if (existsLocally) {
|
||||||
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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue