Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3d9bdcc
quest(moxygen): claim line
kixelated Sep 26, 2026
5e1e491
feat(net): default an unset track priority to the midpoint (#4273)
kixelated Sep 27, 2026
fd599c0
feat(moq-net): serve IETF FETCH of whole groups, one group at a time …
kixelated Sep 27, 2026
8a86efa
feat(moq-net): carry datagrams over moq-transport as OBJECT_DATAGRAM …
kixelated Sep 27, 2026
f6e69d8
quest(moxygen): block the line on the #4276 group fetch fill findings
kixelated Sep 28, 2026
6dba670
Merge origin/main into quest/m1/moxygen/README
kixelated Sep 28, 2026
c36fb8d
Merge remote-tracking branch 'origin/quest/m1/moxygen/README' into au…
kixelated Sep 29, 2026
a9491be
quest(moxygen): whole-group truncation needs a concrete end signal
kixelated Sep 29, 2026
1297cb3
quest(moxygen): keep the accepted requested-end decline
kixelated Sep 29, 2026
3649817
quest(moxygen): exclusive end locations and every inherited first-obj…
kixelated Sep 29, 2026
eb1bccc
quest(moxygen): the fill starts its producer at frame_start
kixelated Sep 29, 2026
1cee2e1
quest(moxygen): align the fetch-fill summary
kixelated Sep 29, 2026
ff1ed07
Merge pull request #4376 from moq-dev/audit/moxygen-blockers
kixelated Sep 29, 2026
befe0de
chore(quest): claim quest/m1/moxygen/fetch-fill
kixelated Sep 29, 2026
3ab08bd
chore(quest): claim quest/m1/moxygen/fetch-span
kixelated Sep 29, 2026
338bc1b
fix(moq-net): an IETF group fetch fill is complete or refused, from t…
kixelated Sep 29, 2026
df72c33
chore(quest): finish quest/m1/moxygen/fetch-fill
kixelated Sep 29, 2026
6fbaa00
fix(ietf): skip FETCH holes to the next cached group when nothing is …
kixelated Sep 29, 2026
82cc24a
chore(quest): narrow fetch-span to the upstream range fill
kixelated Sep 29, 2026
5fe1502
quest(moxygen): fetch-span's relay half moves to subscribe-ranges
kixelated Sep 30, 2026
f75b885
quest(moxygen): drop fetch-span from the Required list
kixelated Sep 30, 2026
3c83a08
Merge pull request #4544 from moq-dev/quest/m1/moxygen/fetch-fill
kixelated Sep 30, 2026
be31e4e
Merge remote-tracking branch 'origin/quest/m1/moxygen/README' into tm…
kixelated Sep 30, 2026
ff70882
quest(moxygen): size the line now its last child is done
kixelated Sep 30, 2026
7b51ce5
Merge pull request #4558 from moq-dev/quest/m1/moxygen/fetch-span
kixelated Sep 30, 2026
5b5407c
Merge origin/main into quest/m1/moxygen/README
kixelated Sep 30, 2026
0effaaf
quest(moxygen): finish the line
kixelated Sep 30, 2026
5634346
fix(ietf): refuse to encode a non-Normal datagram status with properties
kixelated Sep 30, 2026
d400ee1
feat(ietf): serve FETCH within one group only
kixelated Sep 30, 2026
67f65f0
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/moxygen/R…
kixelated Sep 30, 2026
fd8c48d
fix(ietf): refuse FETCH on draft-20 and later
kixelated Sep 30, 2026
3b644be
fix(ietf): harden the group FETCH fill and End of Track
kixelated Sep 30, 2026
1b2cddb
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/moxygen/R…
kixelated Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1775,7 +1775,7 @@ class MoqTrackInfo {
final int? maxAgeUs;
final int? timescale;
MoqTrackInfo({
this.priority = 0,
this.priority = 127,
this.maxAgeUs = null,
this.timescale = null,
});
Expand Down Expand Up @@ -12573,7 +12573,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackconsumer_recv_datagram() !=
29049) {
17412) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackconsumer_recv_group() != 60887) {
Expand Down
31 changes: 24 additions & 7 deletions doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,35 @@ the block; other groups and the session stay open.
An IETF publisher declares the track's default priority in `SUBSCRIBE_OK` or
`PUBLISH` when that draft carries track properties. Groups without a priority
flag inherit it. If the property is absent, the IETF wire default of 128 maps
to model priority 127, where higher values are served first.

On drafts 14–19, the Rust publisher serves relative joining `FETCH` requests
with offset zero for `NextObject` subscriptions. The fetch delivers the saved
current-group prefix, and the subscription delivers later objects. Standalone,
absolute joining, and nonzero-offset fetches are refused. Draft-20 uses
subscription fills instead. JavaScript publishing does not yet serve `FETCH`;
to model priority 127, where higher values are served first. A track that
never sets a priority is 127 as well, so it goes out as 128 on IETF and 127
on moq-lite.

On drafts 14–19, the Rust publisher answers a standalone `FETCH` within one
group from the cache. A relay fetches a missing group upstream with a `FETCH`
of that one whole group, and an upstream refusal is the refusal the fetcher
sees. A range touching several groups is refused with `NOT_SUPPORTED`, as is
any `FETCH` on draft-20 and later, which moved the range into
`LOCATION_FILTER`. A standalone `FETCH`
carries no timestamps, since no `SUBSCRIBE_OK` declared a timescale for it.

On drafts 14–19, the Rust publisher also serves relative and absolute joining
`FETCH` requests for `NextObject` subscriptions, for the subscription group's
saved prefix only, while the subscription delivers later objects. One reaching
back to earlier groups is refused with `NOT_SUPPORTED`. Draft-20 uses
subscription fills instead. JavaScript
publishing does not yet serve `FETCH`;
Rust and JavaScript subscribers request unfiltered delivery on older drafts
because they do not issue joining fetches. Other publishers may replay a cached
backlog for that filter; selecting the next group instead would leave static
tracks waiting for a group that never arrives.

A moq-lite datagram is a single-frame group, so on moq-transport it travels
as an `OBJECT_DATAGRAM` at object 0 whose Group ID is the sequence, and a relay
forwards it without renumbering. A datagram carrying any other Object ID, or a
status other than Normal, is dropped. JavaScript does not yet carry datagrams
on moq-transport.

A client may present one credential in its `SETUP` with the `AUTHORIZATION
TOKEN` option. The server reads a value (`USE_VALUE`, or `REGISTER`, which it
treats as a value since it advertises no token cache) and hands its Token Type
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ above ([hang](/lib/rs/hang)); relays and CDNs implement only this.
- **Tracks** carry groups with a priority, a retention window, and a timescale. Subscribers set their own priority and max age and can change them live.
- **Groups** are written frame by frame and delivered on independent streams. Old groups are cached for fetch-by-sequence; stale groups are skipped per the subscriber's budget.
- **Track ends**: `finish()` ends a track at its live edge, while `finish_at(n)` declares the exclusive end ahead of it and still accepts the groups below. A subscriber awaits it with `finished()`. A remote track ends only once every group below its end has arrived or was dropped; one reset before its header arrived is skipped after the subscription's max age on moq-lite (one second without one), or after one second on IETF.
- **Datagrams** send a single small frame unreliably on moq-lite 05+.
- **Datagrams** send a single small frame unreliably on moq-lite 05+ and moq-transport.
- **Routes** record the relay hops and a cost, which is what the relay [cluster](/bin/relay/cluster) routes on. A hop of 0 marks the chain anonymous: `Route::is_anonymous()` is true, and that route ranks below every fully identified one. `Route::source()` says where a delivered route entered: `Source::Local`, or `Source::Peer(hop)` when a handle marked `origin::Producer::peer()` announced it. `origin::Consumer::local()` sees only the local ones.
- **Stats** counters per broadcast and session, drained by [`moq-stats`](https://docs.rs/moq-stats).

Expand Down
3 changes: 2 additions & 1 deletion drafts/draft-lcurley-moq-e2ee.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ An Object ID above `2^32-1` MUST be refused as `identity` before encryption or d

## Datagrams
A datagram uses `domain = 0x01`, its 64-bit sequence as `group`, and `frame = 0`.
MoQ Transport has no datagram mapping in this profile; shared vectors cover grouped tracks on both transports and datagrams on moq-lite only.
On MoQ Transport, a datagram is an Object at ID 0 in an OBJECT_DATAGRAM whose Group ID is the sequence.
Shared vectors cover grouped tracks on both transports and datagrams on moq-lite only.

## Nonce
The 96-bit AES-GCM nonce is:
Expand Down
2 changes: 1 addition & 1 deletion go/wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Raw tracks support best-effort datagrams alongside groups: `TrackProducer.Append
sends one `Frame` and returns its sequence number, while `TrackConsumer.RecvDatagram`
and `TrackConsumer.Datagrams` receive them in arrival order. Payloads are capped at
1200 bytes. Datagram delivery requires a datagram-capable transport and lite-05 or
newer moq-lite; IETF moq-transport, pre-lite-05, WebSocket, and TCP paths do not
newer moq-lite, or moq-transport; pre-lite-05, WebSocket, and TCP paths do not
deliver them, and there is no stream fallback.

## Versioning
Expand Down
1 change: 1 addition & 0 deletions go/wrapper/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type (
// Subscription holds subscriber-side delivery preferences: priority, ordering, max age, and group range.
Subscription = ffi.MoqSubscription
// TrackInfo holds publisher-side track properties: priority, ordering, max age, and timescale.
// A zero Priority is the least urgent, not the default; set 127 for the midpoint a nil TrackInfo uses.
TrackInfo = ffi.MoqTrackInfo
// Video describes one catalog rendition, including whether the publisher recommends temporarily avoiding it.
Video = ffi.MoqVideo
Expand Down
5 changes: 5 additions & 0 deletions js/net/src/ietf/priority.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "bun:test";
import { infoDefaults } from "../track.ts";
import { fromWire, toWire } from "./priority.ts";

test("IETF subscriber priority is lower first", () => {
Expand All @@ -17,3 +18,7 @@ test("subscriber priority round trips", () => {
expect(fromWire(toWire(priority))).toBe(priority);
}
});

test("an unset track priority is the draft's usual publisher priority", () => {
expect(toWire(infoDefaults().priority)).toBe(128);
});
2 changes: 1 addition & 1 deletion js/net/src/lite/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test("TrackInfo round-trips on draft-05", async () => {
test("TrackInfo defaults match cross-language wire bytes", async () => {
const info = new TrackInfo(infoDefaults());
expect(await bytes((w) => info.encode(w, Version.DRAFT_05), Version.DRAFT_05)).toEqual(
new Uint8Array([0x06, 0x00, 0x00, 0x53, 0x88, 0x43, 0xe8]),
new Uint8Array([0x06, 0x7f, 0x00, 0x53, 0x88, 0x43, 0xe8]),
);
});

Expand Down
4 changes: 2 additions & 2 deletions js/net/src/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ function mockMonotonicTime(initial: number) {
};
}

test("priority reads the committed info and is 0 before accept", () => {
test("priority reads the committed info and is the midpoint before accept", () => {
const producer = new TrackProducer("video");
expect(producer.priority).toBe(0);
expect(producer.priority).toBe(127);
producer.accept({ priority: 60 });
expect(producer.priority).toBe(60);
});
Expand Down
12 changes: 8 additions & 4 deletions js/net/src/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const PRUNE_SLICES = 8;
/** Default {@link Info.maxAge} window (milliseconds) when the publisher does not set one. */
export const DEFAULT_MAX_AGE_MS = Milli(5000);

// The higher-first midpoint. IETF flips priority (lower first), so this goes out as 128, the
// draft's usual publisher priority, while moq-lite carries 127 as written: one urgency on both.
const DEFAULT_PRIORITY = 127;

/** Maximum buffered datagrams per subscriber; mirrors Rust's bounded send buffer. */
const MAX_DATAGRAMS = 64;

Expand Down Expand Up @@ -70,7 +74,7 @@ export interface Info {
* or non-finite value and a result past `Number.MAX_SAFE_INTEGER`.
*/
maxAge: Milli;
/** Tie-break priority between subscriptions of equal subscriber priority (`0..=255`). */
/** Tie-break priority between subscriptions of equal subscriber priority (`0..=255`, higher first). Defaults to `127`. */
priority: number;
}

Expand Down Expand Up @@ -103,7 +107,7 @@ export function infoDefaults(info: Partial<Info> = {}): Info {
return {
timescale: Timescale(info.timescale ?? Timescale.MILLI),
maxAge: maxAgeMillis(info.maxAge ?? DEFAULT_MAX_AGE_MS),
priority: priorityByte(info.priority ?? 0),
priority: priorityByte(info.priority ?? DEFAULT_PRIORITY),
};
}

Expand Down Expand Up @@ -481,13 +485,13 @@ export class Producer {
}

/**
* Publisher priority from the committed {@link Info}, or 0 before {@link accept}.
* Publisher priority from the committed {@link Info}, or the default before {@link accept}.
*
* Higher is served first. Hang publishers set this from `Catalog.PRIORITY` so
* audio outranks video on the wire and in the bandwidth allocator.
*/
get priority(): number {
return this.#state.info.peek()?.priority ?? 0;
return this.#state.info.peek()?.priority ?? DEFAULT_PRIORITY;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion kt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The `dev.moq` package is intentionally thin: Kotlin has extension functions, so
- **Fetched media**: `fetchMediaGroup(...).frames()` streams the decoded frames of one retained group, then completes.
- **Duration extensions** (`Durations.kt`): the FFI carries microseconds as integers, so `stats.rtt`, `backoff.initial`, `frame.timestamp`, and their siblings read back as a `kotlin.time.Duration`.
- **`logLevel(...)`**: configures native Rust tracing without importing the raw bindings package.
- **Raw datagrams**: `TrackProducer.appendDatagram(Frame(payload, timestampUs))` sends one best-effort frame and returns its sequence; `TrackConsumer.recvDatagram()` and `datagrams()` receive them. Payloads are capped at 1200 bytes, require a datagram-capable transport plus lite-05 or newer moq-lite, and have no stream fallback.
- **Raw datagrams**: `TrackProducer.appendDatagram(Frame(payload, timestampUs))` sends one best-effort frame and returns its sequence; `TrackConsumer.recvDatagram()` and `datagrams()` receive them. Payloads are capped at 1200 bytes, require a datagram-capable transport plus lite-05 or newer moq-lite or moq-transport, and have no stream fallback.
- **`MoqException.isShutdown`** (`Errors.kt`): true for the graceful `Cancelled`/`Closed` cases.

## Versioning
Expand Down
2 changes: 1 addition & 1 deletion py/moq-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Every handle whose cleanup is `cancel()` is an async context manager, so exiting
- **`Catalog`**. `.audio: dict[str, Audio]`, `.video: dict[str, Video]`, `.display`, `.rotation`, `.flip`.
- **`Frame`**. `.payload: bytes`, `.timestamp_us: int`. The unit of every write and every raw read.
- **`MediaFrame`**. `.payload: bytes`, `.timestamp_us: int`, `.keyframe: bool`. Returned by media subscriptions. `keyframe` marks a group start or video keyframe; for audio it is true only at a group start.
- **`Datagram`**. `.sequence: int`, `.timestamp_us: int`, `.payload: bytes`. Delivered only on datagram-capable transports and lite-05 or newer moq-lite.
- **`Datagram`**. `.sequence: int`, `.timestamp_us: int`, `.payload: bytes`. Delivered only on datagram-capable transports with lite-05 or newer moq-lite, or moq-transport.
- **`Audio`**. `.codec`, `.sample_rate`, `.channel_count`, `.bitrate`, `.description`.
- **`Video`**. `.codec`, `.coded: Dimensions`, `.display_aspect`, `.bitrate`, `.stalled`, `.framerate`, `.description`. A true `.stalled` recommends temporarily avoiding the rendition without making it unavailable.
- **`Subscription`**. Subscriber delivery preferences: priority, staleness, and optional group range.
Expand Down
2 changes: 1 addition & 1 deletion quest/m0/ietf-legal-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Each of these closes the session today with `PROTOCOL_VIOLATION`:
- FETCH on drafts 20+ still decodes the removed Fetch Type field
(`ietf/fetch.rs`). Decode the draft-20 layout: namespace, name and params,
with the range in LOCATION_FILTER. Refuse it `NOT_SUPPORTED` until
[moxygen FETCH](/quest/m1/moxygen/fetch.md) serves it. Fix the encoder and
[Draft-20 FETCH](/quest/m1/ietf-fetch-location.md) serves it. Fix the encoder and
the pinned test in `version.rs`.
- Request parameters the draft allows on a message fail `decode_params!`:
AUTHORIZATION TOKEN (0x03) anywhere, NEW_GROUP_REQUEST (0x32) and the
Expand Down
4 changes: 0 additions & 4 deletions quest/m0/ietf-subgroup-refusal.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,3 @@ is the check.
Moved from the moxygen line to m0 in the 2026-09-30 audit: a non-zero
subgroup ending the upstream session is exactly m0's "legal input never fails
a session", and moxygen will send it at Seattle interop on 2026-10-12.

## Related

- [Moxygen compatibility](/quest/m1/moxygen/README.md) - subgroups stay out of scope; only the blast radius is in
3 changes: 2 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ QUIC studies there on that rule.
- [Data track clock](/quest/m1/data-track-clock.md) - JSON and binary data tracks stamp on the catalog's clock at write time, matching the media's anchored clock
- [Go and Dart doc samples](/quest/m1/doc-samples-go-dart.md) - Go and Dart doc samples compile against their wrappers
- [Data capture in bindings](/quest/m1/data-capture-bindings.md) - moq-ffi and every wrapper pass a data frame's capture time, and the JSON window producer takes one
- [Moxygen compatibility](/quest/m1/moxygen/README.md) - one subgroup per group, whole-group FETCH, and one datagram per group, never a full moxygen pass
- [Draft-20 FETCH](/quest/m1/ietf-fetch-location.md) - a draft-20+ FETCH within one group is served from its LOCATION_FILTER, as older drafts are
- [Fetch without SUBSCRIBE](/quest/m1/ietf-fetch-only.md) - a relay fetches from an IETF upstream without subscribing, finished tracks included, with End of Track always reported
- [JS IETF datagrams](/quest/m1/js-ietf-datagram.md) - `@moq/net` sends and receives datagram groups over moq-transport, like Rust
- [Datagrams are live-only](/quest/m1/datagram-unfetchable.md) - no FETCH, replay to a new subscriber, or cache fill ever returns a datagram group
- [#2991](/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md) - one dynamic producer per track name in both languages, with the sequence namespace surviving a replacement
Expand Down
3 changes: 1 addition & 2 deletions quest/m1/datagram-unfetchable.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ datagram flag, and reports it as "unsupported":
- A received fetch object with the datagram flag is refused as not fetchable.
It fails only that fetch or fill, not the session, because draft-16+ allows
the flag (maintainer, 09-29, on Codex's review). That covers the relay's group
fill (`recv_group_fetch_objects`, on the moxygen line) and the joining-fetch
fill (`recv_group_fetch_objects`) and the joining-fetch
fill (`run_fill_objects`).
- Update `drafts/draft-lcurley-moq-lite.md` to say datagrams are neither
cached nor fetchable, and `doc/concept/`. Run `just drafts check` and
Expand All @@ -51,6 +51,5 @@ range apart from one dropped for any other reason.

## Related

- [Moxygen compatibility](/quest/m1/moxygen/README.md) - says it carries one datagram per group live; this keeps them out of FETCH
- [JS IETF datagrams](/quest/m1/js-ietf-datagram.md) - JS datagram send and receive over moq-transport
- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - datagram groups stay best effort there too
2 changes: 1 addition & 1 deletion quest/m1/e2ee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The Rust and TypeScript cores expose the same surface, and nothing else:

- Deterministic secret-derived physical names hide catalog, codec, role, quality, timeline, and custom-track semantics. Authorized clients derive the encrypted catalog track name, then learn the remaining opaque names from its decrypted contents. Every catalog representation is encrypted; Rust publishers must not emit a plaintext MSF catalog.
- A platform that forwards and meters protected bytes must never preview, record, archive, transmux, transcode, transcribe, compose, or inspect them, rejecting those paths before opening a processing session or writing product state. Applications needing those operations terminate E2EE outside the platform. A platform classifies protected broadcasts by its own credential or product state, never by name; the moq.pro (downstream) exclusion classifier and dashboard work stay downstream.
- The first proof covers browser TypeScript and native Rust publication and playback in both directions, with grouped audio and video over both moq-lite and MoQ Transport. Shared vectors cover groups and moq-lite datagrams; MoQ Transport has no datagram delivery.
- The first proof covers browser TypeScript and native Rust publication and playback in both directions, with grouped audio and video over both moq-lite and MoQ Transport. Shared vectors cover groups and moq-lite datagrams; JavaScript has no MoQ Transport datagram delivery yet.

## Required

Expand Down
23 changes: 23 additions & 0 deletions quest/m1/ietf-fetch-location.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# [S] Draft-20 FETCH

## Goal

On drafts 20 and later, a FETCH whose LOCATION_FILTER stays within one group
is answered the way drafts 14 to 19 answer a standalone FETCH: from cache, with
a miss fetched upstream, and an upstream refusal passed through. A range
touching several groups is refused `NOT_SUPPORTED`, as on older drafts.

## Plan

Today both sides refuse FETCH on draft-20+ `NOT_SUPPORTED`: the publisher in
`run_fetch_stream`, and the relay's upstream group fill in `run_group_fetch`,
because the codec still carries the removed Fetch Type field.
[Legal IETF input](/quest/m0/ietf-legal-input.md) fixes the codec. Then drop
both refusals, and flip `fetch_moq_transport_20` in `rs/moq-tokio/tests` back
to a served fetch.

Update the draft-20 note in `doc/concept/standard.md`.

## Required

- [Legal IETF input](/quest/m0/ietf-legal-input.md) - decodes the draft-20 FETCH this serves
29 changes: 29 additions & 0 deletions quest/m1/ietf-fetch-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# [M] Fetch without SUBSCRIBE

## Goal

A relay serves a FETCH-only demand for an IETF upstream track without
SUBSCRIBE upstream. A finished upstream track can still be fetched, and a
FETCH_OK that reaches the end of the track says so every time.

## Plan

The origin splices a route only once the track's info is known. moq-lite gets
it from TRACK_INFO, so its fetch-only demand never subscribes. IETF has no
TRACK_INFO, so today fetch-only demand still makes the relay SUBSCRIBE upstream.
Two symptoms follow:

- A finished upstream track refuses the SUBSCRIBE, so its groups cannot be
fetched either.
- The live subscription races the group FETCHes. End of Track is known only
once an upstream FETCH_OK reports it, so a downstream FETCH that runs to the
end can answer before that and leave End of Track unset. moxygen's "FETCH
with large objects" case flakes on this.

TRACK_STATUS is the likely source of the info. The publisher refuses it today,
so both sides are in scope. Keep the SUBSCRIBE path for real subscription
demand.

## Related

- [JavaScript FETCH](/quest/m1/js-fetch.md) - the browser publisher answers these fetches
Loading
Loading