Repository navigation
fix(net): apply a resumed reader's ranges and cursor before each read - #7869
Conversation
Quest-Claim: f0ce184c9c9e3476
A resumed reader's preferences and group cursor reached its route copies only when their signals notified, a microtask after the change. A read in between judged the copies by the old ones: a fully read range replaced by none read as exhausted and ended the track cleanly, and a narrowed range still handed out a buffered group it left out. Every read now applies both first, as the Rust Reader::sync does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. WalkthroughReader now applies subscription and cursor changes to its copies through a shared method, including when the track has ended. Three regression cases cover range changes while a track remains live. The related quest entry and planning document were removed. Merge Risk:
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reader.#follow()applies signals at read time —sync()now calls#follow()before judging a copy, mirroring the reader's subscription and cursor synchronously instead of waiting for the signal microtask; both signal callbacks call the same helper, so a change is applied once whichever fires first.- Cursor re-bounding shares the helper — a lowered cursor start still backfills the newly covered groups and lets go once ended, matching the old cursor callback's behavior.
- Three model regressions — pin the seed 12345 clean-end case, the narrowing case, and the widening case that fails with only the subscription mirrored.
- Quest bookkeeping — deletes the finished quest and its README entry.
I verified MOQ_RESUME_SEED=12345 MOQ_RESUME_CASES=400, resume.test.ts, and track.test.ts pass at c3921614. I also checked the documented seed 7 failure at 2000 cases: it still fails ("missing groups 1") and involves no ranges/cursor update, so #follow is a no-op there; the base fails earlier on a case this PR fixes. It is correctly left as a pre-existing follow-up.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head c392161 against base 68c72cc. No actionable correctness findings; applying both preferences and cursor bounds before each read is the right direction.
The new regressions exercise the immediate-read window for unbounding, narrowing and backward widening, including the backfill case. The separate seed 7 failure described in the PR remains outside this change.
Verification: GitHub-only static review of the reader, signal lifecycle, model cases and Rust parity. I did not run the tests or independently reproduce the reported seeds.
(Written by review (OpenAI))
|
quest land watch: the merge queue removed this pull request at c392161 because it conflicts with The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs. (written by quest land watch) |

Finishes
quest/m1/js-resume-model-12345.md:MOQ_RESUME_SEED=12345 MOQ_RESUME_CASES=400 bun test src/resume.model.test.ts(injs/net) passes, fixed at the cause.Problem
A resumed reader (
js/net/src/resume.ts,Reader) applied its subscriber's preferences and group cursor to its route copies only from signal callbacks, which notify a microtask after the change. A read in between judged the copies by the old values:Up, Join [0,1), Write g0, Ranges none): the read after the update calledexhausted(), which judged the serving copy still bounded at[0,1). It found the window fully read and ended the track cleanly, though the reader now follows live.… Down, Ranges [1,2)): the narrowed reader handed out buffered group 0 from a copy still bounded at[0,1). This also fails onmain.Approach
Reader.sync(), which every read calls first, now applies both signals to the copies through one#follow(). It mirrors the subscription when it changed, and re-bounds the copies when the cursor changed: backfilling a lowered start and letting go once ended, exactly as the cursor callback did. Both signal callbacks call the same#follow(), so a change is applied once, whichever comes first.This matches the Rust
Reader::sync. That applies the reader's preferences on every poll, and itswith_rangereaches the reader synchronously under the lock.Why the cursor too
Mirroring only the subscription, as moq-dev/moq#7736's
resume.tshunk does, fixes seed 12345 but breaks another case, which passes onmain(seed 8 and the default seed, 400 cases): an ordered reader whose range widens from[1,2)to[0,1), below group 0 its copy already passed, ends cleanly without group 0. The copy took the new ranges at once, but its local floor (set by#boundfrom the cursor) stayed at 1. Lowering that floor is what re-offers group 0 from the producer's cache (withRange→reofferCached), so the copy judged[0,1)drained with nothing held. Applying the cursor in the same step closes this.Tests
resume.model.test.tsregressionsgains three minimal cases:main("the track ended cleanly though no route declared an end");main("group 0 outside cursor 1,2");mainbut fails with only the subscription mirrored ("missing groups 0"), guarding the cursor half.Validation
MOQ_RESUME_SEED=12345 MOQ_RESUME_CASES=400 bun test src/resume.model.test.tsinjs/net.MOQ_RESUME_CASES=400passes on seeds 1 to 16, 777, 4242, 12345 and the default0x6d6f71.MOQ_RESUME_CASES=2000passes on 12345, 8 and the default.bun testinjs/net: 2888 pass, 0 fail.nix develop --command just check origin/main(biome, JS typecheck and tests, markdown, gh, shell, text, toml, nix, justfile) andquest check --base origin/mainpass.main: seed 7 at 2000 cases,Up i1s1, GiveUp i1s1 g2, Expire, Up i1s2, Join, Write i1s1, Write i1s2 g2, Down i1s2, Leave→ "missing groups 1". It involves no range or cursor change. See follow-ups.Impact
Subscriber.update()orwithRange()to its copies at its next read, rather than a microtask later.Choices made without asking
resume.tshunk mirrors the subscription insync(). That hunk alone regresses the widening case above, and feat(json)!: snapshot consumers subscribe themselves at zero delay #7736 is a broad breakingmoq-jsonchange that currently conflicts withmain. This PR replaces the hunk with#follow(). When feat(json)!: snapshot consumers subscribe themselves at zero delay #7736 mergesmain, it should take this version ofReaderand keep its ownresume.test.tscase, which still passes here.resume.test.ts: the three model regressions already pin each edge on the real routed path.Suggested follow-ups
Review
mechanical fix by quest land watch:
merged main at ba130ea3d864fd346e6cf0836fb216b18e460794 with the quest merge driver(head 0477d0c)quest review), gpt-6.1-sol, medium effort, session01a1a6ffc3921614d2ced8a47e6aff75b5da6e62e4dbcb39, round 1(Written by Claude Opus 5.5)