Repository navigation
A click should never wait on a prefetch that never started - #5
Merged
Merged
Conversation
ephnagler
pushed a commit
that referenced
this pull request
Sep 12, 2026
The node 22 CI failure turned out to be a test leak rather than a limiter bug, and the autofocus message turned out not to be benign — both worth recording with the reasoning, since both were filed under a conclusion that was the wrong way round. Also corrects a dangling reference: STATE filed the peekInFlight finding as "backlog item 8", but item 8 there is the Bucket H bridge snippet. The finding was never in the backlog at all — it went straight into PR #5. Numbers in BACKLOG.md are now stated to be stable identifiers, so closing an item leaves a gap instead of renumbering everything other documents cite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjTmKaEaizVN7ZPTKVzrK8
ephnagler
pushed a commit
that referenced
this pull request
Sep 12, 2026
The node 22 CI failure turned out to be a test leak rather than a limiter bug, and the autofocus message turned out not to be benign — both worth recording with the reasoning, since both were filed under a conclusion that was the wrong way round. Also corrects a dangling reference: STATE filed the peekInFlight finding as "backlog item 8", but item 8 there is the Bucket H bridge snippet. The finding was never in the backlog at all — it went straight into PR #5. Numbers in BACKLOG.md are now stated to be stable identifiers, so closing an item leaves a gap instead of renumbering everything other documents cite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjTmKaEaizVN7ZPTKVzrK8
peekInFlight handed navigate() any entry in `inFlight`, but a warm lands there when it is requested, not when it reaches the network. With both slots of MAX_CONCURRENT_PREFETCH busy, a queued warm has sent no bytes — and awaiting it stalls the navigation for as long as the queue takes, bounded only by the 10s nav timeout. That is the opposite of what the limiter's own comment promises: "leaves the connection budget for real navigation, which always bypasses this queue." A nav firing its own fetch does bypass it. A nav deduping against a queued warm inherited the wait. Found while proving the nav timeout in a browser: two stalled warms held both slots, the target's warm never started, and the nav awaited it for the full ten seconds. The timeout did its job — but it should not have been the thing doing the job. Gate on a `started` set instead. Bypassing costs at most one speculative duplicate, for a page the buyer has already navigated to; wasted background bandwidth beats a stalled click. Also cuts 4s from the suite. jsdom fires neither load nor error on an injected <link rel=stylesheet>, so syncHeadStyles waited out its real 2s cap in two tests. The fixture now dispatches load the way a browser does, rather than shortening the product's timeout to suit the tests. 11.1s → 7.1s, 147 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AK7NgrSeLCJzR3Tv5iwDVF
CI was red on node 22.13 and green on 24, failing `prefetch never runs more than two requests at once` — a test the branch does not touch. The failure shape named the cause: `served == 6` and `peak == 2` both passed while `inFlight == 1` failed, and that combination is only reachable if a SEVENTH fetch ran against a stub that had accounted for six. warmupNavLinks schedules its work 200ms out and kept no handle, so _resetPrefetchForTests could not cancel it. Each test tears its document down and builds a fresh one, so a warmup scheduled by an earlier test fired inside a later one and warmed the new document's nav links. Nothing was wrong with the limiter; node's timer scheduling only decided which test caught the stray. The added tests shifted the timing enough to make an old leak land somewhere visible. Hold the cancel thunk for every scheduled warmup and drop it on reset. Harmless in a browser either way — one document, and warmedFrom stops a repeat — but a handle is the right shape regardless. The regression test asserts on fetch traffic after a reset rather than on the concurrency counters: the stray request is the damage, wherever it happens to land. Verified to fail without the cancel. No change to config keys, hook or event names, bucket rules, install layout or the --json contract, so init, audit, the skill and the README are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjTmKaEaizVN7ZPTKVzrK8
ephnagler
force-pushed
the
prefetch-peek-and-suite-speed
branch
from
September 12, 2026 11:20
84cb5f1 to
912a076
Compare
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.
Two fixes, both found while proving the navigation timeout in a browser.
peekInFlighthandednavigate()work that had not begunA warm lands in
inFlightwhen it is requested, not when it reaches thenetwork. With both slots of
MAX_CONCURRENT_PREFETCHbusy, a queued warm hassent no bytes — and
navigate()awaited it anyway, stalling the click for aslong as the queue took, bounded only by the 10s nav timeout.
That inverts the guarantee the limiter's own comment makes:
A nav firing its own fetch does bypass it. A nav deduping against a queued warm
inherited the wait.
How it surfaced. While testing the timeout, two stalled warms held both
slots, the target's warm never started, and the nav awaited it for the full ten
seconds. The timeout did its job — but it should not have been the thing doing
the job.
Gated on a
startedset. Bypassing costs at most one speculative duplicate, fora page the buyer has already navigated to. Wasted background bandwidth beats a
stalled click.
The suite spent 4s waiting on a cap jsdom never releases
syncHeadStyleswaits up to 2s for injected stylesheets. jsdom fires neitherloadnorerroron an injected<link>, so two tests waited the full capevery run.
unrefalone does not fix this — it stops a timer holding the event loop open,it does not make the
awaitresolve. The fixture now dispatchesloadthe waya browser does, so the product's timeout is unchanged and the tests stop paying
for a jsdom gap. (The
unrefis added too, for the process-exit case.)11.1s → 7.1s.
Checks
npm run typecheckcleannpm run buildclean🤖 Generated with Claude Code
https://claude.ai/code/session_01AK7NgrSeLCJzR3Tv5iwDVF