Skip to content

fix(net,signals): js/net join, refusal, lister, and stale-effect phases; Reload status and Following hold one state - #8322

Merged
kixelated merged 18 commits into
mainfrom
quest/m1/explicit-state/net-phases
Nov 9, 2026
Merged

kixelated merged 18 commits into
mainfrom
quest/m1/explicit-state/net-phases

Conversation

@kixelated

@kixelated kixelated commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • js/net Join (ietf/join.ts): after the track ended, a second fetch stream for the join replaced the head in #ended and leaked the first producer, never finished or aborted. A discarding second stream also called finish/fail, which settled or aborted the first stream's head mid-write. The head being written after the end now lives in the ended variant, and finish(producer)/fail(err, producer) act only for the stream open gave the head to.
  • js/net RequestStream.refuse() and the bare refuse(): a refusal whose write threw left the stream open. Both now reset the stream with the error (the method goes through serve).
  • js/net Lister: after close() it still wrote into the listing, so a stale lister's add/synced/abort reached its successor's readers. It now answers through a #answering slot that close() clears.
  • @moq/signals Effect.get(): a spawn task that outlived its run and called get() 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 stale get() now only peeks.

Redesigns with no reachable bug:

  • js/net auth (auth_session.ts): #pending and #setupPending are recounted from the presented set in one place (#recount, called from #recompute and #present) instead of adjusted by hand at six sites; #settle/#unsettle are gone. IssuedGrant holds one #state union, open carrying the queued grant or done, and close() still flushes a grant queued just before it.
  • js/net Reload.status: derived in one place (#report) from established and whether a dial is in flight, which #serve and #setDialing update. It used to be written at seven sites.
  • Rust Following (model/follow.rs): last, moved, and absorbed are one Option<Last>, and live plus ended are one Handed enum.

Choices made without asking

  • IssuedGrant's reported race didn't reproduce. The quest said two updates in one tick both pass the async #done check. 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.
  • A stale Effect.get() peeks without warning, since a task resuming after its run is expected.
  • A closed Lister's writes do nothing, rather than throwing. The callers in lite/subscriber.ts and ietf/subscriber.ts close it in finally, and Rust's Lister can't be used after drop.
  • Left for the quest: auth::Slot. Its tests call granted on 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: IETF SubscribeOnce, TrackIo, and js/net origin.ts. The lite subscriber, fills.rs, and js/net lite SubscribeEntry items still wait on moq-dev/moq#8220.
  • No benchmark. Nothing here is per-track or per-group state: Following is per origin follower, and the JS changes are on control paths.

Impact

  • Public API: none. Join, RequestStream, and IssuedGrant are @internal. Lister and Reload.status keep their shapes, and only what happens after close() and when status flips change.
  • Wire: none.

Caused by

Each bug is as old as the code it lives in. No single later PR caused it.

Checks

just check passes 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 review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1bc71-6360-7560-a31b-9223bba421fb, round 3
  • Head: ce493ff
  • Base: origin/main at 9a07302
  • Score: 10/10, The only follow-up change clarifies Handed::Live’s documentation to match existing behavior and introduces no defects.
  • Findings: none

kixelated and others added 17 commits November 7, 2026 23:55
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>
…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>
@kixelated kixelated changed the title moq-net phase data fix(net,signals): js/net join, refusal, lister, and stale-effect phases; Reload status and Following hold one state Nov 8, 2026
@kixelated
kixelated marked this pull request as ready for review November 8, 2026 21:29
@coderabbitai

coderabbitai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fd352580-76a0-41c7-b850-c918c0542295
Commits

Reviewing files that changed from the base of the PR and between f7926f8 and ce493ff.

Files selected for processing: 13

  • js/net/src/auth_session.ts
  • js/net/src/connection/reload.ts
  • js/net/src/ietf/join.test.ts
  • js/net/src/ietf/join.ts
  • js/net/src/ietf/request_stream.test.ts
  • js/net/src/ietf/request_stream.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/listing.test.ts
  • js/net/src/listing.ts
  • js/signals/src/index.test.ts
  • js/signals/src/index.ts
  • quest/m1/explicit-state/net-phases.md
  • rs/moq-net/src/model/follow.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestions only.

Reviewed changes

  • Join keeps its head inside the phase: js/net's ended variant now carries the head's producer, and finish(producer)/fail(err, producer) act only for the stream open gave 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.refuse resets on a failed write: both the method (through serve) and the bare refuse() now abort the stream when the refusal cannot be encoded.
  • Lister answers through one slot: writes go through #answering, which close() clears, so a stale lister's add/synced/end/abort no longer reaches a successor's readers; #listing remains only for wanted/closed.
  • Effect.get during the stale window only peeks: a spawn task that outlives its run can no longer plant a subscription in the next run's unwatch list.
  • js/net auth recounts: #setupPending/#pending are derived from #presented in #recount instead of hand-adjusted at six sites, and IssuedGrant holds an open/done union whose close() still flushes a grant queued just before it.
  • Reload.status derived once: #report computes it from established and #dialing, replacing seven writes, with #serve/#setDialing as the only mutators.
  • Rust Following holds one value per role: last/moved/absorbed are one Last, and live/ended are one Handed enum, including the refusal path that keeps the owed end without the broadcast.
  • Quest rewrite: net-phases.md records what landed here and keeps only the open items, with auth::Slot's need to open the stream before granted noted; #8063 and #8222 are merged, #8220 still open.
  • Verification: bun test net passes (3180 pass, 3 skip) plus js/signals; the Rust half was read rather than run.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Comment thread rs/moq-net/src/model/follow.rs Resolved
…l serves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 clears last while leaving handed untouched.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 38c0339 Nov 9, 2026
6 checks passed
@kixelated
kixelated deleted the quest/m1/explicit-state/net-phases branch November 9, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant