Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ class MoqSubscription {
final MoqOrder? order;
MoqSubscription({
this.priority = 0,
this.maxDelay = null,
this.maxDelay,
this.ranges = null,
this.order = null,
});
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/js/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ publication.announce();
- **One track per name.** Concurrent subscriptions share one request and producer. `createTrack` on a name with a request still queued answers it. The application owns sequence numbers: the broadcast keeps nothing of an ended track, so a track that replaces it, created or requested, starts at 0. A name always means the same content, so the replacement continues past its predecessor's groups by writing its first group with an explicit sequence.
- **Timing is per track, with no default.** A track that declares a `timescale` carries a `timestamp` on every frame; one without is [untimed](/concept/moq-lite#subscriptions) and its frames carry none. Omitting it means untimed, as in Rust. Nothing stamps a frame for you: the publisher passes the timestamp, and the `Timed` type (`{ value, at }`) carries one into the `@moq/json` producers.
- **Subscriber staleness is `maxDelay`.** It counts from a group's successor, by media time and by wall clock, whichever reaches it first. Passing the old `maxAge` key throws a `TypeError` naming `maxDelay`. Publisher retention is the separate `Track.Info.maxAge`.
- **Subscription defaults.** `Track.Subscription.default()` returns the default subscription, whose zero `maxDelay` skips a superseded group at once; calling `subscribe()` with no options means the same. Spread it to change a field (`{ ...Track.Subscription.default(), priority: 2 }`) rather than passing a bare options object, whose omitted `maxDelay` sets no subscriber limit.
- **Subscription defaults.** `Track.Subscription.default()` returns the default subscription, whose zero `maxDelay` skips a superseded group at once; calling `subscribe()` with no options means the same. An omitted `maxDelay` means the same zero; `null` sets no subscriber limit.
- **Hidden paths** stay out of discovery unless the announce request opts in. See [hidden broadcasts](/concept/moq-lite#hidden-broadcasts).
- **Authorization is in band.** On moq-lite 07 (opt in with `versions: ["moq-lite-07-wip", "moq-lite-06"]`, listing every fallback you still want) and on moq-transport draft-18+ with the [MoQ Auth extension](/draft/moq-auth), `auth.grant` watches what the relay lets this side publish and subscribe to, and `auth.add(token)` presents another token without reconnecting. Publishing outside the grant closes the connection with `SessionCode.Unauthorized`, naming the path. On moq-lite, a subscription or track listing the grant stops covering resets with `StreamCode.Unauthorized` and the session stays up. On moq-transport, `auth.add(token)` keeps the token even without the extension, and a broadcast announced with the `Auth.Credential` it returns (`publication.announce({ auth })`) carries it on that announcement's `PUBLISH_NAMESPACE`; announcing again with another renews the request. A `Connection`'s `auth` handle (a getter on a pooled connection, following its URL) outlives its sessions: a token added there is presented on every reconnect and keeps riding the announcements that name it, with no re-add or re-announce. A one-shot session's `auth` ends with it, unless `Connection.connect` was given a `new Auth.Handle()` as `auth`.
- **A graceful close waits.** `await connection.close()` withdraws announcements and drains for up to one second. Application delivery requires explicit `moq-lite-07-wip`; older Lite versions wait only for transport ACKs. IETF media streams are not drained. `abort()` ends immediately.
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/kt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ shows which module holds each.
- **Tokens.** Pass `ClientConfig(tokens = listOf(...))` instead of putting the credential in the URL. The first is the connection credential, sent in SETUP and, while an offered version can't carry it there (lite before 07), as the URL's `?jwt=`; each other token widens the grant over AUTH. `moq.tokens().set(listOf(...))` replaces them before they expire: the next dial presents them, a live AUTH session at once, and after an authorization refusal the session redials only if they changed. Suspending `tokens().answered()` returns once the relay accepts them and throws its refusal with the relay's code. A `?jwt=` already in the URL stays the connection credential, which is how to reach a relay older than this release.
- **Errors.** Read diagnostic payloads from `reason` and structured protocol failures from `details`.
- **Disabled renditions.** A rendition with `enabled: false` has no frames coming until its publisher re-enables it. Select an enabled rendition from the latest catalog before opening a decoder. Keep one already playing while every rendition of its kind is disabled: the convenience decoders accept a disabled configuration, and a re-enable, such as an unmute, then plays without a resubscribe. Among the video renditions you can decode, prefer the highest `preference`; `0` is the default and `-1` marks a compatibility transcode.
- **Subscription defaults.** `Subscription.default()` returns moq-net's defaults, whose zero `maxDelay` skips a superseded group at once; passing no subscription means the same. `copy` it rather than starting from `Subscription()`, whose null `maxDelay` sets no subscriber limit.
- **Subscription defaults.** `Subscription.default()` returns moq-net's defaults, whose zero `maxDelay` skips a superseded group at once; passing no subscription means the same. `copy` it to change a field; building a `Subscription(...)` must name `maxDelay`, where null sets no subscriber limit.
- **Track demand.** Obtain a dynamic track request’s `demand()` before accepting it to watch subscribers through the handoff to its producer. The watch does not keep the track alive and ends when the track closes or the request is rejected.

## Runtime
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ shows which module holds each.
- **Tokens.** Pass `tokens=[...]` instead of putting the credential in the URL. The first is the connection credential, sent in SETUP and, while an offered version can't carry it there (lite before 07), as the URL's `?jwt=`; each other token widens the grant over AUTH. `session.tokens().set([...])` replaces a client session's tokens before they expire: the next dial presents them, a live AUTH session at once, and after an authorization refusal the session redials only if they changed. `await tokens.answered()` returns once the relay accepts them and raises its refusal with the relay's code. A `?jwt=` already in the URL stays the connection credential, which is how to reach a relay older than this release.
- **Errors.** Read diagnostic payloads from `reason` and structured protocol failures from `details`.
- **Disabled renditions.** A rendition with `enabled: false` has no frames coming until its publisher re-enables it. Select an enabled rendition from the latest catalog before opening a decoder. Keep one already playing while every rendition of its kind is disabled: the convenience decoders accept a disabled configuration, and a re-enable, such as an unmute, then plays without a resubscribe. Among the video renditions you can decode, prefer the highest `preference`; `0` is the default and `-1` marks a compatibility transcode.
- **Subscription defaults.** `moq.Subscription.default()` returns moq-net's defaults, whose zero `max_delay` skips a superseded group at once; passing no subscription means the same. `dataclasses.replace` it rather than starting from `moq.Subscription()`, whose `None` `max_delay` sets no subscriber limit.
- **Subscription defaults.** `moq.Subscription.default()` returns moq-net's defaults, whose zero `max_delay` skips a superseded group at once; passing no subscription means the same. A bare `moq.Subscription()` has the same defaults; an explicit `max_delay=None` sets no subscriber limit.
- **Track demand.** Obtain a dynamic track request’s `demand()` before accepting it to watch subscribers through the handoff to its producer. The watch does not keep the track alive and ends when the track closes or the request is rejected.

## Runtime
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/swift/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ shows which module holds each.
- **Tokens.** Pass `Client(tokens: [...])` instead of putting the credential in the URL. The first is the connection credential, sent in SETUP and, while an offered version can't carry it there (lite before 07), as the URL's `?jwt=`; each other token widens the grant over AUTH. `try session.tokens()?.set([...])` replaces them before they expire: the next dial presents them, a live AUTH session at once, and after an authorization refusal the session redials only if they changed. `try await tokens.answered()` returns once the relay accepts them and throws its refusal with the relay's code. A `?jwt=` already in the URL stays the connection credential, which is how to reach a relay older than this release.
- **Errors.** Diagnostic cases use the `reason:` argument label; match their associated value to inspect it. Protocol errors carry `details`.
- **Disabled renditions.** A rendition with `enabled: false` has no frames coming until its publisher re-enables it. Select an enabled rendition from the latest catalog before opening a decoder. Keep one already playing while every rendition of its kind is disabled: the convenience decoders accept a disabled configuration, and a re-enable, such as an unmute, then plays without a resubscribe. Among the video renditions you can decode, prefer the highest `preference`; `0` is the default and `-1` marks a compatibility transcode.
- **Subscription defaults.** `Subscription.default` holds moq-net's defaults, whose `maxDelay` of 0 skips a superseded group at once; passing no subscription means the same. Copy it and change a field rather than starting from `Subscription()`, whose `nil` `maxDelay` sets no subscriber limit.
- **Subscription defaults.** `Subscription.default` holds moq-net's defaults, whose `maxDelay` of 0 skips a superseded group at once; passing no subscription means the same. Copy it to change a field; building a `Subscription(...)` must name `maxDelay`, where `nil` sets no subscriber limit.
- **Track demand.** Obtain a dynamic track request’s `demand()` before accepting it to watch subscribers through the handoff to its producer. The watch does not keep the track alive and ends when the track closes or the request is rejected.

## Runtime
Expand Down
15 changes: 2 additions & 13 deletions js/net/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,9 @@ How to update your code for each breaking change, newest first.

## Unreleased

### An omitted `maxDelay` sets no subscriber limit ([#7077](https://github.com/moq-dev/moq/pull/7077))
### `maxDelay: null` sets no subscriber limit ([#7403](https://github.com/moq-dev/moq/pull/7403))

A `Track.Subscription` without `maxDelay` now reaches back as far as the publisher's `Track.Info.maxAge` keeps, as `None` does in Rust, instead of meaning zero.
`subscribe()` with no options still uses the default subscription, now spelled `Track.Subscription.default()`, whose zero `maxDelay` skips a superseded group at once.
A live reader that passed options without `maxDelay` spreads the default to keep its behavior:

```ts
// Before
const sub = track.subscribe({ priority: 2 });
// After
const sub = track.subscribe({ ...Track.Subscription.default(), priority: 2 });
```

The same applies to `Subscriber.update(options)`, which replaces every field.
A `Track.Subscription` whose `maxDelay` is `null` reaches back as far as the publisher's `Track.Info.maxAge` keeps, as `None` does in Rust. An omitted or `undefined` `maxDelay` is still zero.

### Paths fit moq-transport's 4096-byte namespace ([#6927](https://github.com/moq-dev/moq/pull/6927))

Expand Down
2 changes: 1 addition & 1 deletion js/net/api/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ export namespace Track {
};
export interface Subscription {
groups?: Track.Groups;
maxDelay?: Milli;
maxDelay?: Milli | null;
order?: Track.Order;
priority?: number;
}
Expand Down
3 changes: 2 additions & 1 deletion js/net/src/ietf/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
groupBounds,
hiddenBelow,
hooks,
maxDelayOf,
scopeCaptures,
scopeHead,
scopeOverlaps,
Expand Down Expand Up @@ -1087,7 +1088,7 @@ export class Subscriber {
// keeps what it last asked for until one returns, as on lite and in Rust.
const subscription = producer.subscription.peek();
const wanted = toWire(subscription?.priority ?? 0);
const wantedMaxAge = this.#maxAge ? subscription?.maxDelay : undefined;
const wantedMaxAge = this.#maxAge && subscription ? maxDelayOf(subscription) : undefined;
if (!subscription || (wanted === sent && wantedMaxAge === sentMaxAge) || unanswered.peek() > 0) {
const disposes: Dispose[] = [];
const wake = new Promise<void>((resolve) => {
Expand Down
12 changes: 11 additions & 1 deletion js/net/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* @module
*/
import type { Instant, Milli } from "@moq/clock";
import { type Instant, Milli } from "@moq/clock";
import type { Dispose, GetPromise, Getter } from "@moq/signals";
import type { Consumer as BroadcastConsumer, Producer as BroadcastProducer } from "./broadcast.ts";
import type { Datagram } from "./datagram.ts";
Expand All @@ -18,8 +18,18 @@ import type { Request as OriginRequest } from "./origin.ts";
import * as Path from "./path.ts";
import type { Timestamp } from "./time.ts";
import type { Demand, Groups, Info, Position, Producer, Request, Subscriber, Subscription } from "./track.ts";
import * as Varint from "./varint.ts";
import type { Advertised, Advertisements } from "./wire.ts";

/** What a null maxDelay goes out as: the largest value every peer can read, as Rust sends for `None`. */
export const NO_MAX_DELAY = Milli(Varint.MAX_U53);

/** A subscription's budget as the wire carries it: null for no limit, omitted for zero. */
export function maxDelayOf(subscription: Pick<Subscription, "maxDelay">): Milli {
if (subscription.maxDelay === null) return NO_MAX_DELAY;
return subscription.maxDelay ?? Milli.zero;
}

/** Normalize public group bounds into an inclusive start and exclusive end. */
export function groupBounds(groups: Groups = {}): { start: number; end?: number } {
const bound = (value: Groups["start"] | undefined, start: boolean): number | undefined => {
Expand Down
6 changes: 3 additions & 3 deletions js/net/src/lite/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from "../error.ts";
import * as netGroup from "../group.ts";
import { Cost, type Hop, MAX_HOPS, type Route, routesEqual, UNKNOWN_HOP } from "../hop.ts";
import { groupBounds, hiddenBelow, hooks, scopeCaptures, scopeHead, scopeOverlaps } from "../internal.ts";
import { groupBounds, hiddenBelow, hooks, maxDelayOf, scopeCaptures, scopeHead, scopeOverlaps } from "../internal.ts";
import { grace } from "../ledger.ts";
import type { Lister } from "../listing.ts";
import * as Path from "../path.ts";
Expand Down Expand Up @@ -931,7 +931,7 @@ export class Subscriber {
track: request.name,
priority: subscription.priority ?? 0,
order: subscription.order,
maxDelay: subscription.maxDelay,
maxDelay: maxDelayOf(subscription),
startGroup: subscription.groups?.start === undefined ? undefined : bounds.start,
endGroup: inclusiveGroupEnd(bounds.end),
});
Expand Down Expand Up @@ -1650,7 +1650,7 @@ export class Subscriber {
wantedGroup: refresh ?? sentWanted,
priority: current.priority ?? 0,
order: current.order,
maxDelay: current.maxDelay,
maxDelay: maxDelayOf(current),
startGroup: current.groups?.start === undefined ? undefined : bounds.start,
endGroup: inclusiveGroupEnd(bounds.end),
});
Expand Down
12 changes: 8 additions & 4 deletions js/net/src/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ test("zero latency takes the latest group when ages are equal", async () => {
}
});

test("the default subscription skips superseded groups and a bare options object sets no limit", async () => {
test("an omitted maxDelay is the default zero, and null sets no limit", async () => {
expect(Subscription.default()).toEqual({ priority: 0, maxDelay: Milli.zero, order: "descending" });

const producer = new TrackProducer("test").accept({ timescale: Timescale.MILLI, maxAge: Milli(5000) });
Expand All @@ -837,11 +837,15 @@ test("the default subscription skips superseded groups and a bare options object
producer.writeFrame({ payload: enc.encode(`${timestamp}`), timestamp: Timestamp.fromMillis(timestamp) });
}

// No options means the default subscription, whose zero budget keeps only the newest group.
// Each spelling of the default keeps only the newest group.
expect((await explicit.recvGroup())?.sequence).toBe(2);
expect((await implicit.recvGroup())?.sequence).toBe(2);
// An omitted maxDelay leaves the publisher's retention as the only bound.
for (const sequence of [0, 1, 2]) expect((await bare.recvGroup())?.sequence).toBe(sequence);
expect((await bare.recvGroup())?.sequence).toBe(2);
expect(producer.subscription.peek()?.maxDelay).toBe(Milli.zero);

// An explicit null leaves the publisher's retention as the only bound.
const unlimited = producer.subscribe({ maxDelay: null });
for (const sequence of [0, 1, 2]) expect((await unlimited.recvGroup())?.sequence).toBe(sequence);
expect(producer.subscription.peek()?.maxDelay).toBe(Milli(5000));
producer.finish();
producer.close();
Expand Down
Loading