@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.
- package/README.md +1 -111
- package/dist/calls.d.ts +37 -0
- package/dist/calls.js +192 -0
- package/dist/channel.d.ts +84 -0
- package/dist/channel.js +215 -0
- package/dist/fec.d.ts +85 -0
- package/dist/fec.js +281 -0
- package/dist/index.d.ts +10 -74
- package/dist/index.js +10 -15
- package/dist/moq_log.d.ts +23 -0
- package/dist/moq_log.js +176 -0
- package/dist/node.d.ts +145 -0
- package/dist/node.js +407 -0
- package/dist/scene.d.ts +299 -0
- package/dist/scene.js +1531 -0
- package/dist/scene_frames.d.ts +51 -0
- package/dist/scene_frames.js +215 -0
- package/dist/serve.d.ts +46 -0
- package/dist/serve.js +281 -0
- package/dist/session.d.ts +189 -0
- package/dist/session.js +745 -0
- package/dist/signals.d.ts +10 -0
- package/dist/signals.js +63 -0
- package/dist/store.d.ts +145 -0
- package/dist/store.js +898 -0
- package/dist/synapse.d.ts +89 -0
- package/dist/synapse.js +256 -0
- package/dist/topics.d.ts +136 -0
- package/dist/topics.js +938 -0
- package/dist/types.d.ts +679 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +67 -0
- package/dist/util.js +227 -0
- package/dist/view.d.ts +19 -0
- package/dist/view.js +145 -0
- package/package.json +18 -39
- package/dist/node-CSSIXG4k.js +0 -1501
- package/dist/node-DbD7G0Nx.d.ts +0 -466
- package/dist/react.d.ts +0 -63
- package/dist/react.js +0 -141
package/dist/moq_log.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @moq/net has no logger hook: its background tasks write straight to `console.warn`. Some of those
|
|
2
|
+
// lines describe the *normal* end of a track we published, not a failure:
|
|
3
|
+
//
|
|
4
|
+
// publish error: broadcast=client/<name>/<sid>/svc/<name> track=card error=RESET_STREAM: 1
|
|
5
|
+
//
|
|
6
|
+
// The broker reads a one-shot track (`card`, `desc`, the retained `data` group, the `req` log) and
|
|
7
|
+
// drops its subscription with a QUIC RESET_STREAM carrying `StreamCode.Cancel` (1: "the sender is
|
|
8
|
+
// done with this stream, not failing; a routine unsubscribe"), which @moq/net's publisher still
|
|
9
|
+
// reports as an error. Likewise, our own `close()` tears every track and the discovery stream down
|
|
10
|
+
// before @moq/net's session-closed promise settles, so each track reports "session error:
|
|
11
|
+
// connection error: closed" and `forwardAnnounced` reports "broadcast discovery failed".
|
|
12
|
+
//
|
|
13
|
+
// This module wraps `console.warn` once per process and demotes exactly those lines to @moq/net
|
|
14
|
+
// debug chatter: a `publish error` on a path *this SDK announced* whose reason is the Cancel code,
|
|
15
|
+
// a `subscribe error` on a path *this SDK requested* whose reason is the relay's "source gone" reset
|
|
16
|
+
// (the publisher we followed left; the SDK reports that itself), or any track/discovery failure of a
|
|
17
|
+
// session *we* are closing. Everything else passes through untouched — a reset with another code
|
|
18
|
+
// (NotFound, GroupTooLarge, Unauthorized, ...), a line about a path that is not ours, and every track
|
|
19
|
+
// that dies because the *broker* dropped us (that failure is real and the node also reports it
|
|
20
|
+
// through `watchStatus`/`connection_lost`).
|
|
21
|
+
//
|
|
22
|
+
// @moq/net also narrates every announce, subscribe and connect through `console.debug`, which Bun
|
|
23
|
+
// and the browser print by default. The same wrapper covers `console.debug`: lines in @moq/net's
|
|
24
|
+
// own formats (`announced: …`, `subscribe ok: …`, `<url> connected via WebTransport`, …) are dropped
|
|
25
|
+
// unless moq debugging is on (`setMoqDebug(true)`, or `SYNAPSE_MOQ_DEBUG=1` in Bun/Node); every
|
|
26
|
+
// other `console.debug` call passes through. So an application never has to silence `console.debug`
|
|
27
|
+
// itself (D 2026-09-27).
|
|
28
|
+
import { ADMISSION_REFUSED_CLOSE_CODE } from "@synapse-sdk/core";
|
|
29
|
+
let debugOn;
|
|
30
|
+
/** Show @moq/net's debug narration (and the demoted routine-end lines) on `console.debug`; default off, or `SYNAPSE_MOQ_DEBUG=1`. */
|
|
31
|
+
export function setMoqDebug(on) { debugOn = on; }
|
|
32
|
+
function moqDebugEnabled() {
|
|
33
|
+
if (debugOn !== undefined)
|
|
34
|
+
return debugOn;
|
|
35
|
+
const env = globalThis.process?.env?.SYNAPSE_MOQ_DEBUG;
|
|
36
|
+
return env !== undefined && env !== "" && env !== "0" && env.toLowerCase() !== "false";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The first argument of every `console.debug` @moq/net makes (0.4.0): `<subject>[ <verb>]: …` with a
|
|
40
|
+
* subject from its fixed vocabulary (`announced: broadcast=…`, `subscribe ok: id=…`, `track info: …`,
|
|
41
|
+
* `datagram send stopped: sub=…`, `dropping oversize datagram: …`), or a redacted URL followed by a
|
|
42
|
+
* fixed phrase.
|
|
43
|
+
*/
|
|
44
|
+
const MOQ_DEBUG_PREFIX = /^(announced?|subscribe(_namespace)?|publish(_namespace|_blocked)?|track|fetch|fill|datagram (send|receive) stopped|dropping( [a-z]+)*|adapter)( [a-z_]+)*: |^(session stream closed|message write)$/;
|
|
45
|
+
const MOQ_DEBUG_URL_PHRASE = /^(connected via |negotiated ALPN:|performing an insecure fingerprint fetch)/;
|
|
46
|
+
/** Exported for tests: whether a `console.debug` call is @moq/net's narration. */
|
|
47
|
+
export function isMoqDebug(args) {
|
|
48
|
+
const [first, second] = args;
|
|
49
|
+
if (typeof first !== "string")
|
|
50
|
+
return false;
|
|
51
|
+
if (MOQ_DEBUG_PREFIX.test(first))
|
|
52
|
+
return true;
|
|
53
|
+
return /^(https?|wss?):\/\//.test(first) && typeof second === "string" && MOQ_DEBUG_URL_PHRASE.test(second);
|
|
54
|
+
}
|
|
55
|
+
const scopes = new Set();
|
|
56
|
+
/**
|
|
57
|
+
* A released scope stays known for this long: @moq/net logs a track's end from its own tasks, after
|
|
58
|
+
* our close has returned. Expiry is checked at lookup, not by a timer, so nothing keeps the event
|
|
59
|
+
* loop alive.
|
|
60
|
+
*/
|
|
61
|
+
const LINGER_MS = 10_000;
|
|
62
|
+
const PUBLISH_ERROR = /^publish error: broadcast=(\S+) track=\S+ error=(.*)$/s;
|
|
63
|
+
const SUBSCRIBE_ERROR = /^subscribe error: id=\d+ broadcast=(\S+) track=\S+ error=(.*)$/s;
|
|
64
|
+
const DISCOVERY_FAILED = "broadcast discovery failed; broadcasts resolve on request only.";
|
|
65
|
+
/** `StreamCode.Cancel` (1) as the three runtimes spell a peer's reset: the napi polyfill, @moq/net's own `Error.Stream`, Chromium's `WebTransportError`. */
|
|
66
|
+
const CANCEL_RESET = /^RESET_STREAM: 1(?!\d)|^remote error: 1(?!\d)|\bcode=1(?!\d)/;
|
|
67
|
+
/**
|
|
68
|
+
* A peer reset with `Cancel` (1) or `Internal` (0) on a subscription of ours: the relay ends a
|
|
69
|
+
* subscription this way when the broadcast it fed went away (its publisher left). The SDK reports
|
|
70
|
+
* that itself (`publisher left` on the subscription's status, the feedback listener's return), so
|
|
71
|
+
* @moq/net's line is a duplicate. Any other code (NotFound, Unauthorized, GroupTooLarge, a control
|
|
72
|
+
* timeout) is a real answer and passes through.
|
|
73
|
+
*/
|
|
74
|
+
const SOURCE_GONE_RESET = /^RESET_STREAM: [01](?!\d)|^remote error: [01](?!\d)|\bcode=[01](?!\d)/;
|
|
75
|
+
/** The session went away under the track. */
|
|
76
|
+
const SESSION_GONE = /session error|connection error|connection closed|\bclosed\b/i;
|
|
77
|
+
/**
|
|
78
|
+
* The broker refused admission with `admission_refused` (application session code `ADMISSION_REFUSED_CLOSE_CODE`,
|
|
79
|
+
* spelled `app code=502` by the transport) while our `res` subscribe and `req` announce were still
|
|
80
|
+
* pending. The node reports that itself (`reconnecting` with `error.code === "admission_refused"`); the
|
|
81
|
+
* per-track line @moq/net prints on every retry is a duplicate.
|
|
82
|
+
*/
|
|
83
|
+
const REFUSED_ADMISSION = new RegExp(`app code=${ADMISSION_REFUSED_CLOSE_CODE}(?!\\d)`);
|
|
84
|
+
let installed = false;
|
|
85
|
+
let original;
|
|
86
|
+
let originalDebug;
|
|
87
|
+
/** Registers a session with the filter; installs the `console.warn` wrapper on first use. */
|
|
88
|
+
export function moqLogScope() {
|
|
89
|
+
install();
|
|
90
|
+
const s = { paths: new Set(), requests: new Set(), closing: false };
|
|
91
|
+
scopes.add(s);
|
|
92
|
+
return {
|
|
93
|
+
announced: path => { s.paths.add(path); },
|
|
94
|
+
requested: path => { s.requests.add(path); },
|
|
95
|
+
closing: () => { s.closing = true; },
|
|
96
|
+
release: () => { s.releasedAt = Date.now(); },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function live() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const s of scopes)
|
|
102
|
+
if (s.releasedAt !== undefined && now - s.releasedAt > LINGER_MS)
|
|
103
|
+
scopes.delete(s);
|
|
104
|
+
return scopes;
|
|
105
|
+
}
|
|
106
|
+
/** Forgets every scope; for tests, whose earlier nodes would otherwise still count as closing. */
|
|
107
|
+
export function resetMoqLogScopes() { scopes.clear(); }
|
|
108
|
+
function install() {
|
|
109
|
+
if (installed)
|
|
110
|
+
return;
|
|
111
|
+
installed = true;
|
|
112
|
+
original = console.warn;
|
|
113
|
+
originalDebug = console.debug;
|
|
114
|
+
console.warn = function warn(...args) {
|
|
115
|
+
if (isRoutineEnd(args)) {
|
|
116
|
+
if (moqDebugEnabled())
|
|
117
|
+
originalDebug.call(console, "[moq]", ...args);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
original.apply(this ?? console, args);
|
|
121
|
+
};
|
|
122
|
+
console.debug = function debug(...args) {
|
|
123
|
+
if (isMoqDebug(args) && !moqDebugEnabled())
|
|
124
|
+
return;
|
|
125
|
+
originalDebug.apply(this ?? console, args);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Restores the original `console.warn`/`console.debug`; for tests. */
|
|
129
|
+
export function uninstallMoqLogFilter() {
|
|
130
|
+
if (!installed)
|
|
131
|
+
return;
|
|
132
|
+
installed = false;
|
|
133
|
+
console.warn = original;
|
|
134
|
+
console.debug = originalDebug;
|
|
135
|
+
original = undefined;
|
|
136
|
+
originalDebug = undefined;
|
|
137
|
+
}
|
|
138
|
+
/** Exported for tests: whether a `console.warn` call from @moq/net describes the normal end of one of our tracks. */
|
|
139
|
+
export function isRoutineEnd(args) {
|
|
140
|
+
const line = args[0];
|
|
141
|
+
if (typeof line !== "string")
|
|
142
|
+
return false;
|
|
143
|
+
const known = live();
|
|
144
|
+
if (line === DISCOVERY_FAILED) {
|
|
145
|
+
for (const s of known)
|
|
146
|
+
if (s.closing)
|
|
147
|
+
return true;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const pub = PUBLISH_ERROR.exec(line);
|
|
151
|
+
if (pub) {
|
|
152
|
+
const [, path, reason] = pub;
|
|
153
|
+
for (const s of known) {
|
|
154
|
+
if (!s.paths.has(path))
|
|
155
|
+
continue;
|
|
156
|
+
if (CANCEL_RESET.test(reason) || REFUSED_ADMISSION.test(reason))
|
|
157
|
+
return true;
|
|
158
|
+
if (s.closing && SESSION_GONE.test(reason))
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const sub = SUBSCRIBE_ERROR.exec(line);
|
|
164
|
+
if (sub) {
|
|
165
|
+
const [, path, reason] = sub;
|
|
166
|
+
for (const s of known) {
|
|
167
|
+
if (!s.requests.has(path))
|
|
168
|
+
continue;
|
|
169
|
+
if (SOURCE_GONE_RESET.test(reason) || REFUSED_ADMISSION.test(reason))
|
|
170
|
+
return true;
|
|
171
|
+
if (s.closing && SESSION_GONE.test(reason))
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { SynapseError, type ClientType, type ErrorBody, type Identity, type TimeDomain } from "@synapse-sdk/core";
|
|
2
|
+
import { Session, type Dial, type SessionKnobs } from "./session.ts";
|
|
3
|
+
import type { ConnectOptions, ConnectTarget, ConnectionState, Notice, ReconnectPolicy } from "./types.ts";
|
|
4
|
+
import { Cell, type Deferred } from "./util.ts";
|
|
5
|
+
/** `initial × 2^(attempt−1)` capped at `max`, times an equal jitter U(0.5, 1); never gives up. */
|
|
6
|
+
export declare function exponentialPolicy(initialMs: number, maxMs: number): ReconnectPolicy;
|
|
7
|
+
/** A fixed delay (tests). */
|
|
8
|
+
export declare function fixedPolicy(ms: number): ReconnectPolicy;
|
|
9
|
+
/** Stops after `maxAttempts` attempts in one phase. */
|
|
10
|
+
export declare function withMaxAttempts(p: ReconnectPolicy, maxAttempts: number): ReconnectPolicy;
|
|
11
|
+
export declare const defaultReconnectPolicy: ReconnectPolicy;
|
|
12
|
+
/** The node's declared target: bumped `generation` per change; `dial` undefined = deliberately disconnected. */
|
|
13
|
+
export interface Target {
|
|
14
|
+
generation: number;
|
|
15
|
+
dial: Dial | undefined;
|
|
16
|
+
identity: Identity;
|
|
17
|
+
}
|
|
18
|
+
/** The one lifecycle cell: `ConnectionState` with the session on `connected`. State, not history. */
|
|
19
|
+
export type Link = {
|
|
20
|
+
status: "connecting";
|
|
21
|
+
attempt: number;
|
|
22
|
+
} | {
|
|
23
|
+
status: "connected";
|
|
24
|
+
session: Session;
|
|
25
|
+
} | {
|
|
26
|
+
status: "reconnecting";
|
|
27
|
+
attempt: number;
|
|
28
|
+
reason: string;
|
|
29
|
+
error: SynapseError;
|
|
30
|
+
nextRetryAtMs: number;
|
|
31
|
+
} | {
|
|
32
|
+
status: "closing";
|
|
33
|
+
} | {
|
|
34
|
+
status: "closed";
|
|
35
|
+
reason: string;
|
|
36
|
+
} | {
|
|
37
|
+
status: "failed";
|
|
38
|
+
error: SynapseError;
|
|
39
|
+
};
|
|
40
|
+
export declare function stateOf(l: Link): ConnectionState;
|
|
41
|
+
/** What a handle task is told about the node's session, in its own mailbox, in order with its other events. */
|
|
42
|
+
export type SessionEvent = {
|
|
43
|
+
kind: "started";
|
|
44
|
+
session: Session;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "ended";
|
|
47
|
+
} | {
|
|
48
|
+
kind: "nodeEnded";
|
|
49
|
+
};
|
|
50
|
+
/** Where the loop replies to a `connect`/`setTarget` call waiting for a target generation. */
|
|
51
|
+
type Reply = Deferred<void>;
|
|
52
|
+
/** Why the loop is dialing. */
|
|
53
|
+
type Pursuit = {
|
|
54
|
+
kind: "first";
|
|
55
|
+
deadlineMs: number;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "reconnect";
|
|
58
|
+
} | {
|
|
59
|
+
kind: "target";
|
|
60
|
+
};
|
|
61
|
+
type Stand = {
|
|
62
|
+
kind: "live";
|
|
63
|
+
session: Session;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "dialing";
|
|
66
|
+
pursuit: Pursuit;
|
|
67
|
+
attempt: number;
|
|
68
|
+
delayMs: number;
|
|
69
|
+
reason: string;
|
|
70
|
+
error: SynapseError;
|
|
71
|
+
} | {
|
|
72
|
+
kind: "resting";
|
|
73
|
+
};
|
|
74
|
+
type Next = {
|
|
75
|
+
kind: "retry";
|
|
76
|
+
delayMs: number;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "failed";
|
|
79
|
+
} | {
|
|
80
|
+
kind: "closed";
|
|
81
|
+
};
|
|
82
|
+
/** The rules (SDK §2, WP §10.4) for the delay before attempt `attempt`. */
|
|
83
|
+
export declare function decide(pursuit: Pursuit, policy: ReconnectPolicy | null, err: SynapseError, attempt: number, nowMs: number): Next;
|
|
84
|
+
export declare class Node {
|
|
85
|
+
#private;
|
|
86
|
+
readonly link: Cell<Link>;
|
|
87
|
+
readonly target: Cell<Target>;
|
|
88
|
+
readonly dom: TimeDomain;
|
|
89
|
+
readonly knobs: SessionKnobs;
|
|
90
|
+
readonly policy: ReconnectPolicy | null;
|
|
91
|
+
/** Broker notices addressed to this node, across its sessions (fed by the loop from the current session's). */
|
|
92
|
+
readonly notices: Set<(n: ErrorBody) => void>;
|
|
93
|
+
readonly projectNotices: Set<(n: Notice) => void>;
|
|
94
|
+
/** Resolves once the node has ended for good (`close()`, the first connect failing, the policy giving up). */
|
|
95
|
+
readonly ended: Deferred<void>;
|
|
96
|
+
epochs: number;
|
|
97
|
+
reconnects: number;
|
|
98
|
+
callsSent: number;
|
|
99
|
+
callsAnswered: number;
|
|
100
|
+
constructor(target: Target, dom: TimeDomain, knobs: SessionKnobs, policy: ReconnectPolicy | null);
|
|
101
|
+
get isEnded(): boolean;
|
|
102
|
+
get isConnected(): boolean;
|
|
103
|
+
currentSession(): Session | undefined;
|
|
104
|
+
state(): ConnectionState;
|
|
105
|
+
/** The loop's one write. A write that names a session or withdraws one tells every monitoring handle, in the same step. */
|
|
106
|
+
setLink(next: Link): void;
|
|
107
|
+
/**
|
|
108
|
+
* Registers a handle to be told about sessions (and the node's end) and returns the session the
|
|
109
|
+
* node has right now; the next event is its end. `deliver` returns `false` once the handle's
|
|
110
|
+
* mailbox is gone, which unregisters it; `unmonitor` unregisters it at once.
|
|
111
|
+
*/
|
|
112
|
+
monitor(deliver: (e: SessionEvent) => boolean): {
|
|
113
|
+
session: Session | undefined;
|
|
114
|
+
unmonitor: () => void;
|
|
115
|
+
};
|
|
116
|
+
/** The node ends for good in `link`: handles are told, feeds end. */
|
|
117
|
+
end(link: Link): void;
|
|
118
|
+
/** Builds the freshly opened session completely (its epoch, its tasks running), then names it. */
|
|
119
|
+
attach(session: Session): Session;
|
|
120
|
+
/** `close()`: posts the command; resolves once the node has ended. */
|
|
121
|
+
close(): Promise<void>;
|
|
122
|
+
/** `setTarget()`: declares the target under the cell's write, posts the command with its reply, and awaits it. */
|
|
123
|
+
setTarget(dial: Dial | undefined): Promise<void>;
|
|
124
|
+
/** Starts the loop. `first` is the `connect()` call's reply. `signals`: close on SIGINT/SIGTERM (Bun/Node). */
|
|
125
|
+
run(stand: Stand, first: Reply | undefined, signals: boolean): void;
|
|
126
|
+
}
|
|
127
|
+
/** Resolves the identity a node declares (`resolve_identity`): a missing name is minted, a `web`/`hmi` node without a `user_id` mints one. */
|
|
128
|
+
export declare function resolveIdentity(o: {
|
|
129
|
+
type: ClientType;
|
|
130
|
+
name?: string;
|
|
131
|
+
userId?: string;
|
|
132
|
+
credential?: string;
|
|
133
|
+
}): Identity;
|
|
134
|
+
/** Resolves a `ConnectTarget` against the node's current identity (fields left out are kept). */
|
|
135
|
+
export declare function resolveTarget(node: Node, t: ConnectTarget): Dial;
|
|
136
|
+
/**
|
|
137
|
+
* Builds the node and starts its loop in the first pursuit. Resolves as soon as the loop runs, with
|
|
138
|
+
* the node (so a `close()` can reach it during the first dial) and `connected`: the loop's reply to
|
|
139
|
+
* the first connect, a session established or why there is none (`not_connected` after a `close()`).
|
|
140
|
+
*/
|
|
141
|
+
export declare function startNode(opts: ConnectOptions, giveUpAfterMs: number): Promise<{
|
|
142
|
+
node: Node;
|
|
143
|
+
connected: Promise<void>;
|
|
144
|
+
}>;
|
|
145
|
+
export {};
|