Repository navigation
feat(moq-c): JSON window producer and consumer - #7082
Conversation
|
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 50 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. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
| let event = tokio::select! { | ||
| biased; | ||
| _ = &mut close => return Ok(()), | ||
| next = consumer.next() => match next? { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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)
kixelated
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
|
Merge summary for head
Landing through the merge queue. (Written by Claude Opus 5.5) |
* 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>
…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>
09ede4f to
561d5c0
Compare
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>
moq-c now publishes and reads moq-json windows, as moq-ffi's
MoqJsonWindowProducer/MoqJsonWindowConsumerdo. This closes the last moq-c parity gap that moq-dev/moq#6946 found. Completesquest/m1/c/json-window.md.Changes
Publish:
moq_publish_json_window(broadcast, name, name_len, config)creates the track. Edit it withmoq_publish_json_window_push(a JSON document),_pop(count),_cutand_finish.UNROUTABLEand 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_eventfillsmoq_json_window_event:kind:MOQ_JSON_WINDOW_KIND_PUSH,_POPor_SKIP.[start, end): the absolute index range. A push covers the single record atstart.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_cancelnow stops window consumers too.Cancel during subscribe:
moq_consume_json_cancelnow 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, souser_datacould not be released. A sharedConsume::json_subscriberaces the close againstsubscribe(None).moq_json_window_configis 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: trueon its track info), and the consumer takes no config.Refactor: the snapshot's refresh holder and task (
JsonSnapshot,json_snapshot_refresh) become a genericRefreshed<T>plusPublish::refreshed, so the snapshot and the window share one task. Behavior is unchanged.Docs:
doc/lib/cgains a JSON windows bullet.doc/lib/layers.mddrops "the C API has no window yet". The shared feature list indoc/lib/index.mdnow names windows.Impact
moq_json_window_config,moq_json_window_event,moq_json_window_kind,moq_publish_json_window{,_push,_pop,_cut,_finish}andmoq_consume_json_window{,_event,_event_free}.moq_consume_json_cancelalso accepts a window task, and now also works on a consumer still subscribing. Nothing to migrate.Choices made without asking
MoqTrackProducer/MoqTrackConsumer. moq-c's JSON snapshot and stream takebroadcast, 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 andmoq-mux's HLS window.checkpoint_records = 0means the whole window. moq-ffi takesOption<u64>and refusesSome(0). Here a zeroed config is the default, as everywhere in moq-c, and 0 has no other meaning.[start, start + 1). moq-ffi hasPush { index }. A singlestart/endpair keeps the C struct flat, with no union.moq_publish_close. This matches raw tracks and media and the "Ending cleanly" promise indoc/lib/c. The test checks that a reader joining after the finish still gets the window. Mutatingkeepto a drop fails it. The JSON snapshot and stream drop their producer on finish instead (see Follow-ups).demandon the window producer. moq-ffi's JSON producers havedemand(), 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
const moq_timestamp *, which arrives with moq-dev/moq#6946. This PR followsmain's moq-c JSON convention: push and pop stamp the broadcast clock's current time, and events readtimestamp_usplustimestamp_present. Whichever of this PR and feat(bindings)!: exact {value, scale} timestamps in every binding #6946 merges second converts the window the way feat(bindings)!: exact {value, scale} timestamps in every binding #6946 converts snapshot and stream: a trailingconst moq_timestamp *on push and pop (NULL stamps the clock), andconst moq_timestamp *timestamponmoq_json_window_event. feat(bindings)!: exact {value, scale} timestamps in every binding #6946 had not merged when this PR was marked ready, so the conversion falls to feat(bindings)!: exact {value, scale} timestamps in every binding #6946 if this one lands first.Verification
cargo test -p moq-c --lib json_passes:json_cancel_while_subscribing: for each of the snapshot, stream, and window consumers,moq_publish_requestsleaves the subscribe pending, then a cancel delivers terminal0. Without the fix it times out (checked by makingjson_subscribeignoreclose).json_window_publish_consume: push, pop, clock stamps, NULLjsonon 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 getsSKIP [0, 38), then the two pushes, then a clean end.mainat d33b591, branch-widejust checkpasses, including clippy and the moq-c tests.just rs c-testspasses. The generatedmoq.hexports every new symbol.just test interop --allwas not run: this PR has no wire,moq-ffi, or gateway change.Review
gpt-6.1-sol, medium effort, round 1 on head 09ede4f againstorigin/maind33b591: 0 findings.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_subscribecould become a shared helper for all of them.moq-c consumer tasks on kio. Every moq-c consumer loop drives a
tokio::select!over aoneshotclose.rs/AGENTS.mdasks forpoll_*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}_finishandmoq_publish_binary_{snapshot,stream}_finishdrop 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 shouldChildren::keepits 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 likemoq_publish_track_demand. This is a parity gap for the m1 cut, covered by the moq-c parity epic's exports-vs-moq.hdiff.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code