From c205d5dc5b3b7f60c7ca217f9dfbbc37a020de26 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 03:05:14 -0800 Subject: [PATCH 1/4] chore(quest): claim quest/m1/api-audit/resolve/follow-options.md Quest-Claim: 561726bd295c2cea From 134643a0cf388d948efba7cbb2e71ea51b905af9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 03:07:47 -0800 Subject: [PATCH 2/4] refactor(net)!: give origin follow its own options Co-Authored-By: GPT-6.1 Sol --- quest/m1/api-audit/resolve/README.md | 4 ---- quest/m1/api-audit/resolve/follow-options.md | 22 -------------------- 2 files changed, 26 deletions(-) delete mode 100644 quest/m1/api-audit/resolve/follow-options.md diff --git a/quest/m1/api-audit/resolve/README.md b/quest/m1/api-audit/resolve/README.md index 2006acb5bed..522db9cafbb 100644 --- a/quest/m1/api-audit/resolve/README.md +++ b/quest/m1/api-audit/resolve/README.md @@ -54,10 +54,6 @@ Public API: breaking task shape in moq-net; behavior changes in `@moq/net` and the bindings. The method and epoch renames broke every wrapper. Wire: none. -## Required - -- [JS follow takes FollowOptions](/quest/m1/api-audit/resolve/follow-options.md) - `follow` and `request` get their own JS options types - ## Related - [Origin fronts park](/quest/m2/origin-front-parks.md) - touches the same retry diff --git a/quest/m1/api-audit/resolve/follow-options.md b/quest/m1/api-audit/resolve/follow-options.md deleted file mode 100644 index 783f4ab0aa3..00000000000 --- a/quest/m1/api-audit/resolve/follow-options.md +++ /dev/null @@ -1,22 +0,0 @@ -# [S] JS follow takes FollowOptions - -## Goal - -In `@moq/net`, `Origin.Consumer.follow` (and the `Producer` and `Table` -passthroughs) takes its own `FollowOptions`, and `request` keeps -`RequestOptions`. Each options type documents only what its method does. -Every caller in `js/` compiles against the split, and the API report is -regenerated. - -## Plan - -Planned 2026-10-31 (`/quest-plan`, maintainer), a follow-up from -[JS follow returns a Following](https://github.com/moq-dev/moq/pull/7476), -which split the return types but kept one `RequestOptions` for both -methods. Keep the shared fields' names. Drop a field from `FollowOptions` -only if `follow` ignores it today, and say which in the PR. If the two -types still end up identical, a shared base with two aliases is fine; the -point is that each method's docs describe one behavior. - -Public API: breaking for TypeScript type annotations that name -`RequestOptions` for a `follow` call. Wire: none. From 9200410f7b69d825f19c3281f28929539fe16173 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 03:07:56 -0800 Subject: [PATCH 3/4] refactor(net)!: split follow options from request options Co-Authored-By: GPT-6.1 Sol --- js/net/MIGRATE.md | 5 +++++ js/net/api/index.api.md | 9 ++++++--- js/net/src/origin-request.test.ts | 12 ++++++++++-- js/net/src/origin.ts | 14 ++++++++++---- quest/m1/api-audit/resolve/README.md | 2 +- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index 8346353ad3b..80207875505 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `follow` takes `FollowOptions` ([#7724](https://github.com/moq-dev/moq/pull/7724)) + +Use `Origin.FollowOptions` for options passed to `Origin.Consumer.follow`, `Origin.Producer.follow`, or a borrowed `Origin.Reader` or `Origin.Table`. `request` still takes `Origin.RequestOptions`. +Both keep the optional `instance` pin, so existing option objects remain assignable; only the declared options type changes. No fields or runtime behavior change. + ### `Subscription.maxDelay` is `delay: Track.Delay` ([#7597](https://github.com/moq-dev/moq/pull/7597)) `Track.Subscription` carries a `delay: Track.Delay` (`{ min?: Milli | null; max?: Milli | null }`) in place of `maxDelay`, which is now refused at runtime. diff --git a/js/net/api/index.api.md b/js/net/api/index.api.md index b56597dcd5b..4f93af96a7e 100644 --- a/js/net/api/index.api.md +++ b/js/net/api/index.api.md @@ -388,10 +388,13 @@ export namespace Origin { announced(scope?: Path.Pattern, options?: Announce.Options): Announce.Consumer; get closed(): Signals.GetPromise; get discovery(): Signals.Getter; - follow(path: Path.Valid, options?: Origin.RequestOptions): Origin.Following; + follow(path: Path.Valid, options?: Origin.FollowOptions): Origin.Following; request(path: Path.Valid, options?: Origin.RequestOptions): Origin.Requesting; } export type Cost = bigint; + export interface FollowOptions { + instance?: Instance.Valid; + } export class Following { readonly active: Signals.Getter; close(): void; @@ -414,7 +417,7 @@ export namespace Origin { consume(): Origin.Consumer; createBroadcast(path: Path.Valid): Origin.Publication; get discovery(): Signals.Getter; - follow(path: Path.Valid, options?: Origin.RequestOptions): Origin.Following; + follow(path: Path.Valid, options?: Origin.FollowOptions): Origin.Following; request(path: Path.Valid, options?: Origin.RequestOptions): Origin.Requesting; requests(prefix: Path.Valid): Origin.Requests; scope(root: Path.Valid, patterns: Path.Patterns): Origin.Producer; @@ -435,7 +438,7 @@ export namespace Origin { announced(scope?: Path.Pattern, options?: Announce.Options): Announce.Consumer; readonly closed: Signals.GetPromise; readonly discovery: Signals.Getter; - follow(path: Path.Valid, options?: Origin.RequestOptions): Origin.Following; + follow(path: Path.Valid, options?: Origin.FollowOptions): Origin.Following; request(path: Path.Valid, options?: Origin.RequestOptions): Origin.Requesting; } export class Request { diff --git a/js/net/src/origin-request.test.ts b/js/net/src/origin-request.test.ts index 9f490a0622d..2b0c8e105aa 100644 --- a/js/net/src/origin-request.test.ts +++ b/js/net/src/origin-request.test.ts @@ -4,7 +4,14 @@ import { StreamCode, StreamError } from "./error"; import { Route } from "./hop"; import * as Instance from "./instance.ts"; import { hooks } from "./internal.ts"; -import { type Following, Producer, type Publication, type Request, type Requesting } from "./origin"; +import { + type Following, + type FollowOptions, + Producer, + type Publication, + type Request, + type Requesting, +} from "./origin"; import * as Path from "./path"; import { wireOf } from "./wire"; @@ -45,7 +52,8 @@ test("an instance-pinned follower refuses another instance", async () => { const firstPublication = origin.createBroadcast(path); const first = firstPublication.broadcast; firstPublication.announce({ instance }); - const request = origin.follow(path, { instance }); + const options: FollowOptions = { instance }; + const request = origin.follow(path, options); const secondPublication = origin.createBroadcast(path); const second = secondPublication.broadcast; secondPublication.announce({ instance: Instance.random() }); diff --git a/js/net/src/origin.ts b/js/net/src/origin.ts index cbabea6b1bd..ce31ad0afba 100644 --- a/js/net/src/origin.ts +++ b/js/net/src/origin.ts @@ -1220,7 +1220,7 @@ export interface Reader { request(path: Path.Valid, options?: RequestOptions): Requesting; /** Follow `path` across publisher instances; see {@link Consumer.follow}. */ - follow(path: Path.Valid, options?: RequestOptions): Following; + follow(path: Path.Valid, options?: FollowOptions): Following; /** The available broadcasts under `scope`, as a live stream; see {@link Consumer.announced}. */ announced(scope?: Path.Pattern, options?: Announce.Options): Announce.Consumer; @@ -1258,12 +1258,18 @@ export interface Table extends Reader { ): Requests; } -/** Options for resolving a broadcast path, by {@link Consumer.request} or {@link Consumer.follow}. */ +/** Options for resolving one publisher instance with {@link Consumer.request}. */ export interface RequestOptions { /** Refuse a different publisher instance, including an asynchronous answer or a retry. */ instance?: Instance.Valid; } +/** Options for following a broadcast path with {@link Consumer.follow}. */ +export interface FollowOptions { + /** Follow only this publisher instance, waiting for it to return across gaps. */ + instance?: Instance.Valid; +} + /** * The write side of an origin: create broadcasts by path and advertise them. * @@ -1662,7 +1668,7 @@ export class Producer implements Table { } /** Follow `path` across publisher instances; see {@link Consumer.follow}. */ - follow(path: Path.Valid, options?: RequestOptions): Following { + follow(path: Path.Valid, options?: FollowOptions): Following { return this.#reader.follow(path, options); } @@ -1954,7 +1960,7 @@ export class Consumer implements Reader { * its path until closed. Requires discovery: a session without it never serves a follower. * A selected handler's refusal or the origin closing ends it. */ - follow(path: Path.Valid, options: RequestOptions = {}): Following { + follow(path: Path.Valid, options: FollowOptions = {}): Following { const instance = options.instance; const absolute = this.#scope.path(path); Path.Pattern.literal(path); diff --git a/quest/m1/api-audit/resolve/README.md b/quest/m1/api-audit/resolve/README.md index 522db9cafbb..14d21088e4b 100644 --- a/quest/m1/api-audit/resolve/README.md +++ b/quest/m1/api-audit/resolve/README.md @@ -1,4 +1,4 @@ -# Two broadcast resolutions +# [M] Two broadcast resolutions ## Goal From 8b732029c6e345c6678cec97bc0fdd92b07a0455 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 03:12:16 -0800 Subject: [PATCH 4/4] refactor(net): keep the options split source-compatible Co-Authored-By: GPT-6.1 Sol --- js/net/MIGRATE.md | 5 ----- js/net/src/origin-request.test.ts | 12 ++---------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index 80207875505..8346353ad3b 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -4,11 +4,6 @@ How to update your code for each breaking change, newest first. ## Unreleased -### `follow` takes `FollowOptions` ([#7724](https://github.com/moq-dev/moq/pull/7724)) - -Use `Origin.FollowOptions` for options passed to `Origin.Consumer.follow`, `Origin.Producer.follow`, or a borrowed `Origin.Reader` or `Origin.Table`. `request` still takes `Origin.RequestOptions`. -Both keep the optional `instance` pin, so existing option objects remain assignable; only the declared options type changes. No fields or runtime behavior change. - ### `Subscription.maxDelay` is `delay: Track.Delay` ([#7597](https://github.com/moq-dev/moq/pull/7597)) `Track.Subscription` carries a `delay: Track.Delay` (`{ min?: Milli | null; max?: Milli | null }`) in place of `maxDelay`, which is now refused at runtime. diff --git a/js/net/src/origin-request.test.ts b/js/net/src/origin-request.test.ts index 2b0c8e105aa..9f490a0622d 100644 --- a/js/net/src/origin-request.test.ts +++ b/js/net/src/origin-request.test.ts @@ -4,14 +4,7 @@ import { StreamCode, StreamError } from "./error"; import { Route } from "./hop"; import * as Instance from "./instance.ts"; import { hooks } from "./internal.ts"; -import { - type Following, - type FollowOptions, - Producer, - type Publication, - type Request, - type Requesting, -} from "./origin"; +import { type Following, Producer, type Publication, type Request, type Requesting } from "./origin"; import * as Path from "./path"; import { wireOf } from "./wire"; @@ -52,8 +45,7 @@ test("an instance-pinned follower refuses another instance", async () => { const firstPublication = origin.createBroadcast(path); const first = firstPublication.broadcast; firstPublication.announce({ instance }); - const options: FollowOptions = { instance }; - const request = origin.follow(path, options); + const request = origin.follow(path, { instance }); const secondPublication = origin.createBroadcast(path); const second = secondPublication.broadcast; secondPublication.announce({ instance: Instance.random() });