Skip to content

feat(js/net): enforce the subscriber latency budget - #2919

Merged
kixelated merged 1 commit into
claude/latency-enforcementfrom
claude/latency-enforcement-js
Aug 18, 2026
Merged

feat(js/net): enforce the subscriber latency budget#2919
kixelated merged 1 commit into
claude/latency-enforcementfrom
claude/latency-enforcement-js

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Stacked on #2890, which does the same work in rs/moq-net. Split out so each language is reviewable on its own; this was originally filed as #2892.

Summary

js/net carried latencyMax as metadata and never acted on it. It now applies the same drift budget as it reads, and js/watch requests the playback latency ceiling from its first subscription instead of only after acceptance.

  • Drift is measured against both a presentation-time and a wall-clock live edge; either age can expire a group. The arrival clock backstops a stalled or empty group with no timestamp, and uses performance.now() so it does not react to system-clock changes.
  • Drift is measured from the reader's position, not the group's first frame. A reader that has drained a group sits at its newest frame, so a 2s GOP is no longer 2s behind by construction. A group nobody has started reading still sits at its own start, which leaves group selection unchanged.
  • Only a read that would wait is judged. A group with frames in hand always drains: the budget bounds a group that has stalled while the live edge moved on, not a reader slower than the wire. This also keeps the policy off the read path, where it would otherwise rescan the timeline and attach a signal subscription per operation.
  • A drained group the budget gives up on ends rather than fails, since giving up where the reader already stands loses nothing. The latency error is reserved for a caller still holding content: a publisher part-way through writing a frame, or retention eviction discarding frames nobody read (which reports the budget as the more specific reason, falling back to Lagged).
  • fetchGroup stays exempt: a fetch names historical content explicitly, so there is no live edge to be late against.

Public API changes

None. Enforcement and the watch subscription plumbing use package-internal helpers (hooks in internal.ts), so no exported surface moved.

Wire behavior changes

None on the encoding. What changes is what a local subscriber is handed: a group that has drifted past its own latencyMax is skipped rather than delivered, and one that stalls after handoff ends (or fails, if content was unread) instead of waiting indefinitely. The publisher side resets a group stream whose encode or write is blocked as the group expires, which it previously would not have done. The default budget is zero, so a delayed live subscriber deliberately advances to the live edge; callers that need history must request a replay window.

Cross-package sync

Mirrors rs/moq-net in #2890. No draft change: the wire format and parameters are unchanged, and the existing draft already specifies the two age backstops.

Test plan

  • nix develop --command just check
  • nix develop --command just test: 3258 Rust tests pass, 2 skipped; all JS suites green, including 517 @moq/net and 147 @moq/watch.
  • Two regression tests were verified to fail with only their own half of the fix reverted:
    • real time reads a live stream without truncating it — 2s GOPs read as they arrive at the default budget, with the group's close landing after its successor's first frame.
    • a budget is measured from the reader's position — a straggler frame arriving after the next group opened still reaches a reader that is inside its budget.

(Written by Opus 5)

@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: da5a4d2478

ℹ️ 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 thread js/net/src/group.ts
this.#closed.set(closed);
return;
}
if (this.#expire()) return;

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 Preserve buffered frames while watching closure

When an open group has been handed out with unread frames and a newer group advances beyond its budget, a subsequent frame arrival on the old group wakes #runClosed; this unconditional #expire() marks the group as cleanly ended even though #state.frames still contains unread data. Later tryReadFrame or readFrame calls then return undefined, silently truncating an overlapping group despite the stated rule that buffered frames are always drained. Only apply this non-truncating expiry after the frame buffer is empty. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment thread js/net/src/track.ts
const drift = this.#drift();
for (let i = 0; i < groups.length; ) {
const group = groups[i];
if (!this.#isStale(group, drift)) {

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 Measure flattened reads from their current frame cursor

When callers use Subscriber.readFrameSequence() or readFrame() on an open multi-frame group, this stale check supplies no cursor Position, so it compares the live edge with the group's first timestamp instead of the last frame consumed. For example, after consuming through 1900 ms in a group that began at 0, a 2000 ms group makes the old group fail a 1-second budget and removes it, even though a late 1950 ms tail frame is only 50 ms behind. Track-level reads need to measure drift from their current frame position just like handed-out group consumers do. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

The JS mirror of the moq-net work in the parent PR: js/net applies the
same drift budget as it reads, and js/watch requests the playback
latency ceiling from its first subscription.

Drift is measured against both a presentation-time and a wall-clock live
edge, from the reader's own position rather than the group's first frame,
so a reader keeping pace is not convicted by how long ago its group
opened. A group with frames in hand always drains; only a read that would
wait is judged, which is also what keeps the policy off the read path. A
drained group the budget gives up on ends rather than fails, since
nothing was lost; the latency error is reserved for a caller still
holding content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the claude/latency-enforcement-js branch from da5a4d2 to 4b63fb6 Compare August 18, 2026 21:41

@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: 4b63fb6e74

ℹ️ 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 thread js/net/src/group.ts

#evict() {
const frames = this.#state.frames.peek();
if (frames.length > 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.

P1 Badge Cancel pending writes when evicting a drained mirror

When the only frame has already been dequeued into a slow IETF or Lite encode, frames is empty even though hooks.guardGroup still owns unread bytes. If retention pruning runs then, this condition skips expiry, Producer.#evict immediately removes the group from timeline, and subsequent guard checks can no longer recognize it as stale, so the blocked write continues past the latency and retention budget. Track pending guarded operations and expire them before removing the timeline entry even when the frame queue is drained. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

@kixelated
kixelated merged commit 4b63fb6 into claude/latency-enforcement Aug 18, 2026
2 checks passed
@kixelated
kixelated deleted the claude/latency-enforcement-js branch August 18, 2026 23:25
@kixelated

Copy link
Copy Markdown
Collaborator Author

Closed in error, not actually merged. A force-push of mine briefly put these commits onto the base branch (claude/latency-enforcement), which made GitHub see this PR's head as reachable from its own base and auto-close it as merged.

Nothing landed on dev. The base branch has been corrected and now carries no js/ changes. This work continues in #2926.

(Written by Opus 5)

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