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
13 changes: 13 additions & 0 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ Rust and TypeScript speak moq-lite 01 through 06 and moq-transport drafts
still in progress: it negotiates as `moq-lite-07-wip`, and only when both
sides explicitly enable it.

## Subscription completion

On moq-lite 07, `SUBSCRIBE_END` counts the group streams opened for the
subscription. Rust and TypeScript stop waiting for missing streams once that
many headers have arrived; skipped group sequences add no wait. Groups already
being received continue until their own stream ends or resets.

A stream reset before its header arrived cannot be counted, so the subscriber
still allows a grace period for late streams. The grace uses the subscription's
nonzero effective maximum age, or one second when no maximum age is set.
moq-lite 05 and 06 instead account for group sequences using received headers
and `SUBSCRIBE_DROP`.

## Discovery

A session can ask for announcements matching a path prefix. The peer replies
Expand Down
25 changes: 17 additions & 8 deletions js/net/src/lite/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ import {
SubscribeUpdate,
} from "./subscribe.ts";
import { TrackInfo, Track as TrackMessage } from "./track.ts";
import { hasAnnounceId, hasAnnounceOk, hasDatagrams, hasProbeRtt, restartSupported, Version } from "./version.ts";
import {
hasAnnounceId,
hasAnnounceOk,
hasDatagrams,
hasProbeRtt,
hasStreamCount,
restartSupported,
Version,
} from "./version.ts";

// Bound on how long stream-open plus the first response (SUBSCRIBE_OK on older
// drafts, or TRACK_INFO on lite-05+) may take. Browsers cap concurrent QUIC streams
Expand Down Expand Up @@ -83,6 +91,8 @@ interface SubscribeEntry {
// (SUBSCRIBE_END), once it declares them.
start?: number;
end?: number;
// Group streams opened by the publisher, when SUBSCRIBE_END carries the count.
streams?: number;
}

/**
Expand Down Expand Up @@ -833,6 +843,7 @@ export class Subscriber {
} else if ("end" in resp) {
if (entry.end !== undefined) throw new ProtocolViolation("duplicate SUBSCRIBE_END");
entry.end = resp.end.group;
if (hasStreamCount(this.version)) entry.streams = resp.end.streams;
// A local close can win the race with the response; there is nothing left to end.
if (entry.track.closed.peek() !== undefined) continue;
try {
Expand All @@ -848,20 +859,18 @@ export class Subscriber {

// Wait for the group streams the publisher still owes once it has ended the subscription.
//
// Its FIN says every group below the end is accounted for, but QUIC does not order streams,
// so one can still be in flight. Wait until each group from SUBSCRIBE_START to the end has
// a stream (read to its end) or a SUBSCRIBE_DROP. A group reset before its header arrived
// never shows up, so give up on missing groups after the subscription's effective max age,
// then end cleanly with them skipped like any stale group. That is a wall-clock stopgap for
// a presentation-time budget; a publisher sending SUBSCRIBE_DROP for every group it reset
// would account for them with no timer at all.
// lite-07 counts streams, so skipped sequences owe nothing. Older drafts account for
// the range using received headers and SUBSCRIBE_DROP. A counted stream reset before
// its header leaves no trace, so the grace still bounds that wait. Streams whose
// headers arrived keep reading until their own FIN or reset.
#settleTail(entry: SubscribeEntry): Promise<void> {
const { tail, track } = entry;
// Already the smaller of the subscriber's and the track's max age.
const maxAge = track.subscription.peek()?.maxAge ?? Time.Milli.zero;
const grace = maxAge > 0 ? maxAge : TAIL_GRACE_MS;

const complete = () => {
if (entry.streams !== undefined) return tail.streams >= entry.streams;
// Without SUBSCRIBE_END (older drafts) nothing says which groups are owed.
if (entry.end === undefined) return false;
// Without SUBSCRIBE_START the publisher served no group at all.
Expand Down
Loading
Loading