Skip to content

Commit 2ead9b4

Browse files
popmechanicnecrodome
authored andcommitted
fix(clerk): poll allDocs after attach to kick CRDT sync processing
database.attach() resolves when the WebSocket connects, but historical data streams in asynchronously. The streamed metadata isn't processed until something queries the database, so useLiveQuery never sees the data on a second device without a manual refresh. A periodic allDocs() call after attach forces the CRDT to process pending sync metadata, advancing the clock and triggering subscriptions. Polling stops once the document count stabilizes (3 consecutive polls unchanged) or after 20 seconds — whichever comes first. After that, real-time updates flow through the normal subscription path. Typical cost: 4-5 IndexedDB reads, then done.
1 parent 3c9fb90 commit 2ead9b4

File tree

1 file changed

+65
-0
lines changed

1 file changed

+65
-0
lines changed

use-fireproof/clerk/use-fireproof-clerk.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ const MAX_RETRY_DELAY_MS = 30 * 1000;
1717
const MAX_RETRY_COUNT = 8;
1818
// Small cleanup delay after detach (100ms)
1919
const DETACH_CLEANUP_DELAY_MS = 100;
20+
// Interval for polling database after attach to kick CRDT processing (2 seconds)
21+
const SYNC_POLL_INTERVAL_MS = 2000;
22+
// Stop early once doc count is stable for this many consecutive polls
23+
const SYNC_STABLE_THRESHOLD = 3;
24+
// Hard ceiling — stop polling regardless (20 seconds)
25+
const SYNC_POLL_MAX_MS = 20 * 1000;
2026

2127
/**
2228
* React hook that combines useFireproof with automatic Clerk-authenticated cloud sync.
@@ -271,6 +277,65 @@ export function useFireproofClerk(name: string | Database): UseFireproofClerkRes
271277
return () => clearTimeout(timer);
272278
}, [attachState, isSessionReady]);
273279

280+
// Workaround: kick the CRDT to process sync data after initial attach.
281+
// database.attach() resolves when the WebSocket connects, but historical data
282+
// streams in asynchronously. The streamed metadata isn't processed until
283+
// something queries the database, so useLiveQuery (which only re-queries on
284+
// subscription events) never sees the data. A periodic allDocs() call forces
285+
// the CRDT to process pending metadata, advancing the clock and triggering
286+
// subscriptions. Once the document count stabilizes, sync has caught up and
287+
// real-time updates flow through the normal subscription path — no more
288+
// polling needed.
289+
useEffect(() => {
290+
if (attachState.status !== "attached") return;
291+
292+
let stopped = false;
293+
let lastCount = -1;
294+
let stableRuns = 0;
295+
296+
const poll = async () => {
297+
if (stopped) return;
298+
try {
299+
const { rows } = await database.allDocs();
300+
const count = rows.length;
301+
302+
if (count === lastCount) {
303+
stableRuns++;
304+
if (stableRuns >= SYNC_STABLE_THRESHOLD) {
305+
console.debug("[fireproof-clerk] Initial sync settled, polling stopped");
306+
stopped = true;
307+
return;
308+
}
309+
} else {
310+
stableRuns = 0;
311+
}
312+
lastCount = count;
313+
} catch {
314+
// ignore polling errors
315+
}
316+
if (!stopped) {
317+
setTimeout(poll, SYNC_POLL_INTERVAL_MS);
318+
}
319+
};
320+
321+
// Start after a brief delay to let the first metadata packet arrive
322+
const startTimer = setTimeout(poll, SYNC_POLL_INTERVAL_MS);
323+
324+
// Hard ceiling — stop regardless
325+
const maxTimer = setTimeout(() => {
326+
if (!stopped) {
327+
console.debug("[fireproof-clerk] Sync poll hit max duration, stopping");
328+
stopped = true;
329+
}
330+
}, SYNC_POLL_MAX_MS);
331+
332+
return () => {
333+
stopped = true;
334+
clearTimeout(startTimer);
335+
clearTimeout(maxTimer);
336+
};
337+
}, [attachState.status, database]);
338+
274339
// Cleanup refresh timer on unmount
275340
useEffect(() => {
276341
return () => {

0 commit comments

Comments
 (0)