@synapse-sdk/client 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ import { type Age, type ClockInfo, type ErrorBody, type LocalTime, type ProjectSnapshot, type ServiceContract, type TimeOffsets, type TopicContract } from "@synapse-sdk/core";
2
+ import { Node } from "./node.ts";
3
+ import { StoreHandleImpl } from "./store.ts";
4
+ import { type Warn } from "./topics.ts";
5
+ import type { CallEvent, CallOptions, Catalog, ConnectOptions, ConnectTarget, ConnectionState, Envelope, Info, Metrics, Notice, Publisher, Registration, Run, ServeHandler, ServeOptions, SceneHandle, StoreHandle, StoreOptions, SubscribeOptions, Subscription, View, ViewOptions } from "./types.ts";
6
+ /** How long the first `connect()` keeps trying a retryable failure before `failed` (SDK §2: the first connect is bounded). */
7
+ export declare const FIRST_CONNECT_GIVE_UP_MS = 30000;
8
+ export declare class Synapse implements AsyncDisposable {
9
+ #private;
10
+ readonly opts: ConnectOptions;
11
+ warn: Warn;
12
+ constructor(opts: ConnectOptions);
13
+ /** `await Synapse.connect(opts)`: a node with a session, or the reason there is none. */
14
+ static connect(opts: ConnectOptions): Promise<Synapse>;
15
+ /** Connects (once; later calls return the same promise). Resolves once a session is established. */
16
+ connect(): Promise<this>;
17
+ get node(): Node;
18
+ get status(): ConnectionState;
19
+ get connected(): boolean;
20
+ /** Called on every change of the connection state; returns the unsubscribe. */
21
+ watchStatus(cb: (s: ConnectionState) => void): () => void;
22
+ /** Resolves on the next connected session (at once if connected); rejects once the node ended. */
23
+ whenConnected(): Promise<void>;
24
+ /** Declares a new target (broker, identity); resolves once connected to it (or rejects with why not). `null` disconnects for good. */
25
+ setTarget(target: ConnectTarget | null): Promise<void>;
26
+ /** Tells the runtime we are leaving, ends every handle, and resolves once the node has ended. */
27
+ /** During the first `connect()` it ends that attempt: the node ends `closed` and `connect()` rejects `not_connected`. */
28
+ close(): Promise<void>;
29
+ [Symbol.asyncDispose](): Promise<void>;
30
+ /** Resolves once the node has ended (closed, failed, or the policy gave up). */
31
+ get ended(): Promise<void>;
32
+ info(): Info;
33
+ metrics(): Metrics;
34
+ /** Broker notices addressed to this node (an error body on `res` without a call). */
35
+ onNotice(cb: (n: ErrorBody) => void): () => void;
36
+ /** `promoted`/`demoted` notices (WP §3.5). */
37
+ onProjectNotice(cb: (n: Notice) => void): () => void;
38
+ nowUs(): number;
39
+ clock(): ClockInfo;
40
+ timeOffsets(): TimeOffsets;
41
+ /** A publisher stamp in our clock (WP §7.4), when the offsets allow it. */
42
+ toLocalUs(tUs: number, dom: string): LocalTime | undefined;
43
+ /** How old a message is now, in our clock. */
44
+ ageUs(env: {
45
+ tUs: number;
46
+ dom: string;
47
+ }): Age | undefined;
48
+ /** A publisher of `topic`; resolves once the runtime admitted it on the current session (a refusal is asked again, paced, warned). */
49
+ publisher<TIn, TOut>(topic: TopicContract<TIn, TOut>): Promise<Publisher<TIn>>;
50
+ publisher<TIn = unknown>(topic: string): Promise<Publisher<TIn>>;
51
+ /** A subscription to `topic` (every publisher the catalog lists but ourselves); its follow task starts now. */
52
+ subscribe<TIn, TOut>(topic: TopicContract<TIn, TOut>, opts?: SubscribeOptions): Subscription<TOut>;
53
+ subscribe<TOut = unknown>(topic: string, opts?: SubscribeOptions): Subscription<TOut>;
54
+ /** The topic's latest value as a cell (SDK §4.3); subscribed while someone listens. */
55
+ view<TIn, TOut, S = TOut>(topic: TopicContract<TIn, TOut>, opts?: ViewOptions<TOut, S>): View<S>;
56
+ view<TOut = unknown, S = TOut>(topic: string, opts?: ViewOptions<TOut, S>): View<S>;
57
+ /** Starts a call on the current session (SDK §5.2): `await` it for the result, or read `updates()` first. `not_connected` between sessions. */
58
+ call<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): Run<U, R>;
59
+ call<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): Run<U, R>;
60
+ /** The call's updates and result as one ordered stream; `break` cancels. */
61
+ callStream<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): AsyncGenerator<CallEvent<U, R>, void, undefined>;
62
+ callStream<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): AsyncGenerator<CallEvent<U, R>, void, undefined>;
63
+ /** Registers `handler` as the provider of `svc`; resolves once the runtime lists us on the current session. One provider per service path per node. */
64
+ serve<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, handler: ServeHandler<TInput, U, R>, opts?: ServeOptions): Promise<Registration>;
65
+ serve<I = unknown, U = unknown, R = unknown>(svc: string, handler: ServeHandler<I, U, R>, opts?: ServeOptions): Promise<Registration>;
66
+ /** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
67
+ store(name: string, opts?: StoreOptions): Promise<StoreHandle>;
68
+ /** The runtime's `config` namespace (operators write it). */
69
+ config(opts?: StoreOptions): Promise<StoreHandle>;
70
+ /** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
71
+ scene(): Promise<SceneHandle>;
72
+ /** The runtime's catalogs as the current session last read them; empty between sessions. */
73
+ get catalog(): Catalog;
74
+ /** Called when either catalog changes on the current session. */
75
+ watchCatalog(cb: (c: Catalog) => void): () => void;
76
+ /** The project state the current session last read (WP §3.5); `undefined` between sessions or before the first snapshot. */
77
+ project(): ProjectSnapshot | undefined;
78
+ watchProject(cb: (p: ProjectSnapshot | undefined) => void): () => void;
79
+ /** `synapse/project/activate {id}`: activates `id` (or joins it when active); promoted to operator. */
80
+ activateProject(id: string): Promise<ProjectSnapshot>;
81
+ /** `synapse/project/deactivate {}`: every operator is demoted. */
82
+ deactivateProject(): Promise<ProjectSnapshot>;
83
+ /** `synapse/project/join {as: "operator"}`: promoted on the active project, or `project_inactive`. */
84
+ joinProject(as?: "operator"): Promise<ProjectSnapshot>;
85
+ /** `synapse/project/leave {}`: drops our standing (every session of our `user_id`). */
86
+ leaveProject(): Promise<ProjectSnapshot>;
87
+ getProject(): Promise<ProjectSnapshot>;
88
+ }
89
+ export type { Envelope, StoreHandleImpl };
@@ -0,0 +1,256 @@
1
+ // `Synapse` (SDK §2–§9): the public face of one node. It owns nothing the modules do not: the node's
2
+ // loop (`node.ts`) drives sessions; publishers, registrations, subscriptions and namespaces are their
3
+ // own tasks, told about sessions by the loop; every fact about the world is read from the current
4
+ // `Session` (the runtime's statement) at the moment it is asked for, never copied here.
5
+ import { ClockSync, SynapseError, ageOf, defaultDomId, fecSymbolSizeFor, isServiceContract, nowUs, resolveTopic, serviceName, toLocalUs, } from "@synapse-sdk/core";
6
+ import { callStream, startOn } from "./calls.js";
7
+ import { Node, resolveTarget, startNode } from "./node.js";
8
+ import { ServiceRegistration } from "./serve.js";
9
+ import { openScene } from "./scene.js";
10
+ import { Namespace, StoreHandleImpl, openStore } from "./store.js";
11
+ import { PublisherHandle, TopicPublisher, TopicSubscription, wasmFor } from "./topics.js";
12
+ import { TopicView } from "./view.js";
13
+ /** How long the first `connect()` keeps trying a retryable failure before `failed` (SDK §2: the first connect is bounded). */
14
+ export const FIRST_CONNECT_GIVE_UP_MS = 30_000;
15
+ /** The descriptor a publisher of `contract` on node `n` declares (as `TopicPublisher` builds it: a `fec` symbol size of 0 is the node's). */
16
+ function wantedDesc(contract, n) {
17
+ const d = contract.desc(n.dom);
18
+ if (d.delivery === "datagram" && d.fec && d.fec.symbol_size === 0)
19
+ d.fec = { ...d.fec, symbol_size: fecSymbolSizeFor(n.knobs.maxDatagram) };
20
+ return d;
21
+ }
22
+ /** The parts of a descriptor two publishers of one topic on one node must agree on, as text (also for the refusal's message). */
23
+ function descKey(d) {
24
+ return JSON.stringify({ delivery: d.delivery, content_type: d.content_type, schema_hash: d.schema_hash ?? null, fec: d.fec ?? null, layout: d.layout ?? null });
25
+ }
26
+ export class Synapse {
27
+ opts;
28
+ #node;
29
+ #connecting;
30
+ #starting;
31
+ #publishers = new Map();
32
+ #stores = new Map();
33
+ warn;
34
+ constructor(opts) {
35
+ this.opts = opts;
36
+ this.warn = msg => console.warn(`[synapse] ${msg}`);
37
+ }
38
+ /** `await Synapse.connect(opts)`: a node with a session, or the reason there is none. */
39
+ static connect(opts) { return new Synapse(opts).connect(); }
40
+ /** Connects (once; later calls return the same promise). Resolves once a session is established. */
41
+ connect() {
42
+ this.#connecting ??= (async () => {
43
+ const started = startNode(this.opts, FIRST_CONNECT_GIVE_UP_MS);
44
+ // The node as soon as its loop runs (or none: the options were refused), for a `close()` during the first dial.
45
+ this.#starting = started.then(s => s.node, () => undefined);
46
+ const { node, connected } = await started;
47
+ await connected;
48
+ this.#node = node;
49
+ return this;
50
+ })();
51
+ return this.#connecting;
52
+ }
53
+ get node() {
54
+ if (!this.#node)
55
+ throw new SynapseError("not_connected", "connect() has not completed");
56
+ return this.#node;
57
+ }
58
+ #session() { return this.node.currentSession(); }
59
+ // ------------------------------------------------------------------------------ lifecycle
60
+ get status() { return this.#node?.state() ?? { status: "connecting", attempt: 1 }; }
61
+ get connected() { return this.#node?.isConnected ?? false; }
62
+ /** Called on every change of the connection state; returns the unsubscribe. */
63
+ watchStatus(cb) { return this.node.link.subscribe(() => cb(this.node.state())); }
64
+ /** Resolves on the next connected session (at once if connected); rejects once the node ended. */
65
+ whenConnected() {
66
+ const n = this.node;
67
+ if (n.isConnected)
68
+ return Promise.resolve();
69
+ if (n.isEnded)
70
+ return Promise.reject(new SynapseError("not_connected", "node closed"));
71
+ return new Promise((resolve, reject) => {
72
+ const un = n.link.subscribe(l => { if (l.status === "connected") {
73
+ un();
74
+ resolve();
75
+ } });
76
+ // The node's end settles it (a resolved promise ignores the later reject).
77
+ void n.ended.promise.then(() => { un(); reject(new SynapseError("not_connected", "node closed")); });
78
+ });
79
+ }
80
+ /** Declares a new target (broker, identity); resolves once connected to it (or rejects with why not). `null` disconnects for good. */
81
+ setTarget(target) { return this.node.setTarget(target ? resolveTarget(this.node, target) : undefined); }
82
+ /** Tells the runtime we are leaving, ends every handle, and resolves once the node has ended. */
83
+ /** During the first `connect()` it ends that attempt: the node ends `closed` and `connect()` rejects `not_connected`. */
84
+ async close() {
85
+ const n = this.#node ?? (await this.#starting);
86
+ if (n)
87
+ await n.close();
88
+ }
89
+ async [Symbol.asyncDispose]() { await this.close(); }
90
+ /** Resolves once the node has ended (closed, failed, or the policy gave up). */
91
+ get ended() { return this.node.ended.promise; }
92
+ info() {
93
+ const n = this.node;
94
+ const id = n.target.get().identity;
95
+ const s = n.currentSession();
96
+ return { name: id.name ?? "", type: id.type, userId: id.user_id, prefix: s?.prefix ?? "", role: s?.role ?? "observer", dom: n.dom, brokerDom: s?.hello.dom ?? "", brokerVersion: s?.hello.broker_version ?? "", robot: s?.hello.robot ?? "", url: s?.url ?? n.target.get().dial?.url ?? "" };
97
+ }
98
+ metrics() { const n = this.node; return { sessions: n.epochs, reconnects: n.reconnects, callsSent: n.callsSent, callsAnswered: n.callsAnswered }; }
99
+ /** Broker notices addressed to this node (an error body on `res` without a call). */
100
+ onNotice(cb) { this.node.notices.add(cb); return () => { this.node.notices.delete(cb); }; }
101
+ /** `promoted`/`demoted` notices (WP §3.5). */
102
+ onProjectNotice(cb) { this.node.projectNotices.add(cb); return () => { this.node.projectNotices.delete(cb); }; }
103
+ // ---------------------------------------------------------------------------------- clock
104
+ nowUs() { return nowUs(); }
105
+ clock() { const s = this.#session(); return s ? s.clock.get() : new ClockSync(this.node.dom.id, "", 0).info(); }
106
+ timeOffsets() { return this.#session()?.timeOffsets.get() ?? {}; }
107
+ /** A publisher stamp in our clock (WP §7.4), when the offsets allow it. */
108
+ toLocalUs(tUs, dom) { const s = this.#session(); return s ? toLocalUs(s.clock.get(), s.timeOffsets.get(), tUs, dom) : undefined; }
109
+ /** How old a message is now, in our clock. */
110
+ ageUs(env) { const s = this.#session(); return s ? ageOf(s.clock.get(), s.timeOffsets.get(), env.tUs, env.dom, nowUs()) : undefined; }
111
+ async publisher(topic) {
112
+ const contract = resolveTopic(topic);
113
+ const n = this.node;
114
+ const name = contract.name;
115
+ let p;
116
+ let held;
117
+ for (;;) {
118
+ const entry = this.#publishers.get(name);
119
+ if (!entry) {
120
+ // Looked up and inserted in one step (no await between): a concurrent `publisher()` for the
121
+ // same name finds this entry and waits for it instead of making a second one.
122
+ const made = wasmFor(contract).then(wasm => new TopicPublisher(n, contract, n.knobs.maxDatagram, wasm, this.warn));
123
+ this.#publishers.set(name, made);
124
+ try {
125
+ p = (await made);
126
+ }
127
+ catch (e) {
128
+ if (this.#publishers.get(name) === made)
129
+ this.#publishers.delete(name);
130
+ throw e;
131
+ }
132
+ held = made;
133
+ break;
134
+ }
135
+ const live = await entry.catch(() => undefined);
136
+ // Gone meanwhile (its making failed, or its last handle closed): look again.
137
+ if (!live || this.#publishers.get(name) !== entry)
138
+ continue;
139
+ // A second one with another descriptor is not this one, and its intent never reached the runtime.
140
+ const have = descKey(live.desc), want = descKey(wantedDesc(contract, n));
141
+ if (have !== want)
142
+ throw new SynapseError("invalid_input", `topic ${name}: a publisher with a different descriptor exists on this node (live: ${have}; asked: ${want})`);
143
+ p = live;
144
+ held = entry;
145
+ break;
146
+ }
147
+ const h = new PublisherHandle(p, () => { if (this.#publishers.get(name) === held)
148
+ this.#publishers.delete(name); });
149
+ try {
150
+ await h.ready();
151
+ }
152
+ catch (e) {
153
+ h.close();
154
+ throw e;
155
+ }
156
+ return h;
157
+ }
158
+ subscribe(topic, opts = {}) {
159
+ const contract = resolveTopic(topic);
160
+ return new TopicSubscription(this.node, contract, opts, this.warn);
161
+ }
162
+ view(topic, opts = {}) {
163
+ const contract = resolveTopic(topic);
164
+ const { staleTime: _s, select: _sel, ...subOpts } = opts;
165
+ return new TopicView(this.node, () => new TopicSubscription(this.node, contract, subOpts, this.warn), opts, () => { });
166
+ }
167
+ call(svc, args = {}, opts = {}) {
168
+ const s = this.#session();
169
+ if (!s)
170
+ throw new SynapseError("not_connected", "no session: the call was not sent");
171
+ const contract = isServiceContract(svc) ? svc : undefined;
172
+ return startOn(this.node, s, serviceName(svc), args, opts, contract);
173
+ }
174
+ callStream(svc, args = {}, opts = {}) {
175
+ return callStream(this.call(svc, args, opts));
176
+ }
177
+ async serve(svc, handler, opts = {}) {
178
+ const n = this.node;
179
+ const name = serviceName(svc);
180
+ const contract = isServiceContract(svc) ? svc : undefined;
181
+ const card = contract ? contract.card() : { name, streaming: true, cancelable: true, min_role: opts.minRole ?? "agent", ...(opts.description ? { description: opts.description } : {}) };
182
+ if (opts.minRole)
183
+ card.min_role = opts.minRole;
184
+ if (opts.description)
185
+ card.description = opts.description;
186
+ if (this.#registrations.has(name))
187
+ throw new SynapseError("already_registered", `${name}: this node already provides it (one provider per service path)`);
188
+ const reg = new ServiceRegistration(n, name, card, contract, handler, this.warn);
189
+ this.#registrations.set(name, reg);
190
+ void reg.handle.exited.promise.then(() => { if (this.#registrations.get(name) === reg)
191
+ this.#registrations.delete(name); });
192
+ try {
193
+ await reg.ready();
194
+ }
195
+ catch (e) {
196
+ reg.unregister();
197
+ throw e;
198
+ }
199
+ return reg;
200
+ }
201
+ #registrations = new Map();
202
+ // ---------------------------------------------------------------------------------- store
203
+ /** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
204
+ store(name, opts = {}) { return openStore(this.node, this.#stores, name, opts, this.warn); }
205
+ /** The runtime's `config` namespace (operators write it). */
206
+ config(opts = {}) { return this.store("config", opts); }
207
+ // ---------------------------------------------------------------------------------- scene
208
+ #scenes = { current: undefined };
209
+ /** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
210
+ scene() { return openScene(this.node, this.#scenes, this.warn); }
211
+ // -------------------------------------------------------------------------------- catalog
212
+ /** The runtime's catalogs as the current session last read them; empty between sessions. */
213
+ get catalog() {
214
+ const s = this.#session();
215
+ const topics = s?.topics.get().topics ?? {};
216
+ const services = s?.services.get().services ?? {};
217
+ return { topics, services, describe: name => services[name]?.card };
218
+ }
219
+ /** Called when either catalog changes on the current session. */
220
+ watchCatalog(cb) {
221
+ let un = [];
222
+ const bind = () => {
223
+ for (const u of un)
224
+ u();
225
+ un = [];
226
+ const s = this.#session();
227
+ if (!s)
228
+ return;
229
+ un.push(s.topics.subscribe(() => cb(this.catalog)), s.services.subscribe(() => cb(this.catalog)));
230
+ };
231
+ bind();
232
+ const unLink = this.node.link.subscribe(() => { bind(); cb(this.catalog); });
233
+ return () => { unLink(); for (const u of un)
234
+ u(); };
235
+ }
236
+ // -------------------------------------------------------------------------------- project
237
+ /** The project state the current session last read (WP §3.5); `undefined` between sessions or before the first snapshot. */
238
+ project() { return this.#session()?.project.get(); }
239
+ watchProject(cb) {
240
+ let un;
241
+ const bind = () => { un?.(); un = this.#session()?.project.subscribe(p => cb(p)); };
242
+ bind();
243
+ const unLink = this.node.link.subscribe(() => { bind(); cb(this.project()); });
244
+ return () => { unLink(); un?.(); };
245
+ }
246
+ #projectCall(op, args) { return this.call(`synapse/project/${op}`, args).result; }
247
+ /** `synapse/project/activate {id}`: activates `id` (or joins it when active); promoted to operator. */
248
+ activateProject(id) { return this.#projectCall("activate", { id }); }
249
+ /** `synapse/project/deactivate {}`: every operator is demoted. */
250
+ deactivateProject() { return this.#projectCall("deactivate", {}); }
251
+ /** `synapse/project/join {as: "operator"}`: promoted on the active project, or `project_inactive`. */
252
+ joinProject(as = "operator") { return this.#projectCall("join", { as }); }
253
+ /** `synapse/project/leave {}`: drops our standing (every session of our `user_id`). */
254
+ leaveProject() { return this.#projectCall("leave", {}); }
255
+ getProject() { return this.#projectCall("get", {}); }
256
+ }
@@ -0,0 +1,136 @@
1
+ import * as Moq from "@moq/net";
2
+ import { type FecParams, type FecWasm, type MediaFrame, type TopicContract, type TrackDesc, type Tlv } from "@synapse-sdk/core";
3
+ import { Handle, type Event as HandleEvent, type Intent } from "./channel.ts";
4
+ import { type FecPubStats } from "./fec.ts";
5
+ import type { Node } from "./node.ts";
6
+ import { type Session } from "./session.ts";
7
+ import type { Envelope, PublishOptions, Publisher, PublisherStats, SubStats, SubscribeOptions, Subscription, Verdict } from "./types.ts";
8
+ export type Warn = (msg: string) => void;
9
+ /** The publisher shared by every `publisher(topic)` handle of one topic on this node. */
10
+ export declare class TopicPublisher<TIn, TOut> implements Intent {
11
+ #private;
12
+ readonly node: Node;
13
+ readonly contract: TopicContract<TIn, TOut>;
14
+ private readonly wasm;
15
+ readonly warn: Warn;
16
+ readonly name: string;
17
+ readonly registerSvc = "synapse/topic/register";
18
+ readonly unregisterSvc = "synapse/topic/unregister";
19
+ readonly desc: TrackDesc;
20
+ readonly fecParams: FecParams | undefined;
21
+ readonly fecStats: FecPubStats | undefined;
22
+ published: number;
23
+ droppedDisconnected: number;
24
+ handles: number;
25
+ readonly handle: Handle;
26
+ constructor(node: Node, contract: TopicContract<TIn, TOut>, maxDatagram: number, wasm: FecWasm | undefined, warn: Warn);
27
+ get verdict(): Verdict;
28
+ get isBound(): boolean;
29
+ registerArgs(bind: string): unknown;
30
+ unregisterArgs(bind: string): unknown;
31
+ /** Admitted: the broadcast is created, the descriptor written, the data track made, then announced (WP §7.1). */
32
+ install(s: Session, bind: string, _mailbox: (e: HandleEvent) => void): void;
33
+ retract(): void;
34
+ setVerdict(v: Verdict): void;
35
+ /**
36
+ * Publishes one frame (SDK §13: synchronous, never awaits the network). While nothing is installed
37
+ * (between sessions, a refused register being asked again) the frame is dropped and counted. A
38
+ * `stream` frame over moq's cap, a plain datagram over the session's datagram size, or a `fec`
39
+ * frame the codec refuses is refused (`invalid_input`) without taking a `seq`.
40
+ */
41
+ publishFrame(type: number, flags: number, tUs: number, tlvs: Tlv[], body: Uint8Array): void;
42
+ /** The typed form: validates against the contract's schema, encodes (msgpack, or the bytes of a raw topic). */
43
+ publish(value: TIn, opts?: PublishOptions): void;
44
+ stats(): PublisherStats;
45
+ }
46
+ /** The application's handle: the last one closed ends the publisher's task (`unregister`). */
47
+ export declare class PublisherHandle<TIn, TOut> implements Publisher<TIn> {
48
+ #private;
49
+ private readonly p;
50
+ private readonly onLast;
51
+ constructor(p: TopicPublisher<TIn, TOut>, onLast: () => void);
52
+ get topic(): string;
53
+ get verdict(): Verdict;
54
+ ready(): Promise<void>;
55
+ publish(value: TIn, opts?: PublishOptions): void;
56
+ /** Any frame type and TLVs (`MEDIA` with `STAGES`, WP §4); raw topics only. */
57
+ publishFrame(type: number, flags: number, tUs: number, tlvs: Tlv[], body: Uint8Array): void;
58
+ /** `tUs` 0 = capture now (as the Rust client stamps it). */
59
+ publishMedia(f: MediaFrame): void;
60
+ stats(): PublisherStats;
61
+ close(): void;
62
+ [Symbol.asyncDispose](): Promise<void>;
63
+ }
64
+ /** Resolves the codec a `fec` topic needs (once per process). */
65
+ export declare function wasmFor(contract: TopicContract<unknown, unknown>): Promise<FecWasm | undefined>;
66
+ /** What a source gives when its track ended. */
67
+ declare const END: unique symbol;
68
+ /**
69
+ * One plain publisher the read takes from (Rust `Source`). A stream source is read synchronously
70
+ * by `next()` (`tryRecvGroup` / `tryReadFrame`, as Rust's poll); when nothing is buffered it holds
71
+ * exactly one pending read (`recvGroup` / `readFrame`) as the wake, whose result is the first item
72
+ * of the next drain. A datagram source has one reader (one `recvDatagram` at a time) filling
73
+ * `held`: latest mode keeps one (the newest `seq`), every-message mode at most `DATAGRAM_RING`.
74
+ */
75
+ interface Source {
76
+ path: string;
77
+ prefix: string;
78
+ publisher: string;
79
+ dom: string;
80
+ /** The request for the publisher's broadcast: closed with the source. */
81
+ req: Moq.Origin.Requesting;
82
+ sub: Moq.Track.Subscriber;
83
+ /** Read as a stream (groups of frames) or as datagrams: the catalog's delivery for the topic. */
84
+ stream: boolean;
85
+ /** A stream source: the group being read. */
86
+ group: Moq.Group.Consumer | undefined;
87
+ /** A stream source: the pending read that wakes the read when nothing is buffered. */
88
+ wait: Promise<void> | undefined;
89
+ /** A stream source: what that pending read gave (a frame, or the track's end), taken first. */
90
+ ready: Uint8Array | typeof END | undefined;
91
+ /** A datagram source: the datagrams its reader took and the read has not. */
92
+ held: Uint8Array[];
93
+ /** A datagram source in latest mode: the `seq` of the one held. */
94
+ heldSeq: number | undefined;
95
+ /** A datagram source: its reader saw the track end; the end is given once `held` is drained. */
96
+ done: boolean;
97
+ /** The `seq` last returned (latest mode): not newer is superseded. */
98
+ last: number | undefined;
99
+ ended: boolean;
100
+ }
101
+ export declare class TopicSubscription<T> implements Subscription<T> {
102
+ #private;
103
+ readonly node: Node;
104
+ readonly contract: TopicContract<unknown, T>;
105
+ readonly opts: SubscribeOptions;
106
+ readonly warn: Warn;
107
+ readonly topic: string;
108
+ readonly stats: SubStats;
109
+ readonly latest: boolean;
110
+ constructor(node: Node, contract: TopicContract<unknown, T>, opts: SubscribeOptions, warn: Warn);
111
+ get signal(): AbortSignal;
112
+ get ownDom(): string;
113
+ /** The follow task hands a plain publisher here (a datagram source's reader starts); the waiting `next()` is woken. */
114
+ addSource(src: Source): void;
115
+ /** The publisher leaves the read: its subscription and request are closed, a payload it gave is never returned. */
116
+ removeSource(src: Source): void;
117
+ /** A `fec` follower's frame: latest mode replaces the publisher's pending one. */
118
+ pushFec(env: Envelope<T>): void;
119
+ dropFecOf(publisher: string): void;
120
+ /** A new session (or none): the `fec` frames and a batch held by `throttleMs` are the previous session's and go. */
121
+ clearFec(): void;
122
+ /** The decoding step: header facts plus the body through the contract, or `mismatch` and dropped. */
123
+ envelope(payload: Uint8Array, publisher: string, dom: string): Envelope<T> | undefined;
124
+ /**
125
+ * The next batch, never empty; `undefined` once the subscription or the node ended. Drains what
126
+ * every source has ready, synchronously (at most `DRAIN_PER_READ` per source), and the `fec`
127
+ * frames; with nothing to return it waits for a source's pending read or reader, a `fec` frame,
128
+ * a change of the set or the end of a `throttleMs` interval, and drains again.
129
+ */
130
+ next(): Promise<Envelope<T>[] | undefined>;
131
+ [Symbol.asyncIterator](): AsyncGenerator<Envelope<T>[], void, undefined>;
132
+ /** Ends the subscription: the follow task stops, every track is closed, a pending `next()` returns `undefined`. */
133
+ unsubscribe(): void;
134
+ [Symbol.asyncDispose](): Promise<void>;
135
+ }
136
+ export {};