Skip to content

feat(moq-c): JSON window producer and consumer - #7082

Merged
moq-bot[bot] merged 13 commits into
mainfrom
quest/m1/c/json-window
Oct 30, 2026
Merged

moq-bot[bot] merged 13 commits into
mainfrom
quest/m1/c/json-window

Conversation

@kixelated

@kixelated kixelated commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

moq-c now publishes and reads moq-json windows, as moq-ffi's MoqJsonWindowProducer / MoqJsonWindowConsumer do. This closes the last moq-c parity gap that moq-dev/moq#6946 found. Completes quest/m1/c/json-window.md.

Changes

  • Publish: moq_publish_json_window(broadcast, name, name_len, config) creates the track. Edit it with moq_publish_json_window_push (a JSON document), _pop(count), _cut and _finish.

    • The track has a millisecond timescale and a refresh ratio of 8. A task restates the window for joiners until the track finishes, like the JSON snapshot.
    • The window is not advertised in the catalog, which has no window mode.
    • Closing the broadcast ends an open window with UNROUTABLE and releases its handle, like every other child.
  • Consume: moq_consume_json_window(broadcast, name, name_len, on_event, user_data) delivers event ids. moq_consume_json_window_event fills moq_json_window_event:

    • kind: MOQ_JSON_WINDOW_KIND_PUSH, _POP or _SKIP.
    • [start, end): the absolute index range. A push covers the single record at start.
    • json: the pushed record, or NULL for a pop or skip.
    • timestamp_us / timestamp_present: the frame's timestamp.

    Release each event with moq_consume_json_window_event_free. moq_consume_json_cancel now stops window consumers too.

  • Cancel during subscribe: moq_consume_json_cancel now stops a snapshot, stream, or window consumer that is still waiting for its subscription to be accepted. Before, a publisher that never answered the subscribe kept the task running with no terminal callback, so user_data could not be released. A shared Consume::json_subscribe races the close against subscribe(None).

  • moq_json_window_config is the producer's config, with one field: checkpoint_records (0 restates the whole window). Following moq-dev/moq#7046, the track is compressed like every moq-c JSON track (compress: true on its track info), and the consumer takes no config.

  • Refactor: the snapshot's refresh holder and task (JsonSnapshot, json_snapshot_refresh) become a generic Refreshed<T> plus Publish::refreshed, so the snapshot and the window share one task. Behavior is unchanged.

  • Docs: doc/lib/c gains a JSON windows bullet. doc/lib/layers.md drops "the C API has no window yet". The shared feature list in doc/lib/index.md now names windows.

Impact

  • Wire: none.
  • Public API (moq-c only): additive. It adds moq_json_window_config, moq_json_window_event, moq_json_window_kind, moq_publish_json_window{,_push,_pop,_cut,_finish} and moq_consume_json_window{,_event,_event_free}. moq_consume_json_cancel also accepts a window task, and now also works on a consumer still subscribing. Nothing to migrate.

Choices made without asking

  • Constructed by broadcast and name, not over a raw track handle. moq-ffi adopts a MoqTrackProducer/MoqTrackConsumer. moq-c's JSON snapshot and stream take broadcast, name, so the window follows its siblings. As a result, the caller cannot pick the timescale or refresh ratio. They are fixed at milliseconds and 8; 8 matches the snapshot and moq-mux's HLS window.
  • checkpoint_records = 0 means the whole window. moq-ffi takes Option<u64> and refuses Some(0). Here a zeroed config is the default, as everywhere in moq-c, and 0 has no other meaning.
  • A push is flattened to [start, start + 1). moq-ffi has Push { index }. A single start/end pair keeps the C struct flat, with no union.
  • A finished window stays served until moq_publish_close. This matches raw tracks and media and the "Ending cleanly" promise in doc/lib/c. The test checks that a reader joining after the finish still gets the window. Mutating keep to a drop fails it. The JSON snapshot and stream drop their producer on finish instead (see Follow-ups).
  • No demand on the window producer. moq-ffi's JSON producers have demand(), but moq-c's JSON snapshot and stream have no demand watcher either. I left it for a parity follow-up rather than adding it to one mode.

Decisions

Verification

  • cargo test -p moq-c --lib json_ passes:
    • json_cancel_while_subscribing: for each of the snapshot, stream, and window consumers, moq_publish_requests leaves the subscribe pending, then a cancel delivers terminal 0. Without the fix it times out (checked by making json_subscribe ignore close).
    • json_window_publish_consume: push, pop, clock stamps, NULL json on a pop, no catalog entry, and broadcast close aborting an open window.
    • json_window_refreshes_a_bounded_checkpoint: a raw joiner past the ratio receives a restated group holding only the 2-record checkpoint, and a reader joining after the finish gets SKIP [0, 38), then the two pushes, then a clean end.
  • After merging main at d33b591, branch-wide just check passes, including clippy and the moq-c tests.
  • just rs c-tests passes. The generated moq.h exports every new symbol.
  • just test interop --all was not run: this PR has no wire, moq-ffi, or gateway change.

Review

  • Codex gpt-6.1-sol, medium effort, round 1 on head 09ede4f against origin/main d33b591: 0 findings.
  • Online Codex review on f0be61d:
    • P2 (cancel while subscribing): fixed in 09ede4f for all three JSON consumers (thread, resolved).
    • P1 (kio polling instead of tokio::select!/oneshot): declined as out of scope (thread). It is listed below as a follow-up.

Follow-ups

  • Cancel during subscribe in the other moq-c consumers. The catalog, raw track, datagram, and media consumers also await SUBSCRIBE_OK before they poll close, so a cancel there has the same gap this PR fixes for JSON. Consume::json_subscribe could become a shared helper for all of them.

  • moq-c consumer tasks on kio. Every moq-c consumer loop drives a tokio::select! over a oneshot close. rs/AGENTS.md asks for poll_* logic with kio primitives. Convert them across the crate together, not just the window.

  • moq-c JSON and binary producers drop on finish. moq_publish_json_{snapshot,stream}_finish and moq_publish_binary_{snapshot,stream}_finish drop their producer instead of keeping it like raw tracks, media and now the window. A subscriber who names the track after the finish is refused, which contradicts the "Ending cleanly" doc. Each should Children::keep its producer.

  • moq-c JSON producers have no demand watcher. moq-ffi's JSON snapshot, stream and window producers expose demand(); moq-c's have nothing like moq_publish_track_demand. This is a parity gap for the m1 cut, covered by the moq-c parity epic's exports-vs-moq.h diff.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

@kixelated kixelated changed the title moq-c JSON window producer and consumer feat(moq-c): JSON window producer and consumer Oct 28, 2026
@kixelated
kixelated marked this pull request as ready for review October 29, 2026 16:12
@coderabbitai

coderabbitai Bot commented Oct 29, 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 50 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: 29d26ad3-0238-4e76-af9b-d989eedbf318
Commits

Reviewing files that changed from the base of the PR and between d33b591 and 09ede4f.

Files selected for processing: 9
  • doc/lib/c/index.md
  • doc/lib/index.md
  • doc/lib/layers.md
  • quest/m1/c/README.md
  • quest/m1/c/json-window.md
  • rs/moq-c/src/api.rs
  • rs/moq-c/src/consume.rs
  • rs/moq-c/src/publish.rs
  • rs/moq-c/src/test.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

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 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-10-30T04:21:15.146701Z 09ede4f 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.

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

ℹ️ 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 rs/moq-c/src/consume.rs
Comment on lines +1038 to +1041
let event = tokio::select! {
biased;
_ = &mut close => return Ok(()),
next = consumer.next() => match next? {

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 Drive the new window loop through kio polling primitives

The new window loop introduces async-only logic using tokio::select! and tokio::sync::oneshot, contrary to rs/AGENTS.md, which requires new logic to have a poll_* implementation and use kio primitives. The underlying window consumer already exposes poll_next, so implement cancellation and event polling with kio and make the async loop a wrapper instead of adding another Tokio-specific implementation.

AGENTS.md reference: rs/AGENTS.md:L25-L30

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.

Out of scope for this PR: run_json_window mirrors run_json_snapshot and run_json_stream, and every other moq-c consumer loop in consume.rs drives a tokio::select! over a oneshot close. Moving only the window to kio polling would split one crate across two models. Converting all moq-c consumer tasks to kio is a crate-wide follow-up, and I'm proposing it as one.

(Written by Claude Opus 5.5)

Comment thread rs/moq-c/src/consume.rs Resolved

@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 commit: f0be61dad86aa95fa9ee2ea333841ccd79fcb700.

Confirmed one actionable issue, already reported in 4414876721: P2, cancellation during subscription setup (rs/moq-c/src/consume.rs:1022–1025). The new window task awaits subscription acceptance before polling its close receiver; an unanswered subscription therefore prevents the promised terminal callback after cancel, retaining the task and caller's user_data. Observe cancellation during setup as well as event delivery, and cover cancellation before acceptance. Referencing the existing finding rather than duplicating an inline.

Direction: reusing the window codec and shared refresh holder is proportionate; retaining finished producers preserves late joins. Prefer the repository's poll/kio approach for the new consumer lifecycle (also noted in 4414876716). No additional concrete defect found in event ownership, timestamp conversion, checkpoint configuration, or broadcast-close handling.

Limits: GitHub-only static review of the diff, task/refresh lifecycle, window producer/consumer/decoder and tests. No builds, tests, C-header generation, or live probes run.

(Written by OpenAI)

@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 commit: 09ede4f969b6a08207f3f6c242ab5aa757ee244c.

Prior P2 fixed: cancellation before subscription acceptance (existing finding). Consume::json_subscribe now races the close receiver against setup for snapshot, stream and window consumers, prioritizes cancellation, and returns through the existing terminal-callback cleanup. The new regression deliberately leaves each subscription unanswered before cancelling, targeting the original stall.

No new actionable bugs found. Sharing this helper is proportionate, and adapting window configuration to the merged compressed-track API is consistent with its siblings. The broader consumer polling migration and cancellation gaps in other consumer modes remain documented follow-ups, not new findings here.

Verification limits: GitHub-only static review of the fix, lifecycle, base adaptation and regression, with an independent pass. I did not run builds, tests, header generation or live C probes.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary for head 09ede4f969b6a08207f3f6c242ab5aa757ee244c:

  • Reviews: Codex 👍 on this head (no findings). The automated maintainer-account review of this head found the earlier cancel-while-subscribing P2 fixed and reported nothing new. The kio-polling P1 was declined as out of scope (a crate-wide follow-up) and has a reply.
  • Decisions: timestamps land as is, per the maintainer's /quest-complete pick. Whichever of this PR and feat(bindings)!: exact {value, scale} timestamps in every binding #6946 merges second converts the window to const moq_timestamp *.
  • Checks: all head checks pass. quest check --base origin/main passes, and the PR deletes quest/m1/c/json-window.md and its link.
  • Follow-ups (for /quest-plan): cancel-during-subscribe in the other moq-c consumers, moq-c consumer tasks on kio, JSON/binary producers that drop on finish, and a demand watcher for moq-c JSON producers.

Landing through the merge queue.

(Written by Claude Opus 5.5)

kixelated and others added 8 commits October 30, 2026 08:32
* chore(quest): claim quest/m1/resume-model-test.md

Quest-Claim: 7bff411939571787

* test(net): check resume against a model of one logical track

A seeded proptest drives an origin's fronts through route flips, route
failures, give-ups, ranges, cache expiry, a newer epoch, and reader joins
and leaves on the simulated clock, and checks every reader's groups,
frames, drops, and end against a plain cursor on the logical track.

Fixes what it found:
- An ordered reader never ended once a finished track left nothing in
  its range, or the rest of the range was given up.
- A drop poll with local bounds below the subscription's floor built a
  reversed span and panicked.
- A resumed reader held a drop back for a replaced copy kept only to back
  a group still read, and was not woken once that read finished.

* test(net): check JS resume against the logical-track model

Mirror rs/moq-net's resume model as a fast-check property over the same
ops, constants and checks, seeded by MOQ_RESUME_SEED and sized by
MOQ_RESUME_CASES (default 64).

It found three bugs, each fixed with a regression case:
- A replacement copy's refusal ended a group the serving route still
  delivered; it is a verdict only once that route won't land it either.
- A failed copy's cache eviction released a group recovering from
  another copy; only the copy a mirror reads from reclaims it.
- A reported drop was final, so a group a later copy delivered was
  never handed out; the report is now once and not final.

* ci: sweep the resume model nightly from a fresh seed

* test(net): answer route fetches in the resume model

A real route forwards a fetch for a group it lacks to the publisher. The
model's routes refused every fetch, so a group continued across a
failover read NotFound although the instance carried it.

* test(net): let model routes hold a tail fill beside the live copy

A frame-bounded fetch caches a partial copy of a group, so a later
whole-group fetch must still be answered. A group a reader stitched
from two copies is complete once any copy wrote its last frame.

* fix(net): keep a recovered group when its old copy is let go

Releasing a copy's subscription released every mirror it cached,
including a group recovery had already moved to another copy, which
then ended cleanly short of its frames. The JS model's routes now
answer fetches, which exposed it.

* quest: finish resume-model-test, plan delivering late groups below the resume point

The maintainer kept the resume point policy on #6947; delivering the
late lower groups is a nice-to-have follow-up in m2.

* test(net): request the model's broadcast through the renamed origin API

* test(net): resume readers only on a route Up or Down actually changes

* test(net): restore Recover's refusal wait, and resume Rust readers only when the cheapest route changes

The main merge took main's whole resume.ts and dropped the PR's Recover hunk.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(quest): claim quest/m1/quic-api/streams.md

Quest-Claim: 3067217ff870abc5

* wip(quic): namespace moq-quic streams as send, recv, and stream

send::{Stream, Offset, End, Options, Group, Groups, Error}, recv::{Stream,
Lease, Chunks, Error}, and stream::{Id, Dir}. Writes take Buf, a lease moves
its chunks out and holds no borrow, send ends stay queryable until released,
and datagrams chain chunks without copying.

* wip(qmux): use moq-quic send ends and Buf writes

* wip(tokio): namespace the QUIC driver as send, recv, and stream

send::{Stream, Finished, Group, Options, Offset, Error} with finish(self)
typestate and consuming resets, recv::{Stream, Lease, Error} with a lease
that owns its bytes and holds no lock, Buf writes and BufMut reads, and
datagrams that chain a header. quic::web collapses onto one error per
direction. moq-quic only reruns the stuck pass on restore when an out lease
held it off.

* wip(uring): namespace the QUIC driver as send, recv, and stream

* wip(uring): port tests and moq-relay to the namespaced driver

* wip(uring): format

* wip(quic): name the namespaced stream API in the fork notes

* wip(quic): fix a stale doc link

* chore(quest): finish quic-api/streams

* fix(quic): keep terminal stream state across the review's lifecycle gaps

- moq-tokio and moq-uring cache a reset `received_reset` reports, since
  moq-quic frees the stream then, so later reads and leases fail with it
  instead of panicking.
- moq-uring's web `poll_closed` observes a clean FIN again through an empty
  read, instead of a reset-only query that waits forever.
- moq-uring's web `finish` keeps a finished stream's handle when called again.
- qmux resets an unfinished stream on drop before releasing its ID, which
  refused the reset.

* fix(quic): size a datagram before taking its payload

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… on moq-dev pairs (#7073)

* chore(quest): claim quest/m0/smoke-released.md

Quest-Claim: 1a9318c095d636e0

* fix(smoke): hold the Cloudflare probe's track reader

The probe dropped its TrackReader, so when the relay rejected an early
SUBSCRIBE with DoesNotExist, cloudflare/moq-rs failed to close the track
and resolved the subscription as done. The harness then failed the cell
instead of retrying the readiness race.

Also rewrite the smoke-released quest to the classified runner failures.

* fix(smoke): gate the upstream runner on moq-dev pairs only

Only moq-dev-rs and moq-dev-js against this checkout's relay fail the
Upstream Docker interoperability job; every other pair is reported, as
relays.sh treats non-moq-dev relays. A required pair the registry stops
pairing fails too. With the Cloudflare probe fix this finishes the quest,
so delete it.

* fix(smoke): annotate optional runner failures; record the moq-rs close bug

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(quest): claim quest/m0/nightly/quic-rustdoc.md

Quest-Claim: 0a0d799823f82f7f

* fix(quic): unbreak moq-quic's all-features rustdoc

The fuzz-gated bench map linked to the private SentPackets type, which
fails rustdoc under -D warnings. Nightly's features lane already builds
workspace rustdoc with --all-features, so no new lane is needed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(quest): claim quest/m0/ci/tracing-filter.md

Quest-Claim: a26a0b73687228f9

* wip: stop forcing tracing-test's no-env-filter

* chore(quest): finish quest/m0/ci/tracing-filter.md

* test: keep captured spans enabled beside parallel tests

With one dispatcher registered, tracing-core caches a callsite's interest
from the thread that first hits it. A parallel test without a subscriber
then disabled the `srt` ingest span for the capturing test, so
`publisher_reports_a_silent_pid` failed about two runs in three under
`cargo test`. A second, no-op dispatcher makes every callsite ask the
current thread's subscriber.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore: point quest and rust2 references at moq-dev

Both repositories moved from the kixelated account to the moq-dev
organization. The flake input keeps the same revision and hash.
Generated quest skills refresh with the next quest bump.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(quest): claim quest/m0/ci/mbx-r2-verify.md

Quest-Claim: 9a556a77ba203b39

* quest(m0/ci): record the first mbx 1.28 Nightly in the R2 verify quest

The bump's checks passed: every hosted Nightly job writes, macOS refused
only an already oversized manifest, and the merge queue shows no E0463.
Reuse is still absent on every platform; record the lockfile-identity
evidence and narrow the quest to what remains.

* ci(platform): install uniffi-bindgen-cpp after the Rust build

The Nightly Windows build loaded the install's 124-prediction mbx
manifest under its own identity on 2026-10-27 and 2026-10-28, which
blocked inheriting the previous lockfile's manifest. Installing after
the build lets the build inherit first. Reported upstream as
jdx/mr-boxington#869.

* quest(m0/ci): record upstream's CRLF cause for the Windows cache miss

jdx/mr-boxington#870 traced the Windows miss to CRLF lockfile digests,
not the uniffi-bindgen-cpp install, and the macOS build did inherit.

* quest(m0/ci): revert the uniffi-bindgen-cpp step move, record settled decisions

#870 showed the Windows miss comes from CRLF lockfile digests, not the
install order, so platform.yml keeps main's order. Windows now waits on an
mbx release with #870 and a pin bump; Linux full history waits on Windows
hitting; lockfile churn past 8 states counts under the rebuild item.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…pshot before parsing (#7072)

* chore(quest): claim quest/m0/ci/stats-oversize.md

Quest-Claim: 4a6d2df5874a8c48

* perf(json): refuse an oversized snapshot before parsing or compressing it

* chore(quest): finish quest/m0/ci/stats-oversize.md

* feat(json): a public snapshot group budget, refusing a snapshot past it before parsing

snapshot::Config gains max_group_bytes (JS maxGroupBytes, no longer
internal), clamped to moq-net's cache limit. The encoder refuses a
snapshot past it before the baseline parse, so moq-stats' oversize tests
lower it to 64 KiB under test instead of building 32 MiB of stats.

* chore(json): regenerate the API report for maxGroupBytes

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 5 commits October 30, 2026 08:32
…e close (#7081)

* chore(quest): claim quest/m1/cut-audit/js-close-cause.md

Quest-Claim: a1a64f23c30db61e

* quest: keep a locally closed session's received tracks ending cleanly

The received track's local-close outcome already mirrors Rust and is pinned by
tests, so the quest has nothing left to implement.

* fix(net)!: a local session close cancels received tracks like a remote close

A deliberate local close now ends received tracks, their open groups, and
pending lite fetches with the session-cancel error a peer's clean close
produces, in Rust and JS. moq-net's AGENTS.md (and a new js/net/AGENTS.md)
state the rule: local and remote paths behave the same.

* fix(net): a local close drains what arrived, as a remote close does

Rust lite and IETF and JS lite skipped the receive drain on a local close,
so a pending lite fetch ended Dropped and a track whose FIN was already
buffered aborted instead of finishing. Both closes now share one teardown.
The lite JS update loop no longer leaves a rejected stopped promise
unobserved when the session closes under it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(quest): claim quest/m1/cut-audit/jitter-admission.md

Quest-Claim: 24af4e62384d7f91

* fix(mux): checkpoint bounded jitter admission

* fix(mux): coordinate jitter admission and source receipt

* fix(ci): exclude scratch trees from TOML formatting

* docs(quest): record completed jitter interop checks

* fix(mux): charge frame footprint and bound the decode clock by bytes

Each jitter and decode clock entry now costs its payload plus its own
footprint, so empty or tiny frames still count against the budget. The
TS and MKV decode clock, which holds video ahead of the jitter buffer,
settles its front frame once it holds the budget: a burst sharing one PTS
read at once never becomes overdue by time.

* style(mux): format

* fix(mux): keep the reorder under byte pressure and charge key copies

The decode clock forces its front frame out at the byte budget only once
it holds more frames than any codec reorders, so one frame filling the
budget cannot settle the B-frames around it. An acquisition keeps a key
per frame, so the MKV and TS exports charge the track name's bytes too.

* fix(mux): count held B-frames in the delay under byte pressure

A decode clock forced out by its byte budget settles every frame it holds
for the reorder delay, not only its front, so a B-frame already read
still sets the delay and decodes no later than it is presented.

* fix(mux): copy out payload views that leave out most of their wire frame

A small view of a large LOC wire frame kept the whole allocation alive past
the consumer, so an export's jitter budget, charging the payload, undercounted
it without bound.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: GPT-6 <noreply@openai.com>
)

* chore(quest): claim quest/m1/cut-audit/peer-loop-budget.md

Quest-Claim: d9cee49d561b5e5c

* fix(net): yield peer-driven receive loops on a per-message budget

A peer streaming small messages at line rate (SUBSCRIBE_UPDATEs,
announces, datagrams, auth grants, empty frames, or PADDING) could keep a
session's driver busy until its source ran dry. Every peer-driven receive
loop now charges a kio::coop::Budget per message, and a SUBSCRIBE_UPDATE
that changes nothing wakes no group.

* style(net): format the update flood tests

* test(net): end the update flood on a unique final priority

The review noted the flood test's completion priority repeats every 100
updates, so a budget boundary could match it before the input runs out.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ption constructor (#7077)

* chore(quest): claim quest/m1/api-audit/binding-max-delay.md

Quest-Claim: eb04c2424a2e59e6

* fix(bindings)!: default a bare subscription to zero max delay

UniFFI cannot spell a duration default, so the generated MoqSubscription
leaves max_delay unset, which moq-net reads as no subscriber limit. Give
the Dart, Go, Swift, and Kotlin wrappers a zero default, as Python and
moq-net already have, and keep null as the explicit no-limit spelling
(moq.NoMaxDelay in Go).

* fix(bindings)!: null max delay means no limit, with a default subscription constructor

Per the maintainer's redirect, every binding keeps an optional max delay whose
absence sets no subscriber limit, and gains an explicit constructor for
moq-net's defaults instead of making zero the implicit default.

- moq-ffi: `MoqSubscription.max_delay` defaults to null, and the new
  `moq_subscription_defaults()` returns moq-net's `Subscription::default()`.
- Go keeps `MaxDelay *time.Duration` and adds `moq.DefaultSubscription()`.
- Swift `Subscription.default`, Kotlin `Subscription.default()`, Dart
  `defaultSubscription()`, Python `Subscription.default()` (whose bare
  `max_delay` is now None), C++ `moq::subscription_defaults()`, and C
  `moq_subscription_defaults()`.
- Deletes the finished quest.

* fix(moq-c)!: an unset max delay means no limit, with moq_subscription_defaults

Brings the hand-written C ABI to the same shape as the other bindings: a
moq_subscription with max_delay_present false sets no subscriber limit, and
moq_subscription_defaults() returns moq-net's defaults. Also drops stale
moq-ffi MIGRATE lines that still said max_delay was required.

* fix(js)!: an omitted maxDelay means no subscriber limit, with Track.Subscription.default()

js/net now matches the other bindings: a Track.Subscription without maxDelay
sets no subscriber limit (the publisher's maxAge alone), and
Track.Subscription.default() builds the default subscription with a zero
maxDelay. subscribe() with no options still uses that default. Live readers
that passed only a priority (the hang and watch catalogs, room metadata,
moq-boy status) spread the default to keep their behavior.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(quest): claim quest/m1/c/json-window.md

Quest-Claim: 41b4ac7e022a523c

* feat(moq-c): JSON window producer and consumer

moq-c publishes and reads moq-json windows, as moq-ffi does:
moq_publish_json_window with push, pop, cut and finish, and
moq_consume_json_window delivering push, pop and skip events. The
snapshot's refresh task is generalized so the window reuses it.

* fix(moq-c): cancel a JSON consumer before its subscription is accepted

`moq_consume_json_cancel` only took effect once SUBSCRIBE_OK arrived, so a
publisher that left the subscribe pending kept the snapshot, stream, and
window tasks alive with no terminal callback. Race the close against
subscription setup in all three.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@moq-bot
moq-bot Bot force-pushed the quest/m1/c/json-window branch from 09ede4f to 561d5c0 Compare October 30, 2026 08:33
@moq-bot
moq-bot Bot merged commit 561d5c0 into main Oct 30, 2026
8 checks passed
@moq-bot
moq-bot Bot deleted the quest/m1/c/json-window branch October 30, 2026 08:33
kixelated added a commit that referenced this pull request Oct 30, 2026
Port main's new JSON window API (#7082) to exact timestamps: a window
event points at a moq_timestamp, NULL when untimed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant