@synapse-sdk/client 0.3.0 → 0.3.3
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node.js +5 -4
- package/dist/session.d.ts +71 -10
- package/dist/session.js +275 -57
- package/dist/store.d.ts +10 -0
- package/dist/store.js +30 -4
- package/dist/synapse.d.ts +6 -2
- package/dist/synapse.js +18 -6
- package/dist/tasks.d.ts +37 -0
- package/dist/tasks.js +132 -0
- package/dist/topics.d.ts +21 -6
- package/dist/topics.js +42 -21
- package/dist/types.d.ts +153 -12
- package/package.json +2 -2
package/dist/store.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `destroyed` ends the handle. Writes name the epoch the mirror holds; the runtime checks ownership
|
|
9
9
|
// (`not_owner`). Between incarnations (a session change, a replay) nothing is known: `store_gone`.
|
|
10
10
|
import * as Moq from "@moq/net";
|
|
11
|
-
import { FLAGS,
|
|
11
|
+
import { FLAGS, STORE_TRACK, SynapseError, decode as decodeBody, decodeHeader, moqPriority, storeBasePath, storeRobotScoped, } from "@synapse-sdk/core";
|
|
12
12
|
import { startOn } from "./calls.js";
|
|
13
13
|
import { REFOLLOW_PACE_MS, holderOf, isTransient, refusalPaceMs } from "./channel.js";
|
|
14
14
|
import { latestSubscription, waitActive } from "./session.js";
|
|
@@ -224,7 +224,8 @@ export function startNamespace(ns, registry) { void run(ns, registry); }
|
|
|
224
224
|
async function run(ns, registry) {
|
|
225
225
|
const { name, opts, state, warn, node } = ns;
|
|
226
226
|
const post = (e) => { ns.post(e); };
|
|
227
|
-
|
|
227
|
+
/** A robot-scoped namespace belongs to the robot: a project switch leaves it as it is. */
|
|
228
|
+
const robotScoped = storeRobotScoped(name);
|
|
228
229
|
let session;
|
|
229
230
|
let phase = { kind: "idle" };
|
|
230
231
|
const ph = () => phase;
|
|
@@ -310,7 +311,7 @@ async function run(ns, registry) {
|
|
|
310
311
|
state.reset();
|
|
311
312
|
unwatchProject?.();
|
|
312
313
|
unwatchProject = undefined;
|
|
313
|
-
if (s && !
|
|
314
|
+
if (s && !robotScoped) {
|
|
314
315
|
const feed = s.project;
|
|
315
316
|
const current = feed.get();
|
|
316
317
|
if (known === undefined)
|
|
@@ -542,7 +543,7 @@ async function run(ns, registry) {
|
|
|
542
543
|
}
|
|
543
544
|
break;
|
|
544
545
|
case "projectChanged":
|
|
545
|
-
if (!
|
|
546
|
+
if (!robotScoped)
|
|
546
547
|
projectChanged(ev.now);
|
|
547
548
|
break;
|
|
548
549
|
case "paced":
|
|
@@ -843,6 +844,8 @@ export class StoreHandleImpl {
|
|
|
843
844
|
subscribe(listener) { return this.ns.state.subscribe(listener); }
|
|
844
845
|
/** Waits for the namespace to be in (the first snapshot applied, or absent). */
|
|
845
846
|
join() { return this.ns.request(reply => ({ kind: "join", reply })); }
|
|
847
|
+
/** `join()` under the SDK's word (Publisher and Registration have `ready()` too). */
|
|
848
|
+
ready() { return this.join(); }
|
|
846
849
|
close() {
|
|
847
850
|
if (this.#closed)
|
|
848
851
|
return;
|
|
@@ -896,3 +899,26 @@ export async function openStore(node, registry, name, opts, warn) {
|
|
|
896
899
|
return h;
|
|
897
900
|
}
|
|
898
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* The synchronous form of `openStore`, for a handle the node itself holds (`node.tasks`): a handle on
|
|
904
|
+
* the namespace's task, joined if one runs, else started now; the value is in once `ready()`
|
|
905
|
+
* resolves. A task that is ending (its last handle closed) is waited out before the new one starts:
|
|
906
|
+
* the new one is in the registry at once (a `store()` meanwhile joins it), its requests wait in its
|
|
907
|
+
* mailbox.
|
|
908
|
+
*/
|
|
909
|
+
export function attachStore(node, registry, name, opts, warn) {
|
|
910
|
+
const live = registry.get(name);
|
|
911
|
+
if (live && live.handles > 0) {
|
|
912
|
+
if (live.opts.access !== opts.access || live.opts.onDeath !== opts.onDeath)
|
|
913
|
+
throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
|
|
914
|
+
return new StoreHandleImpl(live);
|
|
915
|
+
}
|
|
916
|
+
const ns = new Namespace(name, opts, node, warn);
|
|
917
|
+
registry.set(name, ns);
|
|
918
|
+
const h = new StoreHandleImpl(ns);
|
|
919
|
+
if (live)
|
|
920
|
+
void live.exited.promise.then(() => startNamespace(ns, registry));
|
|
921
|
+
else
|
|
922
|
+
startNamespace(ns, registry);
|
|
923
|
+
return h;
|
|
924
|
+
}
|
package/dist/synapse.d.ts
CHANGED
|
@@ -2,12 +2,11 @@ import { type Age, type ClockInfo, type ErrorBody, type LocalTime, type ProjectS
|
|
|
2
2
|
import { Node } from "./node.ts";
|
|
3
3
|
import { StoreHandleImpl } from "./store.ts";
|
|
4
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";
|
|
5
|
+
import type { CallEvent, CallOptions, Catalog, ConnectOptions, ConnectTarget, ConnectionState, Envelope, Info, Metrics, Notice, Publisher, Registration, Run, ServeHandler, ServeOptions, SceneHandle, StoreHandle, StoreOptions, SubscribeOptions, Subscription, Tasks, View, ViewOptions } from "./types.ts";
|
|
6
6
|
/** How long the first `connect()` keeps trying a retryable failure before `failed` (SDK §2: the first connect is bounded). */
|
|
7
7
|
export declare const FIRST_CONNECT_GIVE_UP_MS = 30000;
|
|
8
8
|
export declare class Synapse implements AsyncDisposable {
|
|
9
9
|
#private;
|
|
10
|
-
readonly opts: ConnectOptions;
|
|
11
10
|
warn: Warn;
|
|
12
11
|
constructor(opts: ConnectOptions);
|
|
13
12
|
/** `await Synapse.connect(opts)`: a node with a session, or the reason there is none. */
|
|
@@ -85,5 +84,10 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
85
84
|
/** `synapse/project/leave {}`: drops our standing (every session of our `user_id`). */
|
|
86
85
|
leaveProject(): Promise<ProjectSnapshot>;
|
|
87
86
|
getProject(): Promise<ProjectSnapshot>;
|
|
87
|
+
/**
|
|
88
|
+
* The robot's task declarations, what runs (`status`), what the runtime can run (`catalog`), and the verbs; the same
|
|
89
|
+
* object for the node's life. Each handle opens on its first touch and is the node's own (`node.close()` ends it).
|
|
90
|
+
*/
|
|
91
|
+
get tasks(): Tasks;
|
|
88
92
|
}
|
|
89
93
|
export type { Envelope, StoreHandleImpl };
|
package/dist/synapse.js
CHANGED
|
@@ -7,7 +7,8 @@ import { callStream, startOn } from "./calls.js";
|
|
|
7
7
|
import { Node, resolveTarget, startNode } from "./node.js";
|
|
8
8
|
import { ServiceRegistration } from "./serve.js";
|
|
9
9
|
import { openScene } from "./scene.js";
|
|
10
|
-
import { Namespace, StoreHandleImpl, openStore } from "./store.js";
|
|
10
|
+
import { Namespace, StoreHandleImpl, attachStore, openStore } from "./store.js";
|
|
11
|
+
import { TasksFacet } from "./tasks.js";
|
|
11
12
|
import { PublisherHandle, TopicPublisher, TopicSubscription, wasmFor } from "./topics.js";
|
|
12
13
|
import { TopicView } from "./view.js";
|
|
13
14
|
/** How long the first `connect()` keeps trying a retryable failure before `failed` (SDK §2: the first connect is bounded). */
|
|
@@ -21,18 +22,19 @@ function wantedDesc(contract, n) {
|
|
|
21
22
|
}
|
|
22
23
|
/** 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
24
|
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
|
|
25
|
+
return JSON.stringify({ delivery: d.delivery, content_type: d.content_type, schema_hash: d.schema_hash ?? null, fec: d.fec ?? null });
|
|
25
26
|
}
|
|
26
27
|
export class Synapse {
|
|
27
|
-
opts;
|
|
28
28
|
#node;
|
|
29
29
|
#connecting;
|
|
30
30
|
#starting;
|
|
31
31
|
#publishers = new Map();
|
|
32
32
|
#stores = new Map();
|
|
33
33
|
warn;
|
|
34
|
+
// Private: the options hold the token and the credential, which nothing prints.
|
|
35
|
+
#opts;
|
|
34
36
|
constructor(opts) {
|
|
35
|
-
this
|
|
37
|
+
this.#opts = opts;
|
|
36
38
|
this.warn = msg => console.warn(`[synapse] ${msg}`);
|
|
37
39
|
}
|
|
38
40
|
/** `await Synapse.connect(opts)`: a node with a session, or the reason there is none. */
|
|
@@ -40,7 +42,7 @@ export class Synapse {
|
|
|
40
42
|
/** Connects (once; later calls return the same promise). Resolves once a session is established. */
|
|
41
43
|
connect() {
|
|
42
44
|
this.#connecting ??= (async () => {
|
|
43
|
-
const started = startNode(this
|
|
45
|
+
const started = startNode(this.#opts, FIRST_CONNECT_GIVE_UP_MS);
|
|
44
46
|
// The node as soon as its loop runs (or none: the options were refused), for a `close()` during the first dial.
|
|
45
47
|
this.#starting = started.then(s => s.node, () => undefined);
|
|
46
48
|
const { node, connected } = await started;
|
|
@@ -93,7 +95,7 @@ export class Synapse {
|
|
|
93
95
|
const n = this.node;
|
|
94
96
|
const id = n.target.get().identity;
|
|
95
97
|
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 ?? "",
|
|
98
|
+
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 ?? "", runtimeName: s?.hello.runtime_name ?? "", runtimeId: s?.hello.runtime_id ?? "", url: n.target.get().dial?.url ?? "" };
|
|
97
99
|
}
|
|
98
100
|
metrics() { const n = this.node; return { sessions: n.epochs, reconnects: n.reconnects, callsSent: n.callsSent, callsAnswered: n.callsAnswered }; }
|
|
99
101
|
/** Broker notices addressed to this node (an error body on `res` without a call). */
|
|
@@ -253,4 +255,14 @@ export class Synapse {
|
|
|
253
255
|
/** `synapse/project/leave {}`: drops our standing (every session of our `user_id`). */
|
|
254
256
|
leaveProject() { return this.#projectCall("leave", {}); }
|
|
255
257
|
getProject() { return this.#projectCall("get", {}); }
|
|
258
|
+
// -------------------------------------------------------------------------------- tasks
|
|
259
|
+
#tasks;
|
|
260
|
+
/**
|
|
261
|
+
* The robot's task declarations, what runs (`status`), what the runtime can run (`catalog`), and the verbs; the same
|
|
262
|
+
* object for the node's life. Each handle opens on its first touch and is the node's own (`node.close()` ends it).
|
|
263
|
+
*/
|
|
264
|
+
get tasks() {
|
|
265
|
+
const n = this.node;
|
|
266
|
+
return (this.#tasks ??= new TasksFacet(name => attachStore(n, this.#stores, name, {}, m => this.warn(m)), svc => this.call(svc, {}).result, m => this.warn(m)));
|
|
267
|
+
}
|
|
256
268
|
}
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SynapseError, type TasksReportItem, type TasksRun } from "@synapse-sdk/core";
|
|
2
|
+
import type { StoreHandleImpl } from "./store.ts";
|
|
3
|
+
import type { ReadonlyStoreHandle, TaskCatalogEntry, TaskDeclarations, Tasks, TasksStatus } from "./types.ts";
|
|
4
|
+
import type { Warn } from "./topics.ts";
|
|
5
|
+
/** The namespaces the facet reads and writes (the runtime's names). */
|
|
6
|
+
export declare const TASKS_NAMESPACES: {
|
|
7
|
+
readonly declarations: "tasks";
|
|
8
|
+
readonly status: "task_status";
|
|
9
|
+
readonly catalog: "task_catalog";
|
|
10
|
+
};
|
|
11
|
+
/** What `status.value` is before the runtime said anything: neither scope's state known, no tasks. */
|
|
12
|
+
export declare const EMPTY_TASKS_STATUS: TasksStatus;
|
|
13
|
+
/**
|
|
14
|
+
* `task_status`'s rows as a `TasksStatus`. A run row is the one keyed `robot` or `project`; an instance
|
|
15
|
+
* row goes where its own `scope`, `entity` and `instance` say (its key is never parsed: an instance
|
|
16
|
+
* name may hold `:`). A row that cannot be placed is left out and passed to `unplaced` (the runtime
|
|
17
|
+
* never writes one). The instance rows are the mirror's own objects.
|
|
18
|
+
*/
|
|
19
|
+
export declare function tasksStatusOf(rows: Readonly<Record<string, unknown>>, unplaced?: (key: string) => void): TasksStatus;
|
|
20
|
+
/** `node.tasks`. `attach(name)` hands out the node's handle on a namespace; `call` starts a call on the current session. */
|
|
21
|
+
export declare class TasksFacet implements Tasks {
|
|
22
|
+
private readonly call;
|
|
23
|
+
readonly declarations: TaskDeclarations;
|
|
24
|
+
readonly status: ReadonlyStoreHandle<TasksStatus>;
|
|
25
|
+
readonly catalog: ReadonlyStoreHandle<Readonly<Record<string, TaskCatalogEntry>>>;
|
|
26
|
+
constructor(attach: (name: string) => StoreHandleImpl, call: (svc: string) => Promise<unknown>, warn: Warn);
|
|
27
|
+
runRobotTasks(): Promise<TasksRun>;
|
|
28
|
+
stopRobotTasks(): Promise<void>;
|
|
29
|
+
runProjectTasks(): Promise<TasksRun>;
|
|
30
|
+
stopProjectTasks(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/** `true` for a refused run, `preflight_failed` or `launch_failed`, whose `data.report` names each declaration at fault and why. */
|
|
33
|
+
export declare function isTasksRefusal(e: unknown): e is SynapseError & {
|
|
34
|
+
readonly data: {
|
|
35
|
+
report: TasksReportItem[];
|
|
36
|
+
};
|
|
37
|
+
};
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// `node.tasks` (PLAN v0.29 SDK pass 2): the robot's task declarations (namespace `tasks`), what runs
|
|
2
|
+
// (`task_status`, read-only, typed by scope), what the runtime can run (`task_catalog`, read-only), and
|
|
3
|
+
// the four verbs. Each handle is a handle on the node's own namespace task (`store.ts`), opened on its
|
|
4
|
+
// first touch and held for the node's life: `node.store("task_status")` and `node.tasks.status` are one
|
|
5
|
+
// subscription. Nothing here is decided: the mirror is the runtime's, the verbs' replies are its
|
|
6
|
+
// answers, and the typed values are computed from the mirror (no copy), with structural sharing so an
|
|
7
|
+
// unchanged part keeps its object.
|
|
8
|
+
import { SynapseError } from "@synapse-sdk/core";
|
|
9
|
+
import { replaceEqualDeep } from "./util.js";
|
|
10
|
+
/** The namespaces the facet reads and writes (the runtime's names). */
|
|
11
|
+
export const TASKS_NAMESPACES = { declarations: "tasks", status: "task_status", catalog: "task_catalog" };
|
|
12
|
+
/** What `status.value` is before the runtime said anything: neither scope's state known, no tasks. */
|
|
13
|
+
export const EMPTY_TASKS_STATUS = { robot: { state: undefined, tasks: {} }, project: { state: undefined, tasks: {} } };
|
|
14
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15
|
+
/** A run row (`robot` / `project`: `{state, since, reason?}`) as the scope's run fields. */
|
|
16
|
+
function runOf(row) {
|
|
17
|
+
if (!isObject(row))
|
|
18
|
+
return { state: undefined };
|
|
19
|
+
const out = { state: row.state };
|
|
20
|
+
if (row.since !== undefined)
|
|
21
|
+
out.since = row.since;
|
|
22
|
+
if (row.reason !== undefined)
|
|
23
|
+
out.reason = row.reason;
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `task_status`'s rows as a `TasksStatus`. A run row is the one keyed `robot` or `project`; an instance
|
|
28
|
+
* row goes where its own `scope`, `entity` and `instance` say (its key is never parsed: an instance
|
|
29
|
+
* name may hold `:`). A row that cannot be placed is left out and passed to `unplaced` (the runtime
|
|
30
|
+
* never writes one). The instance rows are the mirror's own objects.
|
|
31
|
+
*/
|
|
32
|
+
export function tasksStatusOf(rows, unplaced = () => { }) {
|
|
33
|
+
const robot = {};
|
|
34
|
+
const project = {};
|
|
35
|
+
for (const [key, row] of Object.entries(rows)) {
|
|
36
|
+
if (key === "robot" || key === "project")
|
|
37
|
+
continue;
|
|
38
|
+
if (!isObject(row) || typeof row.instance !== "string") {
|
|
39
|
+
unplaced(key);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (row.scope === "robot")
|
|
43
|
+
robot[row.instance] = row;
|
|
44
|
+
else if (row.scope === "project" && typeof row.entity === "string")
|
|
45
|
+
(project[row.entity] ??= {})[row.instance] = row;
|
|
46
|
+
else
|
|
47
|
+
unplaced(key);
|
|
48
|
+
}
|
|
49
|
+
return { robot: { ...runOf(rows.robot), tasks: robot }, project: { ...runOf(rows.project), tasks: project } };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A read-only handle over one namespace of the node: `value` is `derive` of the mirror, recomputed only
|
|
53
|
+
* when the mirror changed and shared structurally with the previous value. The namespace is opened on
|
|
54
|
+
* the first touch of any member.
|
|
55
|
+
*/
|
|
56
|
+
class DerivedHandle {
|
|
57
|
+
open;
|
|
58
|
+
derive;
|
|
59
|
+
#h;
|
|
60
|
+
#from;
|
|
61
|
+
#value;
|
|
62
|
+
constructor(open, derive, empty) {
|
|
63
|
+
this.open = open;
|
|
64
|
+
this.derive = derive;
|
|
65
|
+
this.#value = empty;
|
|
66
|
+
}
|
|
67
|
+
#handle() { return (this.#h ??= this.open()); }
|
|
68
|
+
get value() {
|
|
69
|
+
const rows = this.#handle().value;
|
|
70
|
+
if (rows !== this.#from) {
|
|
71
|
+
this.#from = rows;
|
|
72
|
+
this.#value = replaceEqualDeep(this.#value, this.derive(rows));
|
|
73
|
+
}
|
|
74
|
+
return this.#value;
|
|
75
|
+
}
|
|
76
|
+
get synced() { return this.#handle().synced; }
|
|
77
|
+
ready() { return this.#handle().ready(); }
|
|
78
|
+
async *watch() {
|
|
79
|
+
for await (const _ of this.#handle().watch())
|
|
80
|
+
yield this.value;
|
|
81
|
+
}
|
|
82
|
+
subscribe(listener) { return this.#handle().subscribe(listener); }
|
|
83
|
+
}
|
|
84
|
+
/** The robot's declarations: the node's handle on `tasks`; every write waits for the value to be current first. */
|
|
85
|
+
class Declarations {
|
|
86
|
+
open;
|
|
87
|
+
#h;
|
|
88
|
+
constructor(open) {
|
|
89
|
+
this.open = open;
|
|
90
|
+
}
|
|
91
|
+
#handle() { return (this.#h ??= this.open()); }
|
|
92
|
+
get value() { return this.#handle().value; }
|
|
93
|
+
get(instance) { return this.#handle().get(instance); }
|
|
94
|
+
get synced() { return this.#handle().synced; }
|
|
95
|
+
get status() { return this.#handle().status; }
|
|
96
|
+
ready() { return this.#handle().ready(); }
|
|
97
|
+
async set(a, b) {
|
|
98
|
+
const h = this.#handle();
|
|
99
|
+
await h.ready();
|
|
100
|
+
await (typeof a === "string" ? h.set(a, b) : h.set(a));
|
|
101
|
+
}
|
|
102
|
+
async delete(instance) { const h = this.#handle(); await h.ready(); await h.delete(instance); }
|
|
103
|
+
async clear() { const h = this.#handle(); await h.ready(); await h.clear(); }
|
|
104
|
+
watch(instance) { return this.#handle().watch(instance); }
|
|
105
|
+
subscribe(listener) { return this.#handle().subscribe(listener); }
|
|
106
|
+
}
|
|
107
|
+
/** `node.tasks`. `attach(name)` hands out the node's handle on a namespace; `call` starts a call on the current session. */
|
|
108
|
+
export class TasksFacet {
|
|
109
|
+
call;
|
|
110
|
+
declarations;
|
|
111
|
+
status;
|
|
112
|
+
catalog;
|
|
113
|
+
constructor(attach, call, warn) {
|
|
114
|
+
this.call = call;
|
|
115
|
+
const warned = new Set();
|
|
116
|
+
const unplaced = (key) => { if (!warned.has(key)) {
|
|
117
|
+
warned.add(key);
|
|
118
|
+
warn(`tasks: status row ${JSON.stringify(key)} names no scope and instance; left out`);
|
|
119
|
+
} };
|
|
120
|
+
this.declarations = new Declarations(() => attach(TASKS_NAMESPACES.declarations));
|
|
121
|
+
this.status = new DerivedHandle(() => attach(TASKS_NAMESPACES.status), rows => tasksStatusOf(rows, unplaced), EMPTY_TASKS_STATUS);
|
|
122
|
+
this.catalog = new DerivedHandle(() => attach(TASKS_NAMESPACES.catalog), rows => rows, {});
|
|
123
|
+
}
|
|
124
|
+
async runRobotTasks() { return (await this.call("synapse/tasks/run_robot")); }
|
|
125
|
+
async stopRobotTasks() { await this.call("synapse/tasks/stop_robot"); }
|
|
126
|
+
async runProjectTasks() { return (await this.call("synapse/tasks/run_project")); }
|
|
127
|
+
async stopProjectTasks() { await this.call("synapse/tasks/stop_project"); }
|
|
128
|
+
}
|
|
129
|
+
/** `true` for a refused run, `preflight_failed` or `launch_failed`, whose `data.report` names each declaration at fault and why. */
|
|
130
|
+
export function isTasksRefusal(e) {
|
|
131
|
+
return e instanceof SynapseError && (e.code === "preflight_failed" || e.code === "launch_failed") && isObject(e.data) && Array.isArray(e.data.report);
|
|
132
|
+
}
|
package/dist/topics.d.ts
CHANGED
|
@@ -39,10 +39,22 @@ export declare class TopicPublisher<TIn, TOut> implements Intent {
|
|
|
39
39
|
* frame the codec refuses is refused (`invalid_input`) without taking a `seq`.
|
|
40
40
|
*/
|
|
41
41
|
publishFrame(type: number, flags: number, tUs: number, tlvs: Tlv[], body: Uint8Array): void;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The typed form: validates against the contract's schema and encodes msgpack; a raw topic's bytes
|
|
44
|
+
* go as given, and on a `video/*` topic as a `MEDIA` frame whose `KEY`/`CONFIG`/`RECOVERY` flags
|
|
45
|
+
* are read off the bitstream (Rust `Publisher<RawBody>::publish`).
|
|
46
|
+
*/
|
|
43
47
|
publish(value: TIn, opts?: PublishOptions): void;
|
|
48
|
+
/** An encoder's frame with the flags it already carries and its `STAGES` stamps (no re-parse); `tUs` 0 = now. The video helper's path (Rust `publish_media_frame`), not a user verb. */
|
|
49
|
+
publishMediaFrame(f: MediaFrame): void;
|
|
44
50
|
stats(): PublisherStats;
|
|
45
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The key of a publisher handle's encoder-frame path, the JS spelling of Rust's `pub(crate)
|
|
54
|
+
* publish_media_frame`: `@synapse-sdk/media`'s `videoPublisher` reaches it by the same
|
|
55
|
+
* `Symbol.for` key (the two packages share no module), so it is on no public type.
|
|
56
|
+
*/
|
|
57
|
+
export declare const PUBLISH_MEDIA_FRAME: unique symbol;
|
|
46
58
|
/** The application's handle: the last one closed ends the publisher's task (`unregister`). */
|
|
47
59
|
export declare class PublisherHandle<TIn, TOut> implements Publisher<TIn> {
|
|
48
60
|
#private;
|
|
@@ -53,10 +65,8 @@ export declare class PublisherHandle<TIn, TOut> implements Publisher<TIn> {
|
|
|
53
65
|
get verdict(): Verdict;
|
|
54
66
|
ready(): Promise<void>;
|
|
55
67
|
publish(value: TIn, opts?: PublishOptions): void;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/** `tUs` 0 = capture now (as the Rust client stamps it). */
|
|
59
|
-
publishMedia(f: MediaFrame): void;
|
|
68
|
+
/** `@synapse-sdk/media`'s video publisher: an encoder's frame with its flags and `STAGES` (see `PUBLISH_MEDIA_FRAME`). */
|
|
69
|
+
[PUBLISH_MEDIA_FRAME](f: MediaFrame): void;
|
|
60
70
|
stats(): PublisherStats;
|
|
61
71
|
close(): void;
|
|
62
72
|
[Symbol.asyncDispose](): Promise<void>;
|
|
@@ -119,7 +129,12 @@ export declare class TopicSubscription<T> implements Subscription<T> {
|
|
|
119
129
|
dropFecOf(publisher: string): void;
|
|
120
130
|
/** A new session (or none): the `fec` frames and a batch held by `throttleMs` are the previous session's and go. */
|
|
121
131
|
clearFec(): void;
|
|
122
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* The established descriptor's `content_type` (the catalog's, the runtime's statement), set by the
|
|
134
|
+
* follow task from each snapshot: bodies decode by it, not by what this subscription declared.
|
|
135
|
+
*/
|
|
136
|
+
contentType: string | undefined;
|
|
137
|
+
/** The decoding step: header facts plus the body by the established `content_type` (msgpack → a value, validated when the contract has a schema; anything else → bytes), or `mismatch` and dropped. */
|
|
123
138
|
envelope(payload: Uint8Array, publisher: string, dom: string): Envelope<T> | undefined;
|
|
124
139
|
/**
|
|
125
140
|
* The next batch, never empty; `undefined` once the subscription or the node ended. Drains what
|
package/dist/topics.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// when every listed publisher not held is tried again; no timer of our own. A datagram publisher
|
|
13
13
|
// whose own `desc` has `fec` gets a follower (the video path: `FecReader` per publisher).
|
|
14
14
|
import * as Moq from "@moq/net";
|
|
15
|
-
import { FLAGS, MSGPACK, SynapseError, TYPES, decode as decodeBody, decodeHeader, encode as encodeBody, fecSymbolSizeFor, frame, moqPriority, nowUs, stampSentUs, hasPendingSentUs, mediaHeader, topicPath, validateSync, } from "@synapse-sdk/core";
|
|
15
|
+
import { FLAGS, MSGPACK, SynapseError, TYPES, auParamSets, auSyncPoints, decode as decodeBody, decodeHeader, encode as encodeBody, fecSymbolSizeFor, frame, isVideo, mediaFlags, moqPriority, nowUs, stampSentUs, hasPendingSentUs, mediaHeader, topicPath, validateSync, } from "@synapse-sdk/core";
|
|
16
16
|
import { Handle, start as startHandle } from "./channel.js";
|
|
17
17
|
import { FecReader, FecWriter, ReportWriter, feedbackListener, requireFec } from "./fec.js";
|
|
18
18
|
import { RETAINED_FRAME_MAX_AGE_MS, StreamWriter, latestSubscription, moqTimestamp, streamTopicSubscription, waitActive, writeLatest } from "./session.js";
|
|
@@ -152,26 +152,35 @@ export class TopicPublisher {
|
|
|
152
152
|
else
|
|
153
153
|
this.droppedDisconnected++;
|
|
154
154
|
}
|
|
155
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* The typed form: validates against the contract's schema and encodes msgpack; a raw topic's bytes
|
|
157
|
+
* go as given, and on a `video/*` topic as a `MEDIA` frame whose `KEY`/`CONFIG`/`RECOVERY` flags
|
|
158
|
+
* are read off the bitstream (Rust `Publisher<RawBody>::publish`).
|
|
159
|
+
*/
|
|
156
160
|
publish(value, opts = {}) {
|
|
157
|
-
|
|
161
|
+
const tUs = opts.tUs ?? nowUs();
|
|
158
162
|
if (this.contract.raw) {
|
|
159
163
|
if (!(value instanceof Uint8Array))
|
|
160
164
|
throw new SynapseError("invalid_input", `topic ${this.name} (${this.contract.contentType}) takes bytes`);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let v = value;
|
|
165
|
-
if (this.contract.schema) {
|
|
166
|
-
const r = validateSync(this.contract.schema, value);
|
|
167
|
-
if (r.issues)
|
|
168
|
-
throw new SynapseError("invalid_input", { message: r.issues[0]?.message ?? "invalid value", data: { issues: r.issues } });
|
|
169
|
-
v = r.value;
|
|
165
|
+
if (!isVideo(this.contract.contentType)) {
|
|
166
|
+
this.publishFrame(TYPES.PUB, opts.flags ?? 0, tUs, [], value);
|
|
167
|
+
return;
|
|
170
168
|
}
|
|
171
|
-
|
|
169
|
+
const { idr, recovery } = auSyncPoints(value);
|
|
170
|
+
this.publishFrame(TYPES.MEDIA, mediaFlags({ key: idr, config: auParamSets(value) !== undefined, recovery }) | (opts.flags ?? 0), tUs, [], value);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let v = value;
|
|
174
|
+
if (this.contract.schema) {
|
|
175
|
+
const r = validateSync(this.contract.schema, value);
|
|
176
|
+
if (r.issues)
|
|
177
|
+
throw new SynapseError("invalid_input", { message: r.issues[0]?.message ?? "invalid value", data: { issues: r.issues } });
|
|
178
|
+
v = r.value;
|
|
172
179
|
}
|
|
173
|
-
this.publishFrame(TYPES.PUB, opts.flags ?? 0,
|
|
180
|
+
this.publishFrame(TYPES.PUB, opts.flags ?? 0, tUs, [], encodeBody(v));
|
|
174
181
|
}
|
|
182
|
+
/** An encoder's frame with the flags it already carries and its `STAGES` stamps (no re-parse); `tUs` 0 = now. The video helper's path (Rust `publish_media_frame`), not a user verb. */
|
|
183
|
+
publishMediaFrame(f) { const h = mediaHeader(f, 0); this.publishFrame(h.type, h.flags, f.tUs || nowUs(), h.tlvs ?? [], f.data); }
|
|
175
184
|
stats() {
|
|
176
185
|
const s = { published: this.published, droppedDisconnected: this.droppedDisconnected };
|
|
177
186
|
if (this.fecStats)
|
|
@@ -199,6 +208,12 @@ function finish(b) {
|
|
|
199
208
|
}
|
|
200
209
|
catch { /* closed */ }
|
|
201
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* The key of a publisher handle's encoder-frame path, the JS spelling of Rust's `pub(crate)
|
|
213
|
+
* publish_media_frame`: `@synapse-sdk/media`'s `videoPublisher` reaches it by the same
|
|
214
|
+
* `Symbol.for` key (the two packages share no module), so it is on no public type.
|
|
215
|
+
*/
|
|
216
|
+
export const PUBLISH_MEDIA_FRAME = Symbol.for("@synapse-sdk/client:publishMediaFrame");
|
|
202
217
|
/** The application's handle: the last one closed ends the publisher's task (`unregister`). */
|
|
203
218
|
export class PublisherHandle {
|
|
204
219
|
p;
|
|
@@ -216,10 +231,8 @@ export class PublisherHandle {
|
|
|
216
231
|
#open() { if (this.#closed)
|
|
217
232
|
throw new SynapseError("not_connected", "publisher closed"); }
|
|
218
233
|
publish(value, opts) { this.#open(); this.p.publish(value, opts); }
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
/** `tUs` 0 = capture now (as the Rust client stamps it). */
|
|
222
|
-
publishMedia(f) { this.#open(); const h = mediaHeader(f, 0); this.p.publishFrame(h.type, h.flags, f.tUs || nowUs(), h.tlvs ?? [], f.data); }
|
|
234
|
+
/** `@synapse-sdk/media`'s video publisher: an encoder's frame with its flags and `STAGES` (see `PUBLISH_MEDIA_FRAME`). */
|
|
235
|
+
[PUBLISH_MEDIA_FRAME](f) { this.#open(); this.p.publishMediaFrame(f); }
|
|
223
236
|
stats() { return this.p.stats(); }
|
|
224
237
|
close() {
|
|
225
238
|
if (this.#closed)
|
|
@@ -415,7 +428,12 @@ export class TopicSubscription {
|
|
|
415
428
|
/** A new session (or none): the `fec` frames and a batch held by `throttleMs` are the previous session's and go. */
|
|
416
429
|
clearFec() { this.#fec.length = 0; this.#throttled = undefined; }
|
|
417
430
|
#signal() { const w = this.#wake; this.#wake = undefined; w?.resolve(); }
|
|
418
|
-
/**
|
|
431
|
+
/**
|
|
432
|
+
* The established descriptor's `content_type` (the catalog's, the runtime's statement), set by the
|
|
433
|
+
* follow task from each snapshot: bodies decode by it, not by what this subscription declared.
|
|
434
|
+
*/
|
|
435
|
+
contentType;
|
|
436
|
+
/** The decoding step: header facts plus the body by the established `content_type` (msgpack → a value, validated when the contract has a schema; anything else → bytes), or `mismatch` and dropped. */
|
|
419
437
|
envelope(payload, publisher, dom) {
|
|
420
438
|
let header, hlen;
|
|
421
439
|
try {
|
|
@@ -427,7 +445,7 @@ export class TopicSubscription {
|
|
|
427
445
|
}
|
|
428
446
|
const body = payload.subarray(hlen);
|
|
429
447
|
let data;
|
|
430
|
-
if (this.contract.
|
|
448
|
+
if ((this.contentType ?? this.contract.contentType) !== MSGPACK)
|
|
431
449
|
data = body;
|
|
432
450
|
else {
|
|
433
451
|
let v;
|
|
@@ -448,7 +466,8 @@ export class TopicSubscription {
|
|
|
448
466
|
}
|
|
449
467
|
data = v;
|
|
450
468
|
}
|
|
451
|
-
|
|
469
|
+
const f = header.flags;
|
|
470
|
+
return { data, tUs: Number(header.tUs), seq: header.seq, publisher, dom, flags: f, type: header.type, tlvs: header.tlvs ?? [], key: (f & FLAGS.KEY) !== 0, config: (f & FLAGS.CONFIG) !== 0, recovery: (f & FLAGS.RECOVERY) !== 0 };
|
|
452
471
|
}
|
|
453
472
|
/** One payload taken from `src` (Rust `drain_source`'s step): every-message mode keeps it, latest mode keeps the newest `seq`. */
|
|
454
473
|
#take(src, payload, t, nowUs_) {
|
|
@@ -676,6 +695,8 @@ async function followTopic(sub) {
|
|
|
676
695
|
if (session && snapshot) {
|
|
677
696
|
const entry = snapshot.topics[contract.name];
|
|
678
697
|
delivery = entry?.desc?.delivery ?? contract.delivery;
|
|
698
|
+
// ... and so is the body's `content_type` (what the read decodes by).
|
|
699
|
+
sub.contentType = entry?.desc?.content_type;
|
|
679
700
|
const wanted = new Map();
|
|
680
701
|
if (entry)
|
|
681
702
|
for (const [prefix, l] of Object.entries(entry.publishers))
|