Repository navigation
fix(net,signals): js/net join, refusal, lister, and stale-effect phases; Reload status and Following hold one state - #8322
Conversation
Quest-Claim: 5d143ffb66b604a3
TrackServe owns its phase (serving, draining with the track's end, or failed), so RunSubscription stops caching the drained result and polls it every turn. An update that reopens a drained range now serves its fill tail instead of ending the subscription without it. The datagram forward, the session drivers' terminal result, IETF dispatch, lite PeerSetup, and the listing queue each keep one phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open, closing with its drain, or closed: lite's abort() now runs once, and the IETF connection's closing promise and closed flag become one value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e reopens The serving loop ended for good once the track ran out, and the end race then waited only for the fill. An update parked behind that fill could move the range back onto a group the cursor never handed out. The cursor still owed that group, but nothing polled it, so PUBLISH_DONE went out without the tail. Serving now ends only once no update or fill is in flight, and polls the cursor again after each one. This matches moq-net, whose TrackServe leaves Draining on an update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… readd keeps SYNCED behind queued changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…count Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quest-Claim: 579b8c0736b859c9
…om/moq-dev/moq into quest/m1/explicit-state/net-phases
…les the first's head Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… subscribes the next run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssued grant's queue in its open state Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alue Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 22 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed f4fc150. No actionable findings.
Checked the explicit Join, refusal, Lister, stale Effect, Reload, and Rust Following transitions against their callers and cleanup paths. Also inspected auth counter/grant transitions and supporting IETF/lite encoding and failure-close behavior. The state consolidation preserves ownership and terminal behavior in the inspected paths.
Verification limits: GitHub-only static review of all 13 changed files and surrounding source. Builds and tests were not run.
There was a problem hiding this comment.
ℹ️ Minor suggestions only.
Reviewed changes
Joinkeeps its head inside the phase:js/net'sendedvariant now carries the head's producer, andfinish(producer)/fail(err, producer)act only for the streamopengave it, so a duplicate fetch stream can no longer finish, abort, or replace the first stream's head. New tests cover both the ended and the mid-write cases.RequestStream.refuseresets on a failed write: both the method (throughserve) and the barerefuse()now abort the stream when the refusal cannot be encoded.Listeranswers through one slot: writes go through#answering, whichclose()clears, so a stale lister'sadd/synced/end/abortno longer reaches a successor's readers;#listingremains only forwanted/closed.Effect.getduring the stale window only peeks: aspawntask that outlives its run can no longer plant a subscription in the next run's unwatch list.- js/net auth recounts:
#setupPending/#pendingare derived from#presentedin#recountinstead of hand-adjusted at six sites, andIssuedGrantholds anopen/doneunion whoseclose()still flushes a grant queued just before it. Reload.statusderived once:#reportcomputes it fromestablishedand#dialing, replacing seven writes, with#serve/#setDialingas the only mutators.- Rust
Followingholds one value per role:last/moved/absorbedare oneLast, andlive/endedare oneHandedenum, including the refusal path that keeps the owed end without the broadcast. - Quest rewrite:
net-phases.mdrecords what landed here and keeps only the open items, withauth::Slot's need to open the stream beforegrantednoted; #8063 and #8222 are merged, #8220 still open. - Verification:
bun test netpasses (3180 pass, 3 skip) plusjs/signals; the Rust half was read rather than run.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
…l serves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Handed::Live's doc now describes the guarantee: the variant says a handed-out broadcast's end is not yet due, even when a refused takeover means it no longer serves the path, matching the refusal branch that clearslastwhile leavinghandeduntouched.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Continues moq-dev/moq#8275 on moq-net phase data: the items whose files no open PR holds. The quest file is rewritten to what remains.
Summary
Bugs, each reproduced by a new test before the fix:
Join(ietf/join.ts): after the track ended, a second fetch stream for the join replaced the head in#endedand leaked the first producer, never finished or aborted. A discarding second stream also calledfinish/fail, which settled or aborted the first stream's head mid-write. The head being written after the end now lives in theendedvariant, andfinish(producer)/fail(err, producer)act only for the streamopengave the head to.RequestStream.refuse()and the barerefuse(): a refusal whose write threw left the stream open. Both now reset the stream with the error (the method goes throughserve).Lister: afterclose()it still wrote into the listing, so a stale lister'sadd/synced/abortreached its successor's readers. It now answers through a#answeringslot thatclose()clears.@moq/signalsEffect.get(): aspawntask that outlived its run and calledget()in the stale window pushed a subscription onto the next run's unwatch list, so the next run reran on a signal it never read. A staleget()now only peeks.Redesigns with no reachable bug:
auth_session.ts):#pendingand#setupPendingare recounted from the presented set in one place (#recount, called from#recomputeand#present) instead of adjusted by hand at six sites;#settle/#unsettleare gone.IssuedGrantholds one#stateunion,opencarrying the queued grant ordone, andclose()still flushes a grant queued just before it.Reload.status: derived in one place (#report) fromestablishedand whether a dial is in flight, which#serveand#setDialingupdate. It used to be written at seven sites.Following(model/follow.rs):last,moved, andabsorbedare oneOption<Last>, andliveplusendedare oneHandedenum.Choices made without asking
IssuedGrant's reported race didn't reproduce. The quest said two updates in one tick both pass the async#donecheck. They do, but the failed write also cleared#queued, so the second update's write found nothing to send. I made the state a union anyway (queued only exists while open) and wrote no test for it.Effect.get()peeks without warning, since a task resuming after its run is expected.Lister's writes do nothing, rather than throwing. The callers inlite/subscriber.tsandietf/subscriber.tsclose it infinally, and Rust'sListercan't be used after drop.auth::Slot. Its tests callgrantedon slots whose stream never opened, so a phase enum needs those tests to open the stream first. That is a bigger change than this round had room for, so the quest says it. Also left: IETFSubscribeOnce,TrackIo, and js/netorigin.ts. The lite subscriber,fills.rs, and js/net liteSubscribeEntryitems still wait on moq-dev/moq#8220.Followingis per origin follower, and the JS changes are on control paths.Impact
Join,RequestStream, andIssuedGrantare@internal.ListerandReload.statuskeep their shapes, and only what happens afterclose()and when status flips change.Caused by
Each bug is as old as the code it lives in. No single later PR caused it.
Checks
just checkpasses locally: Rust (12833 tests), JS, lint, and quest.Suggested follow-ups
None beyond the quest's remaining items.
Models
Size: L
Implementer: Claude Code, claude-opus-5-5, high effort
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, L)
(Written by Claude Opus 5.5 with high)
🤖 Generated with Claude Code
Review
Rewritten by
quest reviewafter each round.01a1bc71-6360-7560-a31b-9223bba421fb, round 3The only follow-up change clarifies Handed::Live’s documentation to match existing behavior and introduces no defects.