Repository navigation
refactor(net)!: moq-net's small surface fixes - #8266
Conversation
Quest-Claim: 97db455105bc7704
…refusals, datagram priority, routes, and attempts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it report time, and a narrower surface
- track::Info::compression takes a Compression enum (None, Deflate) matching the
wire values, mirrored in @moq/net ("none" | "deflate"), moq-ffi, moq-c, and every
binding.
- stats::Handle::{refuse, session} take #[non_exhaustive] Refusal and SessionInfo
structs, and stats::Registry::report takes the drain's now.
- auth::Grant is #[non_exhaustive].
- origin::Consumer::with_proxy is removed; set it on the origin::Producer.
- group::MAX_CACHE_BYTES is pub(crate); moq-json, moq-mux, and moq-stats keep
their own 32 MiB limits.
- The route refusal of a minted instance is backed out and left for the
maintainer in the quest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head 408f464.
[P2] Preserve an atomic stateless priority wait in the FFI adapter — rs/moq-ffi/src/datagram.rs:163–165. The priority() check and poll_priority_changed() compare against different remembered values. For example, a wait with last=B seeds the producer's prev=B; a change to A completes through the fast path without updating prev. A subsequent wait with last=A can read A, then a consumer changes back to B before poll_priority_changed runs. That poll sees B == prev and parks, although B differs from the caller's last=A. If priority stays B, the binding caller never receives the change. The previous poll_priority(waiter, last) checked and registered against the caller's value atomically. Retain that stateless operation for the shared FFI handle, or use a per-call observer initialized from last; add coverage for a change between the initial read and waiter registration.
Verification: GitHub-only static review of the API migrations, reconnect/auth setup, priority handling, compression/wire/binding conversions, stats/admission changes and cache-limit replacements. No builds or tests were run.
There was a problem hiding this comment.
Important
The shared-handle priority_changed(last) adapter in moq-ffi can park while the priority differs from last, and four quest plans still tell a future run to use the removed compress spelling.
Reviewed changes
- Client origin —
Client::with_originremoved in moq-net and moq-tokio; callers chain.with_publisher(&origin).with_subscriber(origin). - Versions —
Version::{code,from_code}becomepub(crate); newVersion::ALL(all 10, including opt-in-wip) andVersion::has_request_path()replace the relay's code comparison and the CLI/tokio/test scans. - Admission —
server::{Activity, Active, ActivityState}→{Admission, Admitted, AdmissionState}, with the matching fields and methods in moq-tokio, moq-relay, moq-uring and moq-webrtc. - Refusals —
RefusalKind::Lan/Refusals.lan→Membership/membership, through the stats JSON, Prometheus label, tests and docs. - Reconnect —
Attempt::client(&Client)builds each dial's client (target URL, credential viaauth::locate, auth handle), replacingAttempt::{url,token}and theversionsargument ofwith_tokens;Client::url()exposes the result. - Datagram priority —
poll_priority(waiter, last)→ statefulpoll_priority_changed(&mut self, waiter)/priority_changed(), with the stateless form kept crate-private for moq-net's own callers. - Compression —
track::Info.compress→Compression(None = 0,Deflate = 1), mirrored through lite TRACK_INFO, ietf properties, JS, FFI, C and every binding; internal booleans retained where the wire is boolean. - Stats —
Handle::{refuse,session}take new#[non_exhaustive]Refusal/SessionInfo;Registry::report(report, now)takes the drain time. - Cache/proxy/mesh —
group::MAX_CACHE_BYTESprivatized with per-crate 32 MiB bounds,origin::Consumer::with_proxyremoved in favor of the producer's, mesh proof helpers made private,auth::Grant#[non_exhaustive].
ℹ️ Four quest plans still reference the removed compress spelling
This PR rewrites quest/m2/api-shape/net.md and its epic README, but surviving plans still name APIs this PR removes, so whoever implements them next will write code against a method that no longer exists.
Technical details
# Rename the remaining plans off the removed compress spelling
## Affected sites
- `quest/m5/json-value-diff.md:24` — compression-only variant: "group DEFLATE through the track's `with_compress`".
- `quest/m5/net-wrap/e2ee.md:30` — "`track::Info.compress` is logical."
- `quest/m5/e2ee/README.md:38` — "the catalog owner sets logical `track::Info.compress`".
- `quest/m3/stats/README.md:96` — stats encoding on "one track with `compress: true`".
## Required outcome
- Each site names what landed: `track::Info::compression`, `Compression::Deflate`, or `with_compression(Compression::Deflate)`. No behavioral claim in those plans changes.
## Suggested approach
- Four one-line edits. `quest check` does not validate backticked API names in prose, so CI will not catch this.deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
# Conflicts: # rs/moq-net/tests/integration/announce_takeover.rs
…-checks after registering claim_continues (new on main) proxies like every other integration origin. group::Demand docs name paced flights (maintainer, 2026-11-08). The minted refusal item moves to quest/m2/instance-provenance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed 96e436f incrementally from 408f464, accounting for the merge of main. The prior P2 FFI priority-wait finding is resolved in the current code; no new actionable findings.
In rs/moq-ffi/src/datagram.rs, priority_changed(last) now polls/registers before its final current-priority check. A ready value equal to last is polled again to establish a waiter, and a pending poll is followed by the caller-relative comparison, closing the previously reported missed-wakeup window. Also checked the new claim_continues proxy adaptation and the remaining API/provenance quest changes. Typed minted instances remain planned, not implemented by this update.
Verification limits: static GitHub review only. No builds or tests run; the changed files add no dedicated regression test for the reported priority interleaving.
There was a problem hiding this comment.
ℹ️ Minor quest and PR-body drift; the new FFI wait and merge adaptation look correct.
Reviewed changes (incremental, 408f464 → 96e436f)
- FFI priority wait —
priority_changed(last)now pollspoll_priority_changedfirst, loops when the ready value equalslast, and onPendingre-readspriority()after registering. I traced the states: aReadyequal tolastcan happen at most once before the next poll registers againstlast; everyPendingreturn leaves a registration against the current value, and a change after the re-read wakes it. The prior missed-wake thread is resolved and the fix is sound. - Merge of
main— keptannounce_takeover.rsdeleted and adaptedclaim_continues.rs's origin helper to.with_proxy(true), matching every other integration origin. All 10claim_continuestests pass. - Quest split —
api-shape/net.mdis retitled "moq-net's route stream goes public" for the now-unblocked un-hide; the two## Waitingdecisions are recorded asDecided 2026-11-08; the minted-instance refusal is replaced by the newquest/m2/instance-provenance.md, added tolite07-finalize.md's Required.quest checkreports 591 documents ok.
ℹ️ References to update
quest/m2/instance-provenance.md:79— the Related entry links "moq-net's remaining surface fixes" to/quest/m2/api-shape/net.md, whose title this commit renamed to "moq-net's route stream goes public". Update the link text so it matches the target's title.quest/m2/instance-provenance.md:52-54— "Readers of the flag" listsmodel/broadcast.rs/model/track.rs(minted()state). Thoseminted()s read a track Producer's publication state (Publication::Staged/Published), notInstance::is_minted(), and neither file callsis_minted(). Drop them or note they are unrelated so the implementer follows the instance flag's actual readers.- Still outstanding from the previous review: four quest plans name the removed
compressspelling —quest/m5/json-value-diff.md:24(with_compress),quest/m5/net-wrap/e2ee.md:30andquest/m5/e2ee/README.md:38(track::Info.compress),quest/m3/stats/README.md:96(compress: true). Name what landed:Info::compression,Compression::Deflate, orwith_compression(Compression::Deflate). - PR body, "Choices I made without the maintainer": "moq-ffi's
priority_changed(last)stays stateless ... It checkspriority()first, then loops onpoll_priority_changed" describes the pre-fix version. The landed adapter calls the statefulpoll_priority_changedfirst and moves the shared handle's remembered value, as the Summary bullet already says. Update or drop the Choice.
Verification — cargo check -p moq-ffi --lib; cargo test -p moq-ffi --lib (318 passed); cargo test -p moq-net --features async --test integration claim_continues (10 passed); quest check --base origin/main --branch quest/m2/api-shape/net (591 documents ok). The full just check/just ci was not run here; the PR body claims both pass. No dedicated regression test covers the priority interleaving, as the prior review also noted.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
…n names Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ The drift fixes are correct and complete for the sites previously named; a few
compressreferences outside that set remain.
Reviewed changes
Incremental review of d350370 (one docs commit on top of 96e436f), which addresses every finding from the previous review. No code changed.
- Quest compression names —
quest/m5/json-value-diff.md,quest/m5/net-wrap/e2ee.md(both occurrences),quest/m5/e2ee/README.mdandquest/m3/stats/README.mdnow name the landed API:track::Info.compression,Compression::Deflateorwith_compression(Compression::Deflate). - Provenance quest —
quest/m2/instance-provenance.mdlinks the renamed quest as "moq-net's route stream goes public", andmodel/broadcast.rs/model/track.rsare gone from the flag's reader list, whoseminted()is unrelated toInstance::is_minted().
ℹ️ The compress sweep left sites outside the fixed set
The two unmodified MIGRATE files still tell users of the next release to set a field this PR removes, and a few plans that this PR does not touch still name it. The PR body's "Choices" bullet says the unreleased MIGRATE entries were fixed, so these two contradict it.
Technical details
# Finish renaming the track field in prose and migration notes
## Affected sites
- `rs/hang/MIGRATE.md:29-30` (under `## Unreleased`) — "one name with `compress: true` on its `moq_net::track::Info`" and "`Catalog::default_track_info()` sets `compress`". The helper now uses `with_compression(moq_net::track::Compression::Deflate)` (`rs/hang/src/catalog/root.rs:264`).
- `rs/moq-stats/MIGRATE.md:38` (under `## Unreleased`) — "each one track with `compress: true`".
- `quest/m5/net-wrap/e2ee.md:95` — "authenticated logical `compress` flag", in the same file whose line 30 this commit fixed to `track::Info.compression`.
- Untouched plans, lower priority: `quest/m4/track-info-docs.md:19` lists `MoqTrackInfo` fields ending in `compress` (the record now has `compression: Option<MoqCompression>`, `rs/moq-ffi/src/producer.rs:63`); `quest/m5/teleop-robot.md:101`, `quest/m5/teleop-mavlink.md:80`, `quest/m5/e2ee/rust-publish.md:30`, `quest/m5/net-wrap/js.md:21`, `quest/m3/release/migrate-audit.md:44`.
## Required outcome
- Each site names the landed field or setter. No behavioral claim in those documents changes.
## Suggested approach
- One-line edits per site. `quest check` validates links, not backticked API names, so CI will not catch these.deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
…and quests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Incremental review of 43ceded6, the only commit since the prior Pullfrog review. It is docs-only, finishing the compress rename in the two unreleased MIGRATE notes and the quest plans that still named the removed field.
- Migration notes —
rs/hang/MIGRATE.mdandrs/moq-stats/MIGRATE.mdnow saywith_compression(Compression::Deflate)/Compression::Deflateinstead ofcompress: true, matchingCatalog::default_track_info()and the stats track builders. - Quest plans —
quest/m3/release/migrate-audit.md,quest/m4/track-info-docs.md,quest/m5/e2ee/rust-publish.md,quest/m5/net-wrap/js.md,quest/m5/teleop-mavlink.md, andquest/m5/teleop-robot.mdnow namecompression(JScompression: "deflate"), closing the last stale API references. - Wire flag kept —
quest/m5/net-wrap/e2ee.md:95still sayscompress, correctly, because that isdraft-lcurley-moq-e2ee.md's authenticated wire flag, not the moq-net track field.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
# Conflicts: # js/hang/src/report.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Incremental review of the delta since the prior Pullfrog review at 43ceded6: the branch merged main and resolved the one conflict.
- Merged
main— the branch absorbedorigin/mainat9a073029b, newer than thef6a0e549base of the previous review; no other file's patch differs. - Resolved the conflict —
js/hang/src/report.tskeeps main'sWriter<T, I>schema handling (#schema,updateparsing,Json.Snapshot.Producer({ track })) and applies the PR'scompression: "deflate"rename in place of main'scompress: true. - Checked the rename — the merged tree has no stale
compressuses of the track field in JS, Rust, bindings, docs, or quests; only the internal lite/ietf booleans the PR intentionally keeps. - Verified the workspace —
bun run checkpasses forjs/hang,js/net,js/publish,js/watchandjs/json.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
# Conflicts: # js/net/api/index.api.md # js/net/src/lite/publisher.ts # js/net/src/lite/subscriber.ts # js/net/src/track.ts
# Conflicts: # rs/moq-cli/src/args.rs # rs/moq-net/tests/integration/support/mod.rs
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed 9596861 incrementally against the latest published review, accounting for merged main changes.
No new actionable findings in the incremental merge integrations. Checked the required Track.Info compression field, authored defaults and validation, datagram/test fixtures, and the lite publisher's conversion back to the wire boolean. The integration helpers retain Version::ALL and priority_changed() rather than restoring removed APIs. The previously fixed FFI priority-wait path is unchanged by this update.
Verification limits: GitHub-only static source/diff review. No builds or tests were run; author-reported validation was not independently reproduced.
#8266 renamed the JS reader's log line from compress=<bool>; the harness kept the old string, so the compressed-catalog lanes failed on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve against #8266's token plumbing: Tokens::preview/commit keep the probe's read-only preview on main's credential shape (no URL or versions), and Reconnect builds main's Attempt (token + auth handle) for both dials and probes, with moq-tokio's attempt helper using Attempt::client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line #8266 renamed the reader's output from compress=<bool> to compression=<none|deflate> but left the harness matching the old line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xying relay The session bench shared one current-thread runtime across a group's cells, so the sessions a dropped room left behind ran their teardown inside the next cell's setup. After moq-lite-07 at 4096 broadcasts that work took longer than the server's 10s handshake deadline, so moq-transport-22 at 16 broadcasts failed with Timeout. Each cell now gets its own runtime. The model relay also proxies again: #8266 dropped the harness's proxy flag, so moq-lite-07 viewers never learned the publisher's routes and hung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Lands the api-shape/net items that still applied on
main. The quest file is rewritten to what's left: the route-stream un-hide, now unblocked since moq-dev/moq#8063 merged. The minted-instance item moves to a new quest, quest/m2/instance-provenance.md.Summary
moq_net::Client::with_originandmoq_tokio::Client::with_originare gone. Chain.with_publisher(&origin).with_subscriber(origin).Version::{code, from_code}arepub(crate). The relay'sversion.code() >= 0xff0dad05check is nowVersion::has_request_path(). The code scans in moq-cli, moq-tokio and the tests now read the newVersion::ALL, which includes the opt-in-wipdrafts thatVersions::allleaves out.server::{Activity, Active, ActivityState}are renamed{Admission, Admitted, AdmissionState}, with the matching field and method names in moq-tokio, moq-relay, moq-uring and moq-webrtc.RefusalKind::Lan/Refusals.lanare renamedMembership/membership("A membership proof was missing or invalid.").moq_auth::mesh::{PROOF_CONTEXT, proof_message}are private andCHALLENGE_LENispub(crate). The signed message format is now written inProof's docs.datagram::Producer::poll_priority(waiter, last)is replaced bypoll_priority_changed(&mut self, waiter)(asyncpriority_changed()). It keeps its own last value, likepoll_wanted_changed.reconnect::Attempt::client(&self, &Client) -> Clientsets the URL, puts the token where the client's versions expect it, and attaches the token handle. It replacesAttempt::{url, token}, theversionsargument ofReconnect::with_tokens, and the separatewith_authcall.Client::url()was added so callers can read the URL back.track::Info'scompress: bool/with_compressbecomecompression: Compression/with_compression.Compressionis a#[non_exhaustive]enum,None = 0orDeflate = 1, matching the wire values. The mirrors:Track.Info.compression: "none" | "deflate", optional inInfoInitwhere it defaults to"none"MoqTrackInfo.compression: Option<MoqCompression>(DEFLATE)moq_track_info.compressiontakesMOQ_COMPRESSION_NONEorMOQ_COMPRESSION_DEFLATECompressionaliases; Python getsmoq.Compression; Dart is regenerated.stats::Handle::refusetakes a#[non_exhaustive]Refusal::new(label, reason)andHandle::sessiontakes aSessionInfo::new(label), so later records can add fields without breaking callers.stats::Registry::report(&mut report, now)takes the drain time explicitly.auth::Grantis#[non_exhaustive]. Build one fromGrant::default()orGrant::all()and set its fields.origin::Consumer::with_proxyis removed; setwith_proxyon theorigin::Producerinstead. The integration harness applied proxy to every test consumer, not just two sites. So the origin helpers in the moq-net and moq-net-tests integration tests now return proxying producers, and the one non-proxy test setswith_proxy(false).group::Demandandgroup::Producer::demandstay public; their docs now say a paced flight uses them to see its riders leave.MoqDatagramProducer::priority_changed(last)now pollspoll_priority_changedfirst and compares tolastonly after it registered, so a stale remembered value on the shared handle can no longer park the wait while the priority differs fromlast(pullfrog finding).group::MAX_CACHE_BYTESispub(crate). moq-json (MAX_GROUP_BYTES), moq-mux (jitter, container consumer and fMP4 bounds) and the moq-stats bench each keep their own 32 MiB limit. Public docs now say "32 MiB" in place of the private link.Choices I made without the maintainer:
priority_changed(last)keeps its caller-relativelastargument, because FFI handles are shared: it waits until the priority differs from the caller'slast, not from the shared handle's remembered value.Option<MoqCompression>: UniFFI proc-macros can't give an enum-typed record field a default (#[uniffi(default)]generates an invalidType()in Kotlin). moq-c uses au32discriminant checked withError::InvalidCode.Properties.compressand JS's liteTrackInfo.compress.RefusalandSessionInfo.Sessionis already the guard type, andAdmissionis the server's.compress,with_origin,ActivityorMAX_CACHE_BYTESwere fixed so they don't describe APIs that never shipped.Impact
Public API, all breaking (hence
!):Client::with_origin(o)(moq-net, moq-tokio).with_publisher(&o).with_subscriber(o)Version::code(),Version::from_code()Version::has_request_path(),Version::ALL, or parse the ALPN stringserver::{Activity, Active, ActivityState}, moq-tokioactivityserver::{Admission, Admitted, AdmissionState},admissionRefusalKind::Lan,Refusals.lanRefusalKind::Membership,Refusals.membershipdatagram::Producer::poll_priority(waiter, last)poll_priority_changed(waiter)/priority_changed()on a&mutproducerAttempt::url(),Attempt::token(),Reconnect::with_tokens(tokens, versions)attempt.client(&client),Reconnect::with_tokens(tokens)Info::compress/with_compress(bool); JScompress: true; FFIcompress: true; C.compress = trueInfo::compression/with_compression(Compression::Deflate); JScompression: "deflate"; FFIcompression: DEFLATE; C.compression = MOQ_COMPRESSION_DEFLATEHandle::refuse(label, reason),Handle::session(label)Handle::refuse(Refusal::new(label, reason)),Handle::session(SessionInfo::new(label))Registry::report(&mut report)Registry::report(&mut report, Instant::now())Grant { publish, subscribe, expires }literallet mut g = Grant::default(); g.publish = ..;origin::Consumer::with_proxy(true)origin::Producer::with_proxy(true)beforeconsume()group::MAX_CACHE_BYTESCompressionvalues are the TRACK_INFO values the drafts already define.lanbecomesmembership, and the Prometheus labelreason="lan"becomesreason="membership". No dashboard in the repo reads either; a deployed dashboard filtering onreason="lan"has to switch. Both fields are new since release, so there is no serde alias.Decisions
Maintainer, 2026-11-08:
group::Demandstays public, documented for paced flights (rejected: fold reader demand intoFill::poll_unwantedthen make it private; make it private and move the flight test and bench in-crate).routeand rewrite the tests; keep accepting it with docs; refuse in Rust only). The~prefix is an in-band sentinel in a free-form string: any caller can spoof it, and a real name starting with~is ambiguous. A minted instance becomes a flag onInstancethat only a session sets (FromStrandrandom()build named ones), and moq-lite-07 carries the flag beside the instance instead of the prefix, in Rust and JS (Instance.isMinted). Minting and its meaning stay. Planned in the new Minted instances are a typed flag quest (L), which joins Finalize moq-lite-07's Required since it changes lite-07 bytes.Suggested follow-ups
origin::Consumer::routes,RoutesandRouteEvent, and reshapeRouteEventinto{ kind, announce }(kept in the quest).stats::Registry::snapshotstill readsInstant::now()itself. Give it the same explicitnowif the stats records want deterministic snapshots.Overlaps
mainis merged in. feat(net)!: a job's exact route continues its claim's instance #8063 deletedannounce_takeover.rs(kept deleted) and addedclaim_continues.rs, whose origin helper now proxies like the rest.stats.rs,reconnect.rs,server.rs,track.rs,group.rsand moq-relay's auth. Whichever lands second will have to rebase through thestatsargument structs and theadmissionrename. It also changes lite-07 (SUBSCRIBE_DROP/SKIP ranges), as the new instance-provenance quest will; they touch different fields, so only textual conflicts are expected.no-credit-hookandno-sentinelsquests: this PR doesn't touchReceiveWindow,cache_duration, the reconnect timeouts, or the transport credit hook.Testing
just checkpasses on 9596861, after mergingmainup to 6ba771b.just checkunder full-host load timed outmesh_withdraw::{random,capped_random}_meshes_retract_onceat nextest's 60s kill. Alone they take about 30s here and 26s onmain, already past the 30s SLOW mark either way, and they passed in the later runs. It also failed@moq/net'sa re-offer dropped again before it opens stays stale at the endonce, whichmainhas since quarantined in moq-dev/moq#8333.quest check --base origin/mainpasses.Models
Size: M
Takeover, 2026-11-08: the
quest:activehold taken at 15:46Z had gone quiet since about 18:22Z, and the maintainer approved another session taking the PR over. That session kept the same hold token, mergedmainthree times as it moved, and reran the checks. The conflicts:js/hang/src/report.ts: main's schema-inputWritercreates its track withcompression: "deflate".js/nettrack.Info(moq-dev/moq#8296 made its fields required, withnullfor absent):compression: Compressionis required too,authoredInfodefaults it to"none"and refuses unknown values, main's droppedinfoDefaultsstays dropped, and main'sdatagramTrackInfoand new test fixtures saycompression: "none".rs/moq-cli/src/args.rsand the moq-net integrationsupport/mod.rs: keptVersion::ALLandpriority_changed()under main's directmoq_net/kiopaths (moq-dev/moq#8247).Review
Rewritten by
quest reviewafter each round.01a1be74-91f5-74d3-a8e7-9ba7fbbbb8da, round 2No actionable regressions were found in the incremental changes or merge resolutions; builds and tests were not run, as instructed.