Skip to content

feat(net)!: js/net speaks lite-07's route layer - #8443

Open
kixelated wants to merge 30 commits into
mainfrom
quest/m2/cluster-routing/routes/js
Open

kixelated wants to merge 30 commits into
mainfrom
quest/m2/cluster-routing/routes/js

Conversation

@kixelated

@kixelated kixelated commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

js/net now speaks lite-07's route layer as Rust does. It reads and writes ROUTE messages and ANNOUNCE_FLOOR, applies each route's claims a batch at a time, echoes every ROUTE_END, and pins each request to the origin node it selected. A browser stays non-transit: it advertises its own origin's claims under one route of its own and forwards nothing.

It also adopts moq-dev/moq#8458's route Flags byte and RTT and moq-dev/moq#8487's route Hops, so JS matches Rust and the draft. Both have landed, and this branch has merged them.

Quest: deletes quest/m2/cluster-routing/routes/js.md and every link to it. route-stream.md, which main pointed at it, now says the JS layer exists and changes with it. route-latency.md drops its codec bullet and its Required, leaving the JS RouteInfo.rtt read.

Decisions

  • Design checkpoint after review round 3, which found announcement and consume selection disagreeing after a re-price created a tie, the same class as rounds 1 and 2: ✅ break ties as rs/moq-net does, in every JS selector (maintainer, 2026-11-10; supersedes an earlier pick of B, rethink; rejected: A, reorder byPath only for a new claim or restart; C, merge as is).

    • bestCandidate in js/net/src/internal.ts ports best_route. Only an instance's highest epoch is eligible. Routes rank by route_order, through compareRouteCandidates: the rendezvous hash of each lite-07 claim's origin node, or the instance and chain hashes. The newest claim wins a full tie, as Rust's final Reverse(entry.id).
    • Rust skips the hash for a route that entered on a client session (Link::Local) at its own path, which is every route a browser holds. There the newest claim decides. A claim counts as new when it starts or restarts, and a re-price keeps its place, as a Rust entry keeps its id through update.
    • The announcements, BroadcastCache.instance, and BroadcastCache.node all rank through it.
    • Tests: lite07_ties_match_js in rs/moq-net pins the node hash and both tie rules. "a lite-07 tie at a claim's path goes to the newest claim, a re-price keeping its place" checks that the announcement and the consume agree, and fails on a817f15db1. "lite-07 children of a prefix two origins claim spread by node" asserts Rust's pinned hashes and pick.
  • Route RTT in JS: ✅ adopt feat(net)!: lite-07 routes carry an informational RTT #8458's Flags byte and RTT before this lands, and leave RouteInfo.rtt to quest/m2/route-latency.md (maintainer, 2026-11-10, on moq-dev/moq#8458; rejected: land feat(net)!: lite-07 routes carry an informational RTT #8458 after this).

Changes

Codec (lite/announce.ts, lite/instance.ts, the request messages)

  • New messages: ROUTE_START, ROUTE_UPDATE, ROUTE_HOLD, ROUTE_END, and ANNOUNCE_FLOOR. On the reply side, ROUTE_REQUEST and the ROUTE_END echo.
  • Refused on both sides: the last seqno, Node ID 0, and any other reply type.
  • ROUTE_START and ROUTE_UPDATE carry Flags (0x1 down-only, 0x2 an RTT in milliseconds follows) as moq-dev/moq#8458 defines. Any other bit, or the 0x2 bit without its RTT, is a violation. A browser advertises its own route at 0. The subscriber decodes the RTT and drops it: nothing ranks by it, and a ROUTE_UPDATE that changes only the RTT reports nothing.
  • ROUTE_START and ROUTE_UPDATE carry Hops (moq-dev/moq#8487): a count, at most 32, and that many Hop IDs. More than 32 is a violation, and the encoder refuses to write them.
  • An ANNOUNCE_START names its Route ID in place of the hop chain, ANNOUNCE_RESTART carries no hops, and ANNOUNCE_OK is empty.
  • Every TRACK, TRACKS, SUBSCRIBE, FETCH, and DATAGRAMS request carries a Node ID right after its instance pin; 0 lets the publisher choose.
  • version.ts renames hasNodeId to hasRoutes. On lite-07, SETUP's 0x5 parameter carries the publishing origin's Node ID.

Publisher (lite/publisher.ts, new route.ts)

  • Each origin has one route identity (Own): a random 63-bit Node ID shared by its sessions, a seqno, and a revision.
    • The seqno advances when a stream is answered or a ROUTE_REQUEST asks for a newer one.
    • The revision advances when the origin's claims change. A value that would reach 2^64-1 rotates the Node ID instead.
  • The answer follows publisher/routed.rs:
    • ROUTE_START goes out before the first claim.
    • Each batch is closed by a floor. The floor certifies the revision only while neither the grant nor the handle's scope leaves anything out of the requested prefix.
    • After the last claim is withdrawn: its batch, then the floor, then ROUTE_END. The route restarts only after the echo.
    • SYNCED is sent once every route has a floor.
    • An echo for a route that is not ending is a protocol violation.
    • A graceful close ends the route.
  • A grant change that flips whether a floor may certify the requested prefix re-floors the route, even when the claims it shows are unchanged, as Rust's refilter does (pullfrog's review finding).
  • A request that names another origin node is refused Unroutable.
  • The origin's own route carries its Hop ID alone: Own.hop, the Node ID it started with, which a rotation keeps. A tab forwards nothing, so it adds itself to no other route. On lite-07 a publishing session's hop is that Hop ID.

Subscriber (lite/subscriber.ts)

  • #receiveRouted tracks each route and its claims.
    • Each route and each claim holds one announce slot.
    • A second route to one node, or a second claim at one path under one route, is a violation. Both are map lookups.
    • Each route's pending changes are applied whole at its floor.
  • For each path it reports the best claim using compareRouteCandidates, with the newest winning a tie.
    • Cost is the claim's cost plus the route's metric plus a link cost of 1, matching Rust's default.
    • available is false while the route is held.
    • A route that carries 32 Hops, or names this session's Hop ID (a copy of the tab's own route, come back), is no route: its claims go offline and leave the consume cache until a ROUTE_UPDATE makes it usable again, as Rust's router never selects one.
  • Events:
    • ROUTE_HOLD and ROUTE_UPDATE report update, flipping RouteInfo.available.
    • ROUTE_END takes the route's claims offline (not end), unless another route still serves the path. The subscriber then echoes it.
    • GOAWAY drains every reported path.
  • A consume records the selected route's node, and its SUBSCRIBE, TRACK, TRACKS, FETCH, and DATAGRAMS requests name it.

Origin (origin.ts): applies the 2026-10-27 maintainer decision on moq-dev/moq#6947. A detached or pinned request now follows its instance's cheapest route, as Rust's does, instead of staying on the route it first resolved while that route still serves. pick and failover are merged into one follow.

Interop

  • rs/moq-net/fixtures/lite.json gains 18 lite-07 route cases, three of them for the RTT flag and its refusals and two for refused Hops (more than 32, and one missing), replayed by both corpus.rs and corpus.test.ts, with a new announce_reply message kind.
  • announce.test.ts decodes Rust's lite::compress GOLDEN, and its encoder writes JS_LITERAL byte for byte. Rust's compress.rs comments now point at that test.
  • The JS lite-07 lanes are restored:
    • JS_LITE07_SKIP, js_lite07, and the needs_js version guard are removed from interop.sh and socket.sh. Native JS prints its grant again.
    • The lite-07 catalog reader is back.
    • tracks_interop is back in just test interop.
    • The lite-07 legs of max_age_relay_javascript and relay_fill_javascript are back.
    • datagram_tracks_javascript has its original ignore reason, and its test-inventory entries are gone.
    • The Datagram tracks interop workflow step and test drain in the nightly matrix are back.
    • The replacement media quarantine is removed. The Firefox audio-tone quarantine stays.
    • Review fixes: the publisher applies announce replies as they arrive, with no queue. The subscriber files every route's claim in the consume cache under its own owner, so each consume ranks every node's claim for its own path, epochs included. The cache gives a tie at the prefix itself to the newest stream. A consume names the node of its own instance's best route.
    • Ties break as in Rust, through bestCandidate; see Decisions. The announce stream moves a claim to the newest only when it starts or restarts, as the consume cache does.
    • compareRouteCandidates keys each candidate on its own, as route_order does: a lite-07 claim by the rendezvous hash of its node, any other by its instance and chain hashes. The origin's announcement order counts a lite-07 claim as one hop (routeHops), as requests already did.
    • The origin bridge carries every claim. routes() takes an internal claims option (RouteOptions). With it, a lite-07 stream reports each origin node's claim at a path as its own route, as Rust's routed.rs forwards each claim into its origin. forwardAnnounced asks for it and keys its routes by path and node. The origin then spreads a prefix's children by node and resolves a request pinned to a claim that does not win. The public announced() still reports one route per path.
    • A request through a received route consumes the session pinned to that route's instance (Established.consume(path, instance), BroadcastCache.pinned). A handover to a report of another instance restarts the route. Either way, one instance's content is never served under another's identity.
    • The publisher starts no route until every ended one is echoed, so a peer that keeps rotating our identity with a ROUTE_REQUEST for the last seqno, and withholds its echoes, holds one ended route rather than one per rotation. A closed publishing origin ends its claims and route, and its answer finishes, as the pre-lite-07 path does. Both were findings from a review of the earlier, unpushed B head.
    • The datagram_tracks relay is now a proxy, as max_age's became in moq-dev/moq#8023.

Choices made without asking

  • Floor merge is per stream. Each stream applies its own batches at its own floors. The BroadcastCache and the origin then rank streams and sessions against each other, as before.
    • The contract's cross-stream rule is not implemented: the highest certifying revision decides whether a claim exists.
    • Today a browser hears an origin through one upstream. Overlapping requests on one session come from the same relay and agree within one write.
    • A lagging stream can therefore keep a withdrawn claim only briefly, and a request through it fails Unroutable and is retried.
    • See Follow-ups.
  • A subscriber never sends ROUTE_REQUEST. It holds no feasibility distance, since it forwards nothing.
  • A peer declaring our own Node ID in SETUP is not refused. The draft's SHOULD protects a node that forwards. A browser cannot loop.
  • hops on a lite-07 route is empty, with node set. compareRouteCandidates counts such a route as one hop and spreads ties by the rendezvous hash of its node, as route_order does.
  • A graceful close's lite-07 claims go offline, not end. The publisher sends ROUTE_END, as Rust's does, and ROUTE_END withdraws nothing. close.test.ts now expects offline for both protocols.
  • The pre-lite-07 legacy publisher path no longer carries lite-07-only branches (restart in place, the instance on active), since lite-07 always takes the routed path.

Caused by

  • moq-dev/moq#5087 - kept a detached request on the route it first resolved while that route served, rather than following its instance's cheapest route.

Impact

  • Wire: moq-lite-07-wip only, which is unpublished. js/net moves from the pre-route lite-07 wire to the route layer Rust already speaks, with moq-dev/moq#8458's Flags and RTT and moq-dev/moq#8487's Hops. Published versions are unchanged.
  • Public API: none. @moq/net's exported surface (api/index.api.md) is unchanged. The lite codec types and Own are internal.
  • Rust: test-only. Adds corpus cases and the announce_reply corpus kind, restores interop legs, and updates comments.

Checks

Run locally on d448e32e79 unless noted:

  • just check passes: 12936 Rust tests and the js/net suite. The pre-existing check c failure did not show up.
    • On aa4a00df0b and a817f15db1, after review rounds 1 and 2: just check passes again, with 3230 js/net tests.
  • just fix leaves nothing to change.
  • The Rust/JS interop suites from just test interop pass, including the restored lite_tracks_interop.
  • interop.sh --publishers rust,js --subscribers rust,js,js-native-node,js-native-bun passes all 79 cases. That covers native JS on lite-07: claims, track replacement, sockets, compress, the compressed catalog with its lite-07 reader, and the drop and tail legs.
    • Skipped: the Chromium route-key lane (Chromium 156 cannot prove), and finish (already quarantined).
  • just test max-age, relay-fill, and datagram-tracks pass, each now including its JS lite-07 leg.
    • datagram-tracks needed one fix: its relay is now a proxy, as max_age's already was. A lite-07 relay forwards a peer's claims only as a proxy.
  • Not run: the Python, Go, C++, C, and GStreamer clients of interop --all. They speak through moq-ffi, which this PR does not touch.
  • Not run: just test drain and the media replacement case. CI's nightly and media jobs cover them.
  • On 5c8f391f24, after the tie decision, the review fixes, and merging main: just check passes (12938 Rust tests, 3240 js/net tests), and so does quest check --base origin/main. Each new test fails without its fix.
  • On 8a7484fb0a, after merging main and feat(net)!: lite-07 routes carry an informational RTT #8458's head and adopting its Flags and RTT: just check passes, and just test interop --all passes (214 cases). Its only skips are the quarantined python -> go and the Chromium 156 route-key lane. The re-floor test fails without its fix.
  • 764e98c0cd merges main after feat(net)!: lite-07 routes carry an informational RTT #8458 landed as a squash. Its tree is identical to 8a7484fb0a.
  • On 323c67fb61, after adopting fix(net)!: a lite-07 route names the nodes it crossed, and is never feasible where it has been #8487's Hops and merging main (twice, as it moved mid-merge): just check passes (12872 Rust tests and the js/net suite), quest check --base origin/main passes, and just test interop --all passes (204 cases; skips: the quarantined python -> go and the Chromium 156 route-key lane).
  • CI: see the checks on the head commit.

Follow-ups

  • Cross-stream floor merge in js/net. Recommendation: plan it alongside P2P or multi-upstream browsers, when one origin's claims first reach a tab over several sessions. Until then it adds per-node withdrawal state for no observable gain.

Waiting

  • Maintainer: the local review (Codex, rounds 1 and 2 on 8a7484fb0a) keeps one medium finding against a refutation. js/net applies each announce stream's floors to that stream alone ("Floor merge is per stream" above). The draft (drafts/draft-lcurley-moq-lite.md:457-458) says a receiver hearing one origin on several streams lets the highest certifying Revision decide whether a claim exists. So when two overlapping routes() streams from one relay deliver a withdrawal at different times, the lagging stream's claim stays in the consume cache until its own batch lands. A fresh subscribe in that window can pick the withdrawn node, and the relay refuses it Unroutable (an error, not a re-resolve). Options:
    • ✅ Recommended: port the merge now, in a further /quest-iterate run sized L. It ports rs/moq-net/src/route/view.rs's merge_claims and retire into a per-session view per origin node, which the consume cache and forwardAnnounced read. Reason: the draft and the deleted quest's Goal ("merges announcements by floor as the contract says") both require it, and apps routinely open overlapping routes() streams.
    • Merge as is and plan the follow-up below. The window lasts only until the lagging stream's batch, from one relay on one connection, and the Rust-to-TypeScript port later replaces this code.
    • Check: a maintainer comment on this PR picking one.

Models

Size: L
Implementer: Claude Code, claude-opus-5-5, high effort
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, M)
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, M)
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, M)

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1cc12-c08b-7133-9106-ab871a8a29fe, round 1
  • Head: 323c67f, named in full once a round reports no findings
  • Base: origin/main at c2a4749
  • Score: 7/10, The head is ready to land after certified withdrawals stop leaving stale claims selectable across overlapping announce streams.
  • Findings, open until the next round settles them:
    • Medium, js/net/src/lite/subscriber.ts:1056: Merge certified floors across overlapping announce streams
  • The last round's findings, at 8a7484f:
    • Open: medium, js/net/src/lite/subscriber.ts:1049: Merge certified floors across overlapping announce streams; The head still applies certified floors per stream without reconciling revisions, leaving a lagging stream's withdrawn claim selectable.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5 with medium effort)

kixelated and others added 3 commits November 10, 2026 01:28
js/net now reads and writes lite-07-wip's route layer as Rust does:

- The codec carries ROUTE_START/UPDATE/HOLD/END, ANNOUNCE_FLOOR, and the
  subscriber's ROUTE_REQUEST and ROUTE_END echo. Claims name a Route ID in
  place of a hop chain, and every TRACK, TRACKS, SUBSCRIBE, FETCH, and
  DATAGRAMS request names an origin Node ID (0 lets the publisher choose).
- A publisher answers from its origin's own route: one Node ID per origin,
  claims in batches closed by a floor, ROUTE_END after the last withdrawal,
  and a restart only after the echo. A browser forwards nothing.
- A subscriber applies each route's batch at its floor, reports the best
  claim per path with the route's metric, availability, and node, echoes
  every ROUTE_END, and pins each consume's requests to the node it selected.
- A detached request follows its instance's cheapest route, as Rust does.
- Rust and JS share route-message cases in fixtures/lite.json, the
  announce goldens match lite::compress, and the JS lite-07 interop lanes,
  datagram tracks, and nightly drain run again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lite-07 relay forwards a peer's claims to its other peers only as a
proxy, as max_age's relay already does, so the restored JS datagram-tracks
leg through the relay found nothing to subscribe to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title js/net route layer feat(net)!: js/net speaks lite-07's route layer Nov 10, 2026
kixelated and others added 2 commits November 10, 2026 05:44
… inline

- The publisher applies each announce reply as it decodes it rather than
  queueing it, so a peer cannot grow a queue while our writes stall.
- The subscriber files every route's claim in the consume cache under its
  own owner, so a consume ranks each node's claim for its own path, epochs
  included, and spreads a prefix's children by rendezvous hash.
- A consume names the node now serving its instance, following failover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ws its own routes

- BroadcastCache ranks the newest stream first on a tie at its own prefix,
  as refresh(), the origin, and Rust do, so a restart's consumers open the
  instance it reported.
- A consume names the node of its own instance's best route even once
  another instance wins the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Design change: the maintainer picked B (rethink) at the round-3 checkpoint. The announcement and the consume cache no longer rank separately. bestRoute in js/net/src/internal.ts is the one ranking (highest epoch per instance, then compareRouteCandidates, newest wins a tie) over a list ordered oldest first. BroadcastCache keeps that one ordered list per prefix, and instance(), node(), and the new claim(path, owners) all rank it. The subscriber's refresh() calls claim() with its stream's route owners, so byPath and its separate reorder rule are gone. The next review starts a new session with quest review --new.

(Written by Claude Opus 5.5 with medium effort)

kixelated and others added 3 commits November 10, 2026 09:34
- bestCandidate ports best_route's pick: epochs, route_order (the node,
  instance, and chain hashes), then the newest claim. The announcements,
  BroadcastCache.instance and BroadcastCache.node all rank through it.
- A claim is the newest when it starts or restarts, and keeps its place
  through a re-price, as a Rust entry keeps its id. The announce stream
  no longer reorders on every floor, so it and the consumes agree.
- lite07_ties_match_js pins the node hash and both tie rules in Rust,
  and the JS tests assert the same picks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quest-Claim: b14401aa6e4877d6
…ts origin

- No route starts until every ended one is echoed, so a peer that keeps
  rotating our identity (a ROUTE_REQUEST for the last seqno) and withholds
  its echoes holds one ended route, not one per rotation.
- A closed publishing origin ends its claims and route, then the answer
  finishes, as the pre-lite-07 path does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Superseding the B note above: the maintainer then chose to break ties exactly as rs/moq-net does. bestCandidate ports best_route, and the announcements and consume cache both rank through it. It is pushed as 5339f99961. eb45e04578 fixes the two publisher findings from the review of 0b69afdc53: ended routes are bounded across identity rotations, and the answer finishes when the origin closes.

0b69afdc53 was never pushed and now diverges from this branch. Please do not force-push it. Rebuild on eb45e04578 if anything from it is still wanted.

(Written by Claude Opus 5.5 with high effort)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Design change: the maintainer replaced the B pick with one rule. js/net breaks route ties exactly as rs/moq-net does, in every selector. bestCandidate in js/net/src/internal.ts ports best_route: the highest epoch per instance, then route_order through compareRouteCandidates (the node, instance, and chain hashes), and the newest claim on a full tie. A claim counts as new when it starts or restarts, and keeps its place through a re-price, as a Rust entry keeps its id. The announcements and BroadcastCache both rank through it. lite07_ties_match_js pins the tie in Rust, and the JS tests assert the same picks. The next review starts a new session with quest review --new.

(Written by Claude Opus 5.5 with high effort)

kixelated and others added 2 commits November 10, 2026 09:58
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d routes as Rust

- A request through a route the origin received consumes the session
  pinned to that route's instance (Established.consume takes an optional
  instance), so another instance's claim at the prefix, ranked apart, is
  never served under this one's identity.
- compareRouteCandidates keys each candidate on its own, a lite-07 claim
  by its node and any other by its instance and chain, as route_order does,
  instead of keying a lite-06 route at zero beside a lite-07 one.
- The origin's announcement order counts a lite-07 claim as one hop, as
  requests already did (routeHops).
- lite07_ties_match_js pins the mixed pick in Rust.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits November 10, 2026 10:31
…re-pins

- routes() takes an internal claims option: a lite-07 stream then reports
  each origin node's claim at a path as its own route rather than only the
  best one, as Rust's routed.rs forwards each claim. The bridge asks for it
  and keys its routes by path and node, so the origin ranks every candidate,
  spreads a prefix's children by node, and resolves a request pinned to a
  claim that does not win. announced() keeps one route per path.
- A route handed to a report of another instance restarts, and drive reads
  the handle's instance per request, so a handover never serves one
  instance under another's identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing/routes/js

# Conflicts:
#	js/net/src/origin.ts
@kixelated
kixelated marked this pull request as ready for review November 10, 2026 19:06

@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 5c8f391. No actionable defects found in the reviewed route-layer implementation and integration changes.

Direction: the wire codecs align with the Rust route messages, while publisher end/echo handling bounds retained routes. Subscriber batches commit at floors, the consume cache retains every node's claim, and the origin bridge now pins requests to the received instance. Shared ranking and the added tie/reprice, fallback, and pinned-instance regressions address consistency between announcement and consumption. Restored interop lanes exercise the newly supported lite-07 paths.

Scope caveat: cross-stream certifying-floor reconciliation remains explicitly unimplemented; this review does not establish multi-upstream consistency.

Verification limits: GitHub-only static review of the changed source, fixtures, tests, and workflow wiring, with Rust codec comparison. No builds, tests, browser/media runs, or performance measurements executed. Author-reported checks were not independently reproduced; the head's Check workflow was still in progress when inspected.

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The JavaScript network stack now supports Lite 07 routed announcements, including route starts, updates, holds, ends, floors, and replies. Requests carry origin-node information, and route selection supports node-aware ranking and instance pins. Publisher and subscriber flows handle claims, route changes, and failover. The changes also update Rust fixtures and tests, enable Lite 07 interop cases, and revise related workflow and quest documentation.

Merge Risk: Minimal · up to 5c8f3

This change adds lite-07 route-layer support to js/net and enables the matching interop lanes. No concrete merge-blocking defect was identified in the supplied evidence. Some interop clients were not run by the author, so ordinary CI results are worth confirming before merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly identifies the main change: adding lite-07 route-layer support to js/net.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly explains the lite-07 route-layer implementation, publisher and subscriber changes, interoperability updates, tests, scope, and known follow-up work. It is clearly…
Full details: Description check

Explanation

The pull request description directly explains the lite-07 route-layer implementation, publisher and subscriber changes, interoperability updates, tests, scope, and known follow-up work. It is clearly related to the changeset.

Finishing Touches

Simplify code

  • Commit to this branch
  • Create a new PR
  • 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

Recent review info

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: 5c9d6477-2044-4a76-8963-b6e7e47ba553
Commits

Reviewing files that changed from the base of the PR and between 27fb3eb and 5c8f391.

Files selected for processing: 52

  • .github/workflows/interop.yml
  • .github/workflows/nightly.yml
  • js/net/src/announced.ts
  • js/net/src/connection/close.test.ts
  • js/net/src/connection/forward.test.ts
  • js/net/src/connection/forward.ts
  • js/net/src/connection/session.ts
  • js/net/src/consume.test.ts
  • js/net/src/consume.ts
  • js/net/src/hop.ts
  • js/net/src/integers.test.ts
  • js/net/src/internal.ts
  • js/net/src/lite/announce.test.ts
  • js/net/src/lite/announce.ts
  • js/net/src/lite/connection.ts
  • js/net/src/lite/corpus.test.ts
  • js/net/src/lite/datagram.test.ts
  • js/net/src/lite/datagram.ts
  • js/net/src/lite/fetch.test.ts
  • js/net/src/lite/fetch.ts
  • js/net/src/lite/instance.ts
  • js/net/src/lite/publisher.test.ts
  • js/net/src/lite/publisher.ts
  • js/net/src/lite/setup.ts
  • js/net/src/lite/subscribe.ts
  • js/net/src/lite/subscriber.test.ts
  • js/net/src/lite/subscriber.ts
  • js/net/src/lite/track.ts
  • js/net/src/lite/tracks.ts
  • js/net/src/lite/version.ts
  • js/net/src/origin-request.test.ts
  • js/net/src/origin.test.ts
  • js/net/src/origin.ts
  • js/net/src/route.ts
  • js/net/src/wire.ts
  • quest/m2/cluster-routing/routes/README.md
  • quest/m2/cluster-routing/routes/acceptance.md
  • quest/m2/cluster-routing/routes/js.md
  • rs/moq-net/fixtures/lite.json
  • rs/moq-net/src/lite/compress.rs
  • rs/moq-net/src/lite/corpus.rs
  • rs/moq-net/src/model/origin.rs
  • rs/moq-net/src/test_interop.rs
  • rs/moq-tokio/tests/integration/datagram_tracks.rs
  • rs/moq-tokio/tests/integration/max_age.rs
  • rs/moq-tokio/tests/integration/relay_fill.rs
  • sh/gh/test-inventory.allow
  • test/interop/README.md
  • test/interop/clients/js/media.ts
  • test/interop/interop.sh
  • test/interop/socket.sh
  • test/justfile
Files with no reviewable changes: 6

  • quest/m2/cluster-routing/routes/acceptance.md
  • quest/m2/cluster-routing/routes/README.md
  • sh/gh/test-inventory.allow
  • quest/m2/cluster-routing/routes/js.md
  • test/interop/socket.sh
  • test/interop/clients/js/media.ts

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The new lite-07 publisher only re-floors on a claim change, so a grant change that flips whether the prefix may be certified never reaches the peer. Rust re-floors every route on every permit change, and a Rust peer acts on the difference (it drops claims no floor covers when it adopts a view).

Reviewed changes

  • Route codec — ROUTE_START/UPDATE/HOLD/END, ANNOUNCE_FLOOR, and the subscriber's ROUTE_REQUEST/ROUTE_END echo, with lite-07 claims naming a Route ID in place of a hop chain and every request naming a Node ID.
  • Routed publisher — #runRouted advertises the origin's claims under one route, closes each batch with a floor, restarts only after every echo, and refuses requests naming another node.
  • Routed subscriber — #receiveRouted applies each route's claims whole at its floor, reports the best claim per path (or per node for forwarding), takes a ROUTE_END's claims offline, and echoes it.
  • Selection parity — bestCandidate, rendezvousHash, routeKey, and routeHops port Rust's best_route/route_order, including the newest claim winning a full tie while a re-price keeps its place.
  • Consume and forwarding — the cache selects and names the node of an instance's best route; forwardAnnounced keys routes by prefix plus node and pins forwarded requests to the route's instance.
  • Origin — pick and failover merge into follow, so a detached or pinned request follows its instance's cheapest route rather than staying on the first it resolved.
  • Interop and tests — 13 lite-07 route fixture cases replayed by both sides, Rust's lite07_ties_match_js and the shared goldens, and every JS lite-07 lane (interop, sockets, max-age, relay-fill, datagram-tracks, nightly drain, media replacement) restored.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Comment thread js/net/src/lite/publisher.ts Resolved
kixelated and others added 3 commits November 10, 2026 12:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A transport with no RTT never starts reporting one, and a perpetual 1 Hz
timer kept the simulated clock from ever going idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A peer's largest Min RTT plus a link exceeded u64 milliseconds, which failed
the encode and ended the downstream announce stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Heads-up from moq-dev/moq#8458, which follows the maintainer's 2026-11-10 decision: lite-07 ROUTE_START and ROUTE_UPDATE replace Down Only (8) with Flags (8). Bit 0x1 is down-only, bit 0x2 means a Min RTT (i) in milliseconds follows, and any other bit is a PROTOCOL_VIOLATION. Bytes 0 and 1 decode as before, but a Rust relay on #8458 sets 0x2 on every route it measures. This codec's 0/1 check would refuse those routes, so it needs to accept the flag and skip or read the field before both land. The read side (RouteInfo MinRtt) is tracked in quest/m2/route-latency.md.

(Written by Claude Opus 5.5 with high)

kixelated and others added 5 commits November 10, 2026 18:39
`origin::RouteInfo::rtt()` returns the whole path as `Option<Duration>`,
replacing `origin::MinRtt`; the bindings' `RouteInfo.rtt` and the draft's
`RTT (i)` field follow. `--cluster-withhold-min-rtt` and
`origin::Config::withhold_min_rtt` are removed: the value is informational.

The link sampler keeps reading after a read of no RTT, since qmux reports
its first only after a ping, and stops after a window of misses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synced drained the node's outputs before poll started sampling, so the
first claims served carried no RTT until the next read. Note in the
cluster doc that a route this node serves itself has no RTT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits November 11, 2026 02:17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 3 commits November 11, 2026 05:30
…ing/routes/js

# Conflicts:
#	quest/m2/cluster-routing/routes/README.md
#	quest/m2/cluster-routing/routes/js.md
#	sh/gh/test-inventory.allow
Adopts #8458's wire in js/net: ROUTE_START and ROUTE_UPDATE
carry Flags (0x1 down-only, 0x2 an RTT follows, any other bit a
violation) and an optional RTT in milliseconds. A browser advertises its
own route at 0. The shared corpus pins the flag and its refusals.

A grant change that flips whether a floor certifies the requested prefix
now re-floors the route even when the claims it shows are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing/routes/js

# Conflicts:
#	quest/m2/route-latency.md

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No new issues — one docs typo from the merge resolution inline.

Reviewed changes

Re-reviewed the delta since the prior Pullfrog review at 5c8f391f2, re-running the JS lite tests, the js/net typecheck, and the Rust lite corpus.

  • Route Flags and RTT codec — ROUTE_START/ROUTE_UPDATE now carry 0x1 down-only, 0x2 an RTT follows, with any other bit or a missing RTT field a violation, matching rs/moq-net/src/lite/announce.rs; the JS round-trip and refusal cases and three shared corpus entries cover it.
  • Own-route advert — a browser advertises its route at rtt: 0, matching Rust's route_rtt, which returns zero at the identity's own node.
  • Grant-change re-floor — a live route tracks what its last floor certified and re-floors when that flips, with a two-way regression test; this retires the prior review's finding.
  • Informational RTT — the subscriber decodes each route's RTT and drops it, asserted by an RTT-only ROUTE_UPDATE that reports nothing.
  • Quest docs — route-latency.md narrows to the remaining JS RouteInfo.rtt work, and the deleted js.md references are gone from the sibling route quests.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@kixelated

Copy link
Copy Markdown
Collaborator Author

Heads-up for whoever holds this PR: two changes to ROUTE_START and ROUTE_UPDATE meet it.

  • On main already (moq-dev/moq#8458): Down Only (8) became Flags (8), with 0x2 adding an optional RTT (i).
  • In review (moq-dev/moq#8487): after the RTT come Hop Count (i) and that many Hop ID (i). A node never selects a route whose Hops name it. More than 32 is a PROTOCOL_VIOLATION.

What js/net needs once #8487 lands, as quest/m2/cluster-routing/routes/js.md there says:

  • Decode the Hops on both messages.
  • Send the tab's own route with its own Hop ID alone: the Node ID it starts with, which a rotation does not change.
  • A tab forwards nothing, so it adds itself to no other route.

I have not pushed here: this branch was updated within the last hour by another session.

(Written by Claude Fable 5 with high)

kixelated and others added 3 commits November 11, 2026 11:01
Adopts #8487: ROUTE_START and ROUTE_UPDATE carry the Hop ID of
every node a route crossed, at most 32. A tab sends its own route with its
own Hop ID alone, the Node ID its origin started with, and treats a route
that crossed it or carries 32 hops as no route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing/routes/js

# Conflicts:
#	.github/workflows/nightly.yml

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

Re-reviewed the delta since the prior Pullfrog review at 764e98c0c: the branch's own 84b6c9af4, which adopts #8487's route Hops, plus two main merges whose net effect is main-only (the new test route-sweep nightly row, mesh_sweep.rs, quest removals).

  • Route Hops codec — Advert gains hops, ROUTE_START/ROUTE_UPDATE write them after the flags and RTT, and both encode and decode refuse more than ROUTE_MAX_HOPS (32) before reading them, byte-for-byte with rs/moq-net/src/lite/announce.rs.
  • Own-route Hops — Own.hop pins the Node ID the origin started with, so a tab advertises hops: [Own.hop] alone and an identity rotation leaves it unchanged, matching Rust's Router.hop.
  • Unusable routes — a route carrying 32 hops or naming this session's hop is withdrawn from the consume cache, ranks nowhere, and reports offline until a ROUTE_UPDATE makes it usable again, mirroring Rust's crossed and metric checks.
  • Publisher hop identity — a lite-07 publishing connection's hop is its origin's (Own.hop) and is shared by Publisher and Subscriber, so a copy of the tab's own route coming back is dropped rather than consumed.
  • Fixtures and tests — shared corpus entries for the hops round-trip and the over-32 and missing-hop refusals, plus a subscriber regression that drives a full and a self-crossing route offline and back.

Verification: bun test on announce, subscriber, publisher, and corpus (374 tests), bunx tsc --noEmit -p js/net/tsconfig.json, and cargo test -p moq-net --lib --features async lite::corpus all pass. The prior review's docs typo is fixed in 84b6c9af4.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

No deployments
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