DEV Community

Giang Pham Truong
Giang Pham Truong

Posted on AI-assisted

Paginate an Append-Only Sync Feed with a Fixed Snapshot Boundary

I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked the claims against the course-ledger client, server handler and focused tests.

A course-attempt ledger can grow while a client is fetching more than one page. If every page treats the latest row as its endpoint, a new attempt can enter halfway through the pull. For this append-only feed, we keep two sequence values with different jobs: after marks progress, while through fixes the upper bound for one traversal.

The server selects rows with sequence > after and sequence <= through. It fetches one row beyond the requested page size to decide hasMore, then returns an advancing cursor. The client carries the same through value on later pages and rejects a changed upper bound or a page that does not advance.

The tests make the boundary concrete. In the client test, a sequence 104 is appended while the current traversal covers 1–103; the current pull ends at 103. In the server API test, five attempts added after the initial bound are excluded from that traversal, while a fresh traversal can see the later bound. pnpm exec vitest run src/lib/client/course-ledger.spec.ts src/lib/server/api/course-ledger.spec.ts passed 15 tests in two files on 1 November 2026.

This is a bounded design choice for Pianory's append-only course-attempt ledger, not a universal database snapshot guarantee. The focused client transport and server test harness verify these cases; they do not measure production sync latency or every deployment's concurrency behavior. Source: src/lib/client/course-ledger.ts, src/lib/server/api/course-ledger.ts, and their focused specs.

Top comments (4)

Collapse
 
beusebiu profile image
Eusebiu Balan •

An append-only feed also saves you from the sync bug I had. Mine overwrites a day's number, and today was always sent, even when empty. So moving a habit to a new phone turned that day's steps into 0. Now a zero only goes out to clear a number that same phone sent earlier.

Collapse
 
ptrgiang profile image
Giang Pham Truong •

Thanks for the concrete failure case. A missing reading from a new phone and an explicit correction to a value sent by the old phone need different meanings; otherwise an empty local day can erase a real observation. A useful regression test would sync a nonzero day from phone A, bring phone B online with no entry for that day, then confirm the value survives until an explicit clear targeting A's observation. Our article's cursor handles a growing attempt feed, but a daily summary still needs that provenance and correction rule.

Collapse
 
beusebiu profile image
Eusebiu Balan •

I have half of that test. A phone with no history for the day has to send no rows at all, today included.

The clear is looser than what you describe. There's no id on the observation, just this phone's memory of what it sent last time, and a habit belongs to one phone at a time. That only holds because one phone fills a habit. With two at once I'd need your provenance rule.

Thread Thread
 
ptrgiang profile image
Giang Pham Truong •

That makes sense; I overstated the need for an observation ID under your one-phone-per-habit rule. The useful distinction is that no row from the new phone means unknown, while zero from the phone that previously sent the value means a clear. I'd keep two handoff tests: A sends 12, B takes over with no row for that day, and 12 survives; A sends 12 then explicitly clears it before handoff, and B's empty state leaves zero intact. If simultaneous writers become possible later, that ownership rule is where I'd revisit provenance.