fix(sync): hold a drained cursor a minute back so late commits arrive

A page's watermark is its last row's stamp, and a long import stamps its rows before it commits them: a quick write committing in between carried the watermark past them, and the keyset never returned them. A caught-up page now hands out watermarks at most a minute before serverNow in every row domain, as start=now does, so the next pull reads that minute again. The mirror domain keeps its watermark.
This commit is contained in:
Marc-André Bombeck committed 2026-11-10 19:08:36 +01:00
1 parent 928ade5109
commit 6fb0d184f1
7 files changed
+97 -7

No files matched your search

+2
View File
@@ -14,6 +14,7 @@ import {
decodeCursorIssuedAt,
encodeCursor,
encodeCursorAtNow,
holdBackDrainedCursor,
SYNC_DOMAINS,
type SyncCursor,
type SyncDomain,
@@ -232,6 +233,7 @@ export async function pullSyncChanges(input: {
cd.hasMore ||
cy.hasMore ||
mm.hasMore;
if (!hasMore) holdBackDrainedCursor(nextCursor, serverNow.getTime());
annotate({
action: { name: "sync.changes.pull" },
+28
View File
@@ -129,6 +129,34 @@ export function encodeCursorAtNow(nowMs: number): string {
return encodeCursor(cursor, nowMs);
}
/**
* Hold a drained walk's watermarks back to `START_NOW_OVERLAP_MS` before
* `nowMs`, in place. The watermark of a page is the stamp of its last row,
* and that stamp says nothing about rows still in flight: a long write (a
* Garmin or Withings chunk holds its transaction for seconds) stamps its rows
* first and commits them last, so a quick write that commits in between can
* carry the watermark past them, and the keyset would never return them.
* Held back a minute, the next pull reads the last minute again and the
* late rows come with it; anything older is passed as before. Only the
* cursor of a walk that is done is held back: one that still has pages keeps
* moving forward, or a minute holding more than a page could never drain.
*
* `measurementMirror` is left alone. Its watermark is a token's mirror
* stamp, written by one short update, and reading its last minute again
* would send every reading of a token whose mirror switch just flipped
* once more on every pull in that minute.
*/
export function holdBackDrainedCursor(cursor: SyncCursor, nowMs: number): void {
const ceiling = nowMs - START_NOW_OVERLAP_MS;
for (const domain of SYNC_DOMAINS) {
if (domain === "measurementMirror") continue;
const wm = cursor[domain];
if (wm && wm.updatedAtMs > ceiling) {
cursor[domain] = { updatedAtMs: ceiling, id: "0" };
}
}
}
function parseWatermark(raw: unknown): DomainWatermark | null {
if (typeof raw !== "object" || raw === null) return null;
const u = (raw as { u?: unknown }).u;