Skip to content

feat(net): count lite-07 group streams in SUBSCRIBE_END - #4118

Merged
kixelated merged 5 commits into
mainfrom
quest/m1/lite-stream-count
Sep 25, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m1/lite-stream-count

Conversation

@kixelated

@kixelated kixelated commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

moq-lite accounts for a subscription's groups with SUBSCRIBE_DROP: a subscriber is done once every sequence from start to end has a stream or a drop. Neither publisher ever sends SUBSCRIBE_DROP, so a skipped group can only be given up on after a grace. moq-transport solves the same problem with PUBLISH_DONE's Stream Count. lite-07 is unpublished, so it can adopt the count now.

Approach

  • Wire (lite-07 only): SUBSCRIBE_END gains Stream Count, the number of group streams the publisher opened for the subscription. SUBSCRIBE_DROP and its type 0x2 are gone; a 0x2 response decodes as an unknown type. The draft's Subscribe Stream and SUBSCRIBE_END sections are reworded, and the changelog is updated. lite-01 to -06 encode and decode exactly as before.
  • Rust publisher: each subscription shares an Opens counter with its group machines. A group is pending from when it is queued until its stream opens (counted) or it gives up first (expired, or the open failed; not counted). On lite-07 SUBSCRIBE_END is sent once the track is finished and no group is still pending, instead of as soon as the boundary is known.
  • JS publisher: #runGroup is split into #openGroup and #serveGroup, so the serving loop can count successful opens and wait for pending ones before SUBSCRIBE_END. On lite-07 the early END for a boundary held behind a cap is skipped for the same reason. The subscribe FIN still waits for every group stream to finish, including one that has not opened yet.
  • Relays count their own downstream streams, because the counter lives in the serving side.
  • Subscribers decode the count and ignore it for now (see Follow-ups).
  • Tests: codec tests in both languages (END with and without the count, DROP gone on 07). Publisher tests in both languages: a skipped group is not counted, a count of zero, END waits for a pending open, and a group whose stream never opened is not counted.

Impact

  • Wire: moq-lite-07 (moq-lite-07-wip, opt-in only) SUBSCRIBE_END carries Stream Count, SUBSCRIBE_DROP is removed from 07, and SUBSCRIBE_END on 07 arrives once every counted stream is open, not as soon as the final group is known. Published versions are unchanged. The default offer list still starts at moq-lite-06.
  • Public API: none. lite::SubscribeEnd gains a streams field in Rust and JS, but neither type is exported.
  • Behavior: on lite-07 a subscriber whose cap holds back groups below the end no longer learns the end until the cap is raised, since the count cannot be final before then. The same applies to a Rust subscriber's early finish_at.

Alternatives

  • Subscriber settle in this PR: the quest also asked subscribers to stop waiting once the count is reached. The JS tail (fix(net): deliver a track's tail up to its declared end #4086) and the Rust tail (fix(net): deliver a Rust track's tail up to its declared end #4116) have landed. This lands the wire and both publishers. The subscriber check, its tests, and the Rust-JS interop case are the follow-up quest lite-count-settle.
  • Counting finished streams instead of opened ones: this would delay SUBSCRIBE_END to the last FIN. That is what the FIN already signals, and it gives the subscriber nothing early.
  • Option<u64> for the count: the version already says whether the field exists, so a plain u64 that decodes as 0 before lite-07 is simpler.

Follow-ups

(Written by Grok 4.7)

@kixelated
kixelated force-pushed the quest/m1/lite-stream-count branch from 8c7a6f7 to e998717 Compare September 25, 2026 21:25
@kixelated
kixelated marked this pull request as ready for review September 25, 2026 21:26
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 566242da-1509-447b-9577-ee40c0790793

📥 Commits

Reviewing files that changed from the base of the PR and between c87159d and a31e625.

📒 Files selected for processing (13)
  • drafts/draft-lcurley-moq-lite.md
  • js/net/src/lite/publisher.test.ts
  • js/net/src/lite/publisher.ts
  • js/net/src/lite/subscribe.test.ts
  • js/net/src/lite/subscribe.ts
  • js/net/src/lite/version.ts
  • quest/m1/README.md
  • quest/m1/announce-compression.md
  • quest/m1/lite-count-settle.md
  • quest/m1/lite-stream-count.md
  • rs/moq-net/src/lite/publisher.rs
  • rs/moq-net/src/lite/subscribe.rs
  • rs/moq-net/src/lite/version.rs
💤 Files with no reviewable changes (2)
  • quest/m1/announce-compression.md
  • quest/m1/lite-stream-count.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.


Walkthrough

Lite-07 adds a stream count to SUBSCRIBE_END and removes SUBSCRIBE_DROP for stream-count versions. JavaScript and Rust response codecs apply the count according to protocol version. Both publishers report successfully opened group streams and wait for pending opens to settle before sending the end response. Tests cover codec compatibility and publisher stream-count behavior. The draft and quest documentation also reflect the protocol and planned subscriber settlement changes.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to a31e6

No actionable merge-blocking issue remains; the publisher can send SUBSCRIBE_END after pending stream opens settle.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a31e6

No new privilege or data-access path was established. The opt-in protocol version changes when a subscription reports completion, while receivers do not yet use its new stream count; deployment exposure remains uncertain.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A remote subscriber can influence its subscription's cap and cancellation timing, including when lite-07 END becomes eligible. The inspected count belongs to that subscription; no broader authority or tenant-level exposure was established.

Trust Boundaries and Controls

  • observed — The inspected JavaScript subscription path still resolves the requested broadcast through the existing publisher path, applies requested group bounds, cancels queued opens when the subscriber leaves, and guards and cleans up group serving. Lite-07 type-2 responses are rejected by the changed decoders.

Resilience and Maintainability Implications

  • observed — Normal Rust completion retains child group tasks through the drain before FIN; peer FIN instead cancels them without sending END. JavaScript tracks opening separately from serving and waits for serving tasks before its normal subscription FIN.

Hardening Proposals

  • proposed — When implementing stream-count settlement in subscribers, bound waits for counted streams whose headers never arrive, as the new wire contract recommends; this is follow-up hardening, not a verified PR-introduced vulnerability.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 8 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: adding lite-07 group-stream counting to SUBSCRIBE_END.
Description check ✅ Passed The description directly explains the wire changes, publisher behavior, version compatibility, tests, impact, and follow-up work described by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 62.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 8 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR

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

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9987177cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +771 to +772
if (countStreams) while (opening.size > 0) await Promise.all(opening);
const end = new SubscribeEnd(boundary(), streams);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Delay lite-07 END until the track actually drains

When a producer calls finishAt() before writing all groups below that boundary, tryRecvGroup() can return idle and the branch at lines 833-835 calls sendEnd() immediately. This wait covers only groups already popped into opening; later groups below the declared end can still arrive and open streams after SUBSCRIBE_END, so they are omitted from Stream Count and a subscriber may settle early. For counting versions, defer END until tryRecvGroup() returns done, just as the boundary fast path is already disabled.

Useful? React with 👍 / 👎.

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.

Agreed. On lite-07 the idle path no longer sends SUBSCRIBE_END when finishAt is ahead of the live edge. The count waits until the track is done, so a group written after the declaration is opened before the count goes out. Rust already skipped that early boundary when the count is on. Covered by lite draft-07: subscribe end waits for groups below a declared finish.

(Written by Grok 4.7)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T22:12:14.258744Z a31e625 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated
kixelated enabled auto-merge (squash) September 25, 2026 21:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5722d88b09

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Type (i) = 0x1
Message Length (i)
Group (i)
Stream Count (i)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update the concept guide for the lite-07 wire change

This adds Stream Count to lite-07 SUBSCRIBE_END and removes SUBSCRIBE_DROP, but doc/concept/moq-lite.md still describes lite-07 only in terms of hidden-announcement discovery and never explains the new subscription completion semantics. Update the concept documentation in this change so implementers do not rely on the obsolete drop-based behavior.

AGENTS.md reference: AGENTS.md:L94-L98

Useful? React with 👍 / 👎.

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.

The concept page never described SUBSCRIBE_DROP or how a subscription completes. It points at the draft for the wire, and this PR updates that draft. There is no obsolete drop behavior there for implementers to follow. Hidden-announce wording is a separate lite-07 feature and stays as written.

(Written by Grok 4.7)

kixelated and others added 5 commits September 25, 2026 14:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq-lite-07's SUBSCRIBE_END gains Stream Count, the number of group streams
the publisher opened for the subscription, and SUBSCRIBE_DROP is removed from
lite-07. The Rust and JS publishers count the streams they open and send
SUBSCRIBE_END once no served group is still opening its stream. Earlier
versions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open promise resolves to a writer, so the set that SUBSCRIBE_END waits on
cannot be Promise<void>.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
finishAt can name the end while groups below it are still being written.
Sending the count on that idle path omits streams that open afterwards.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
@kixelated
kixelated force-pushed the quest/m1/lite-stream-count branch from 5722d88 to a31e625 Compare September 25, 2026 22:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a31e62556d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +477 to +480
static async decode(r: Reader, version: Version): Promise<SubscribeEnd> {
return Message.decode(
r,
async (r) => new SubscribeEnd(await r.u53(), hasStreamCount(version) ? await r.u53() : 0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Settle lite-07 tails from the decoded stream count

For lite-07 this field is decoded but never used by either subscriber: JS still completes via tail.covers(start, end) in subscriber.ts, and Rust records only end.group before settling an owed sequence range. Sparse subscriptions therefore wait for the full grace period even after every counted stream has arrived; worse, if datagrams already account for the sequence range, the subscriber can remove the subscription before a counted group stream whose QUIC delivery trails SUBSCRIBE_END/FIN arrives, causing that stream to be rejected. Store the count and keep the subscription routable until tail.streams() reaches it, subject to the existing reset grace.

Useful? React with 👍 / 👎.

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.

Intentional. This PR lands the wire and both publishers. Subscribers decode Stream Count and ignore it until quest lite-count-settle, which is the subscriber check, its tests, and the Rust-JS interop case. On lite-07 a skipped group still waits out the grace until that lands.

(Written by Grok 4.7)

@kixelated
kixelated merged commit 6d637a0 into main Sep 25, 2026
6 checks passed
@kixelated
kixelated deleted the quest/m1/lite-stream-count branch September 25, 2026 22:37
@kixelated

Copy link
Copy Markdown
Collaborator Author

Landed on main as 6d637a0.

lite-07 (moq-lite-07-wip, off by default) SUBSCRIBE_END now carries Stream Count, and SUBSCRIBE_DROP is removed on that version only. lite-01 through lite-06 are unchanged. Subscribers decode the count and still ignore it; settling on it is quest lite-count-settle.

(Written by Grok 4.7)

@moq-bot moq-bot Bot mentioned this pull request Sep 25, 2026
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