Repository navigation
Conversation
The bell's 60s poll sent two requests a minute from every open tab; 3 minutes still catches a read on another device quickly. The body batcher's "no body" answers lived only in memory, so every boot asked again about every save imported without its text (thousands of rkeys for a large import). They are now kept in IndexedDB metadata for a day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a2oETY6CeXVjKH8dG2
…ery 5 minutes The open-driven backing poll re-resolved every card on every poll, one getRecord each. Two collections had grown past the snapshot's 900-subrequest budget, so every poll ran out part way, never completed, and the collection stopped syncing, while each Saved-list open spent ~400ms of CPU and ~900 PDS fetches on it. Stored links now go to the snapshot as skipLinks; only new links and a random slice of 25 stored ones are resolved. Rows leave only on a whole listing (or a tombstone, or a re-check that finds the card gone), and newly resolved members are kept even from an incomplete snapshot so a big collection fills in across polls. Staging's per-minute cron was ~20% of the account's Workers CPU. It now runs every 5 minutes; the admin scales its cron and poller-row staleness thresholds to the cadence, and the runbook notes the heartbeat grace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a2oETY6CeXVjKH8dG2
…re race - backed_skipped_links (migration 0118) holds links that resolve to no article (note cards, cards with no URL, deleted items) so the incremental poll skips them instead of fetching each one again every poll; wiped wherever membership is, re-checked by the same random slice, dropped when a whole listing no longer names them. - An early return no longer throws away a re-check's non-transient skips. - Enable's eager body fill runs whenever members landed, not only on a complete poll (an incomplete one now lands members too). - Staging's five-minute cron re-lists five authors per run, keeping the per-minute reconcile rate. - Frontend: the known-empty restore is one shared promise, awaited before any persist write and before list-surface lookups; future-dated entries are dropped on restore. - Comments that still described the wholesale replace are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a2oETY6CeXVjKH8dG2
disnet
marked this pull request as ready for review
October 31, 2026 07:59
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Tim · project thread
Why
An audit of the account's CPU-ms (October: ~40.6M against the 30M included) found about 27.5M came from a Pages project outside this repo (disnetdev.com, handled separately). Skyreader's own Workers came to about 12M. This PR goes after the biggest avoidable Skyreader costs.
What changes
1. The Semble/Margin membership poll is incremental, and this also fixes a sync bug.
pollBackedMembershipused to re-resolve every card in the collection on every poll, onegetRecordeach. Two prod collections had grown past the snapshot's 900-subrequest budget. In the last week, 170 polls ran out part way, so the poll never completed and the collection never synced again. Each of those Saved-list opens still spent ~400ms of CPU (p99 ~1.1s) and ~900 PDS fetches.Now stored link URIs go to the snapshot as
skipLinks, following the patternroom-sync.tsalready uses:last_successful_backing_poll.full: truesemantics are unchanged.backed_skipped_links(migration 0118) remembers links that hold no article (note cards, cards with no URL, deleted items), so they are skipped too. It is wiped wherever membership is wiped.2. Staging cron every 5 minutes (
*/5 * * * *). Staging's per-minute cron was ~20% of Skyreader's Workers CPU on an environment with almost no traffic. The cron handler treats*/5as the main cron; all the minute-gated phases (every 5th minute, every 15th, the hour) still land. The document reconcile re-lists 5 authors per run, which keeps the per-minute rate. The admin scales its cron and poller-row staleness thresholds by the cadence it reads from the cron string. RUNBOOK and plan doc updated.3. Notification bell polls every 3 minutes instead of every minute. Each poll sends 2 requests from every open tab.
4. "No body" answers are remembered across page loads for 24h. Previously, every boot asked again, in batches of 200, about every save imported without its text (~4.6k
/api/saved/bodiescalls a day, and rising). A body that lands later bumpsupdatedAt, which still forces a refetch. Opening a save still asks on its own.Action needed after merge
backend-cron(staging) to a 5-minute period with ~15 minutes' grace, or it will page.Review
Ran the code-review skill at high effort plus an independent adversarial reviewer.
Fixed:
backed_skipped_links.hydrateBodies, or a persist write, could run before the restore and overwrite the saved entries → one shared restore promise, awaited before persisting and before list-surface lookups.complete.Left alone, deliberately:
backedUnsavethrows mid-delete; the refresh slice re-seeds them eventually, and re-seeding every row every poll is the cost this PR removes.Testing
npm run checkclean. New batcher tests cover the TTL and persist/restore.Generated by Claude Code