fix(sync): place a start=now cursor a minute behind the present

updatedAt is stamped when a write is built, not when it commits, so a
transaction in flight when the cursor was handed out could commit a row
stamped before it, and a watermark at serverNow skipped that row for
good. The watermarks now sit one minute back; client upserts are
idempotent, so the overlap is harmless. The token's issue time stays
serverNow.
This commit is contained in:
Marc-André Bombeck committed 2026-11-07 01:33:19 +01:00
1 parent 126bab8c00
commit 01884e91fa
8 files changed
+79 -26

No files matched your search

@@ -1,8 +1,9 @@
/**
* `GET /api/sync/changes?start=now` (#1339) — a cursor at the present.
*
* The answer is an empty page whose cursor sits at `serverNow` in every
* domain, in the feed's own opaque format, and nothing is read: the point is
* The answer is an empty page whose cursor sits a minute before `serverNow`
* in every domain (a write stamped before it and committed after it is not
* skipped), in the feed's own opaque format, and nothing is read: the point is
* to skip the history walk, so a read here would be the bug. The database is
* deliberately not mocked and not reachable: any read would fail the request,
* so the 200 below is itself the proof that none happened.
@@ -54,7 +55,7 @@ beforeEach(() => {
});
describe("GET /api/sync/changes?start=now", () => {
it("answers an empty page with a cursor at serverNow in every domain", async () => {
it("answers an empty page with a cursor a minute before serverNow in every domain", async () => {
const before = Date.now();
const res = await GET(req("?start=now"));
const after = Date.now();
@@ -80,7 +81,10 @@ describe("GET /api/sync/changes?start=now", () => {
const cursor = decodeCursor(data.cursor);
expect(cursor).not.toBeNull();
for (const domain of SYNC_DOMAINS) {
expect(cursor![domain], domain).toEqual({ updatedAtMs: nowMs, id: "0" });
expect(cursor![domain], domain).toEqual({
updatedAtMs: nowMs - 60_000,
id: "0",
});
}
expect(decodeCursorIssuedAt(data.cursor)).toBe(nowMs);
});
+3 -2
View File
@@ -43,8 +43,9 @@
* cycle tracking off gets both blocks empty (never a 403 — see the
* gate note in the handler) and their cursor watermarks untouched.
*
* `start=now` (#1339) answers an empty page whose cursor sits at
* `serverNow` in every domain, in the same opaque format, so a client that
* `start=now` (#1339) answers an empty page whose cursor sits a minute
* before `serverNow` in every domain (`START_NOW_OVERLAP_MS`), in the same
* opaque format, so a client that
* only wants what changes from now on (the Apple Health write-back) pages
* forward from there instead of draining the whole history first.
*