@synapse-sdk/client 0.3.3 → 0.3.4
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/calls.d.ts +7 -1
- package/dist/calls.js +19 -0
- package/dist/node.d.ts +62 -24
- package/dist/node.js +205 -66
- package/dist/scene.d.ts +150 -56
- package/dist/scene.js +995 -163
- package/dist/scene_frames.d.ts +2 -2
- package/dist/scene_frames.js +1 -1
- package/dist/session.d.ts +3 -1
- package/dist/session.js +4 -2
- package/dist/store.d.ts +22 -11
- package/dist/store.js +112 -26
- package/dist/synapse.d.ts +72 -45
- package/dist/synapse.js +179 -112
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +24 -10
- package/dist/topics.d.ts +2 -0
- package/dist/topics.js +17 -1
- package/dist/types.d.ts +268 -97
- package/dist/util.d.ts +43 -0
- package/dist/util.js +97 -1
- package/dist/view.d.ts +5 -3
- package/dist/view.js +21 -10
- package/package.json +2 -2
package/dist/scene_frames.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SceneEntity, SceneTxn, Xform } from "./types.ts";
|
|
2
2
|
/** The ops the materializers queue. */
|
|
3
3
|
type FrameTxn = Pick<SceneTxn, "spawn" | "patch" | "rename" | "despawn">;
|
|
4
|
-
/** The entities to reconcile against: `scene.
|
|
4
|
+
/** The entities to reconcile against: `scene.get()` (id → entity) or a list of entities. */
|
|
5
5
|
type Entities = Iterable<SceneEntity> | Readonly<Record<string, SceneEntity>>;
|
|
6
6
|
/** The presentation name of a materialized derived frame: `<parent name>_<frame>` (e.g. `ur5e_tool0`). */
|
|
7
7
|
export declare function derivedFrameName(parentName: string, frame: string): string;
|
|
@@ -28,7 +28,7 @@ export interface MaterializeReport {
|
|
|
28
28
|
/**
|
|
29
29
|
* Reconciles `robot`'s `robot.frames` with its derived frames (those with `props.from` equal to
|
|
30
30
|
* `provenance`, default `"urdf"`; a frame without `from` counts as `"urdf"`), queueing the ops on
|
|
31
|
-
* `txn`. `entities` is the scene now (`scene.
|
|
31
|
+
* `txn`. `entities` is the scene now (`scene.get()`). `restPoses` maps declared names to poses
|
|
32
32
|
* (FK at the authored joints, the caller's work); a name with no frame yet and no pose is an error,
|
|
33
33
|
* never a spawn, so a call with no poses only renames, backfills `from` and retracts.
|
|
34
34
|
*
|
package/dist/scene_frames.js
CHANGED
|
@@ -29,7 +29,7 @@ export function validateDerivedFrame(frame, parentId) {
|
|
|
29
29
|
/**
|
|
30
30
|
* Reconciles `robot`'s `robot.frames` with its derived frames (those with `props.from` equal to
|
|
31
31
|
* `provenance`, default `"urdf"`; a frame without `from` counts as `"urdf"`), queueing the ops on
|
|
32
|
-
* `txn`. `entities` is the scene now (`scene.
|
|
32
|
+
* `txn`. `entities` is the scene now (`scene.get()`). `restPoses` maps declared names to poses
|
|
33
33
|
* (FK at the authored joints, the caller's work); a name with no frame yet and no pose is an error,
|
|
34
34
|
* never a spawn, so a call with no poses only renames, backfills `from` and retracts.
|
|
35
35
|
*
|
package/dist/session.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export declare function ensureWebTransport(): Promise<void>;
|
|
|
83
83
|
export declare function describeError(e: unknown): string;
|
|
84
84
|
/**
|
|
85
85
|
* The error of a session's close (`classify_close`): `admission_refused` travels as the application
|
|
86
|
-
* close code (retryable: a live session
|
|
86
|
+
* close code (retryable: a live session holds this name; a crashed one is released after the idle timeout), another
|
|
87
87
|
* application code is a broker-side failure, `unauthorized` is the relay's own refusal, everything
|
|
88
88
|
* else is the transport (`connection_lost`); a clean close is a drop too (in one-shot mode a broker
|
|
89
89
|
* GOAWAY looks like one).
|
|
@@ -164,6 +164,8 @@ export declare class Session {
|
|
|
164
164
|
readonly log: MoqLogScope;
|
|
165
165
|
/** Counts *established* sessions of the node (SDK §2); set by the node when attached. */
|
|
166
166
|
epoch: number;
|
|
167
|
+
/** The target generation this session was opened for (`Target.generation`); set by the node's loop when attached. */
|
|
168
|
+
generation: number;
|
|
167
169
|
readonly startedMs: number;
|
|
168
170
|
readonly transport: "webtransport" | "websocket";
|
|
169
171
|
/** Datagrams travel only over WebTransport; the WebSocket fallback is stream-only. */
|
package/dist/session.js
CHANGED
|
@@ -313,7 +313,7 @@ const ADMISSION_REFUSED_SESSION_CODE = 64 + ADMISSION_REFUSED_CLOSE_CODE;
|
|
|
313
313
|
const ADMISSION_REFUSED_REASON = new RegExp(`app code=${ADMISSION_REFUSED_CLOSE_CODE}(?!\\d)`);
|
|
314
314
|
/**
|
|
315
315
|
* The error of a session's close (`classify_close`): `admission_refused` travels as the application
|
|
316
|
-
* close code (retryable: a live session
|
|
316
|
+
* close code (retryable: a live session holds this name; a crashed one is released after the idle timeout), another
|
|
317
317
|
* application code is a broker-side failure, `unauthorized` is the relay's own refusal, everything
|
|
318
318
|
* else is the transport (`connection_lost`); a clean close is a drop too (in one-shot mode a broker
|
|
319
319
|
* GOAWAY looks like one).
|
|
@@ -323,7 +323,7 @@ export function closeError(err) {
|
|
|
323
323
|
return new SynapseError("connection_lost", { message: "session closed by the broker", source: "transport" });
|
|
324
324
|
const text = `${err.message} ${err.reason ?? ""}`;
|
|
325
325
|
if ((err instanceof Moq.Error.Session && err.code === ADMISSION_REFUSED_SESSION_CODE) || ADMISSION_REFUSED_REASON.test(text)) {
|
|
326
|
-
return new SynapseError("admission_refused", { message: "the runtime refused this connection
|
|
326
|
+
return new SynapseError("admission_refused", { message: "the runtime refused this connection; retryable: a live session holds this name (a crashed one is released after the idle timeout); the SDK keeps retrying with backoff", source: "broker", cause: err });
|
|
327
327
|
}
|
|
328
328
|
if (err instanceof Moq.Error.Session && err.code === Moq.SessionCode.Unauthorized)
|
|
329
329
|
return new SynapseError("unauthorized", { message: text.trim(), source: "broker", cause: err });
|
|
@@ -446,6 +446,8 @@ export class Session {
|
|
|
446
446
|
log;
|
|
447
447
|
/** Counts *established* sessions of the node (SDK §2); set by the node when attached. */
|
|
448
448
|
epoch = 0;
|
|
449
|
+
/** The target generation this session was opened for (`Target.generation`); set by the node's loop when attached. */
|
|
450
|
+
generation = 0;
|
|
449
451
|
startedMs = performance.now();
|
|
450
452
|
transport;
|
|
451
453
|
/** Datagrams travel only over WebTransport; the WebSocket fallback is stream-only. */
|
package/dist/store.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Node } from "./node.ts";
|
|
|
3
3
|
import { type Session } from "./session.ts";
|
|
4
4
|
import type { Warn } from "./topics.ts";
|
|
5
5
|
import type { StoreChange, StoreHandle, StoreOptions, StoreStatus } from "./types.ts";
|
|
6
|
-
import { AsyncQueue, type Deferred } from "./util.ts";
|
|
6
|
+
import { AsyncQueue, type Deferred, type StateValue } from "./util.ts";
|
|
7
7
|
export declare const ENDED_DESTROYED = "destroyed";
|
|
8
8
|
export declare const ENDED_CONNECTION_LOST = "connection_lost";
|
|
9
9
|
export declare const ENDED_CLIENT = "client";
|
|
@@ -78,11 +78,19 @@ type Event = {
|
|
|
78
78
|
epoch: string;
|
|
79
79
|
version: number;
|
|
80
80
|
reply: Deferred<void>;
|
|
81
|
-
}
|
|
81
|
+
}
|
|
82
|
+
/** A write of handle `handle` (its parked writes are refused when it leaves). */
|
|
83
|
+
| {
|
|
82
84
|
kind: "write";
|
|
85
|
+
handle: number;
|
|
83
86
|
op: string;
|
|
84
87
|
args: WriteOp;
|
|
85
88
|
reply: Deferred<unknown>;
|
|
89
|
+
}
|
|
90
|
+
/** Handle `handle` was closed, others remain: its parked writes are refused. */
|
|
91
|
+
| {
|
|
92
|
+
kind: "leave";
|
|
93
|
+
handle: number;
|
|
86
94
|
} | {
|
|
87
95
|
kind: "writeDone";
|
|
88
96
|
op: string;
|
|
@@ -107,6 +115,7 @@ export declare class Namespace {
|
|
|
107
115
|
readonly mailbox: AsyncQueue<Event>;
|
|
108
116
|
readonly exited: Deferred<void>;
|
|
109
117
|
handles: number;
|
|
118
|
+
nextHandle: number;
|
|
110
119
|
constructor(name: string, opts: StoreOptions, node: Node, warn: Warn);
|
|
111
120
|
post(e: Event): boolean;
|
|
112
121
|
request<T>(make: (reply: Deferred<T>) => Event): Promise<T>;
|
|
@@ -116,32 +125,34 @@ export declare function startNamespace(ns: Namespace, registry: Map<string, Name
|
|
|
116
125
|
export declare class StoreHandleImpl implements StoreHandle {
|
|
117
126
|
#private;
|
|
118
127
|
private readonly ns;
|
|
128
|
+
/** The handle's own state, a state value: `get()` (the same object while unchanged), `onChange`, `changed()`, `watch()`. */
|
|
129
|
+
readonly status: StateValue<StoreStatus>;
|
|
119
130
|
constructor(ns: Namespace);
|
|
120
131
|
get name(): string;
|
|
121
|
-
/**
|
|
122
|
-
get
|
|
132
|
+
/** `get()`: the whole value, what the runtime has; the same object while nothing changed (reference-stable for React). `get(key)`: that key's value, `undefined` when absent. */
|
|
133
|
+
get(): Record<string, unknown>;
|
|
123
134
|
get(key: string): unknown;
|
|
124
135
|
get synced(): boolean;
|
|
125
|
-
|
|
126
|
-
/** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `value` shows it. */
|
|
136
|
+
/** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `get()` shows it. */
|
|
127
137
|
set(keyOrValues: string | Record<string, unknown>, value?: unknown): Promise<void>;
|
|
128
138
|
delete(key: string): Promise<void>;
|
|
129
139
|
clear(): Promise<void>;
|
|
130
140
|
destroy(): Promise<void>;
|
|
131
141
|
/** `for await (const value of store.watch())`: the whole value after each change; `store.watch(key)`: that key's value each time it changes. Ends with the handle; leaving the loop removes the feed. */
|
|
132
142
|
watch(key?: string): AsyncGenerator<unknown, void, undefined>;
|
|
133
|
-
/** The
|
|
134
|
-
|
|
143
|
+
/** The whole value after its next change (`store_gone` once this handle is closed). */
|
|
144
|
+
changed(): Promise<Record<string, unknown>>;
|
|
145
|
+
/** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
|
|
146
|
+
onChange(cb: (value: Record<string, unknown>) => void): () => void;
|
|
135
147
|
/** Waits for the namespace to be in (the first snapshot applied, or absent). */
|
|
136
|
-
join(): Promise<void>;
|
|
137
|
-
/** `join()` under the SDK's word (Publisher and Registration have `ready()` too). */
|
|
138
148
|
ready(): Promise<void>;
|
|
139
149
|
close(): void;
|
|
140
150
|
[Symbol.asyncDispose](): Promise<void>;
|
|
141
151
|
}
|
|
142
152
|
/**
|
|
143
153
|
* `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
|
|
144
|
-
* it (no open written), with other options `invalid_input`; one exiting is waited out.
|
|
154
|
+
* it (no open written), with other options `invalid_input`; one exiting is waited out. Without a session
|
|
155
|
+
* (before the first, between two) the task waits for the next one, like after a drop; it resolves once in.
|
|
145
156
|
*/
|
|
146
157
|
export declare function openStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): Promise<StoreHandleImpl>;
|
|
147
158
|
/**
|
package/dist/store.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
// here. A refusal is asked again, paced, with a warning (R6); a transient reply paced, silently; an
|
|
7
7
|
// incarnation that ends (writer gone, project switch, the owner restarted) replays the intent;
|
|
8
8
|
// `destroyed` ends the handle. Writes name the epoch the mirror holds; the runtime checks ownership
|
|
9
|
-
// (`not_owner`). Between incarnations (a session change, a replay) nothing is known:
|
|
9
|
+
// (`not_owner`). Between incarnations (a session change, a replay) nothing is known: a write then is
|
|
10
|
+
// parked by the task and sent once the handle binds again (`store_gone` if it ends first).
|
|
10
11
|
import * as Moq from "@moq/net";
|
|
11
12
|
import { FLAGS, STORE_TRACK, SynapseError, decode as decodeBody, decodeHeader, moqPriority, storeBasePath, storeRobotScoped, } from "@synapse-sdk/core";
|
|
12
13
|
import { startOn } from "./calls.js";
|
|
13
14
|
import { REFOLLOW_PACE_MS, holderOf, isTransient, refusalPaceMs } from "./channel.js";
|
|
14
15
|
import { latestSubscription, waitActive } from "./session.js";
|
|
15
|
-
import { AsyncQueue, TIMEOUT, deferred, withTimeout } from "./util.js";
|
|
16
|
+
import { AsyncQueue, StatusCell, TIMEOUT, deferred, withTimeout } from "./util.js";
|
|
16
17
|
export const ENDED_DESTROYED = "destroyed";
|
|
17
18
|
export const ENDED_CONNECTION_LOST = "connection_lost";
|
|
18
19
|
export const ENDED_CLIENT = "client";
|
|
@@ -206,6 +207,7 @@ export class Namespace {
|
|
|
206
207
|
mailbox = new AsyncQueue(Infinity);
|
|
207
208
|
exited = deferred();
|
|
208
209
|
handles = 0;
|
|
210
|
+
nextHandle = 0;
|
|
209
211
|
constructor(name, opts, node, warn) {
|
|
210
212
|
this.name = name;
|
|
211
213
|
this.opts = opts;
|
|
@@ -239,6 +241,9 @@ async function run(ns, registry) {
|
|
|
239
241
|
let unwatchProject;
|
|
240
242
|
const joining = [];
|
|
241
243
|
let versionWaiters = [];
|
|
244
|
+
/** Writes that came while no incarnation was bound (a replay, or no session yet), in arrival order: sent once the handle binds again, refused `store_gone` if it ends first. Each counts in `pending`. */
|
|
245
|
+
let parked = [];
|
|
246
|
+
const ABSENT = "the namespace is absent: open it with an access mode to create it";
|
|
242
247
|
const leaveIncarnation = () => { for (const w of versionWaiters.splice(0))
|
|
243
248
|
w.reply.reject(gone(name, `the handle left the incarnation before version ${w.version}`)); };
|
|
244
249
|
const stopPhase = () => {
|
|
@@ -358,9 +363,12 @@ async function run(ns, registry) {
|
|
|
358
363
|
const done = follow(ns, s, announced, access?.epoch, stop.signal);
|
|
359
364
|
phase = { kind: "following", attempt, stop, done };
|
|
360
365
|
void done.then(reason => post({ kind: "followEnded", attempt, reason }));
|
|
361
|
-
|
|
366
|
+
// Absent: honestly in (nothing to load), and nothing to write to (the parked writes are refused).
|
|
367
|
+
if (!access) {
|
|
362
368
|
for (const j of joining.splice(0))
|
|
363
369
|
j.resolve();
|
|
370
|
+
refuseParked(ABSENT);
|
|
371
|
+
}
|
|
364
372
|
return;
|
|
365
373
|
}
|
|
366
374
|
announced.close();
|
|
@@ -389,6 +397,9 @@ async function run(ns, registry) {
|
|
|
389
397
|
if (state.loaded && state.epoch === epoch && state.version >= wanted)
|
|
390
398
|
for (const j of joining.splice(0))
|
|
391
399
|
j.resolve();
|
|
400
|
+
// The handle is bound again (the reader applied the snapshot that names the epoch): the writes parked meanwhile go out, in order, naming it.
|
|
401
|
+
if (state.ended === undefined && state.epoch !== undefined)
|
|
402
|
+
sendParked();
|
|
392
403
|
};
|
|
393
404
|
const join = (reply) => {
|
|
394
405
|
if (phase.kind === "waitingProject") {
|
|
@@ -426,14 +437,53 @@ async function run(ns, registry) {
|
|
|
426
437
|
again(REFOLLOW_PACE_MS);
|
|
427
438
|
}
|
|
428
439
|
};
|
|
429
|
-
|
|
440
|
+
/**
|
|
441
|
+
* A write. Ended or absent: `store_gone`. Between incarnations (a replay, or no session yet) it is parked: a write is
|
|
442
|
+
* intent, sent once the handle binds again (the runtime decides it then), refused `store_gone` if the handle ends first;
|
|
443
|
+
* it has no deadline of its own. Bound: sent after any parked before it, naming the mirror's epoch (and claim).
|
|
444
|
+
*/
|
|
445
|
+
const write = (handle, op, args, reply) => {
|
|
430
446
|
if (state.ended !== undefined) {
|
|
431
447
|
reply.reject(gone(name, `the handle ended (${state.ended})`));
|
|
432
448
|
return;
|
|
433
449
|
}
|
|
450
|
+
if (state.epoch === undefined) {
|
|
451
|
+
if (state.absent) {
|
|
452
|
+
reply.reject(gone(name, ABSENT));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
parked.push({ handle, op, args, reply });
|
|
456
|
+
state.setPending(state.pending + 1);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
sendParked();
|
|
460
|
+
send(op, args, reply);
|
|
461
|
+
};
|
|
462
|
+
/** The parked writes go out, in arrival order (the handle is bound). */
|
|
463
|
+
const sendParked = () => {
|
|
464
|
+
if (!parked.length)
|
|
465
|
+
return;
|
|
466
|
+
const ws = parked;
|
|
467
|
+
parked = [];
|
|
468
|
+
state.setPending(state.pending - ws.length);
|
|
469
|
+
for (const w of ws)
|
|
470
|
+
send(w.op, w.args, w.reply);
|
|
471
|
+
};
|
|
472
|
+
/** The parked writes are refused `store_gone` (`why`): all of them (the handle ended, or the namespace is absent), or those of handle `of` (that handle was closed, others stay open). */
|
|
473
|
+
const refuseParked = (why, of) => {
|
|
474
|
+
const ws = parked.filter(w => of === undefined || w.handle === of);
|
|
475
|
+
if (!ws.length)
|
|
476
|
+
return;
|
|
477
|
+
parked = parked.filter(w => !ws.includes(w));
|
|
478
|
+
state.setPending(state.pending - ws.length);
|
|
479
|
+
for (const w of ws)
|
|
480
|
+
w.reply.reject(gone(name, why));
|
|
481
|
+
};
|
|
482
|
+
/** One write on the wire, naming the bound epoch (and claim); answered by the broker (`writeDone`). */
|
|
483
|
+
const send = (op, args, reply) => {
|
|
434
484
|
const epoch = state.epoch;
|
|
435
485
|
if (epoch === undefined) {
|
|
436
|
-
reply.reject(
|
|
486
|
+
reply.reject(gone(name, "no incarnation is bound"));
|
|
437
487
|
return;
|
|
438
488
|
}
|
|
439
489
|
if (!session) {
|
|
@@ -534,7 +584,10 @@ async function run(ns, registry) {
|
|
|
534
584
|
waitVersion(ev.epoch, ev.version, ev.reply);
|
|
535
585
|
break;
|
|
536
586
|
case "write":
|
|
537
|
-
write(ev.op, ev.args, ev.reply);
|
|
587
|
+
write(ev.handle, ev.op, ev.args, ev.reply);
|
|
588
|
+
break;
|
|
589
|
+
case "leave":
|
|
590
|
+
refuseParked(`the handle ended (${ENDED_CLIENT})`, ev.handle);
|
|
538
591
|
break;
|
|
539
592
|
case "writeDone":
|
|
540
593
|
if (writeDone(ev.op, ev.result, ev.reply)) {
|
|
@@ -567,6 +620,7 @@ async function run(ns, registry) {
|
|
|
567
620
|
for (const j of joining.splice(0))
|
|
568
621
|
j.reject(e);
|
|
569
622
|
leaveIncarnation();
|
|
623
|
+
refuseParked(`the handle ended (${state.ended ?? reason ?? ENDED_CLIENT})`);
|
|
570
624
|
{
|
|
571
625
|
const p = ph();
|
|
572
626
|
if (p.kind === "following" || p.kind === "paced")
|
|
@@ -580,7 +634,7 @@ async function run(ns, registry) {
|
|
|
580
634
|
if (ev.kind === "writeDone")
|
|
581
635
|
writeDone(ev.op, ev.result, ev.reply);
|
|
582
636
|
else if (ev.kind === "write")
|
|
583
|
-
write(ev.op, ev.args, ev.reply);
|
|
637
|
+
write(ev.handle, ev.op, ev.args, ev.reply);
|
|
584
638
|
else if (ev.kind === "opened") {
|
|
585
639
|
const p = ph();
|
|
586
640
|
if (p.kind === "opening" && p.o.attempt === ev.attempt) {
|
|
@@ -771,31 +825,39 @@ async function follow(ns, session, announced, want, signal) {
|
|
|
771
825
|
export class StoreHandleImpl {
|
|
772
826
|
ns;
|
|
773
827
|
#closed = false;
|
|
828
|
+
/** This handle's id in its namespace: its parked writes are refused when it closes. */
|
|
829
|
+
#id;
|
|
774
830
|
/** Resolved when this handle closes: its version waits end. */
|
|
775
831
|
#left = deferred();
|
|
776
832
|
/** This handle's change feeds: closed with it. */
|
|
777
833
|
#feeds = new Set();
|
|
778
834
|
#valueObj;
|
|
779
835
|
#valueOf;
|
|
836
|
+
/** Told when this handle closes (its status says `ended: client` from then on, whatever the namespace does). */
|
|
837
|
+
#onClose = new Set();
|
|
838
|
+
/** The handle's own state, a state value: `get()` (the same object while unchanged), `onChange`, `changed()`, `watch()`. */
|
|
839
|
+
status;
|
|
840
|
+
#status;
|
|
780
841
|
constructor(ns) {
|
|
781
842
|
this.ns = ns;
|
|
782
843
|
ns.handles++;
|
|
844
|
+
this.#id = ns.nextHandle++;
|
|
845
|
+
this.status = this.#status = new StatusCell(() => this.ns.state.status(this.ns.node.isConnected, this.#closed), l => { const un = this.ns.state.subscribe(l); this.#onClose.add(l); return () => { un(); this.#onClose.delete(l); }; }, () => this.#closed || this.ns.state.ended !== undefined);
|
|
783
846
|
}
|
|
784
847
|
#ended() { return gone(this.ns.name, `the handle ended (${ENDED_CLIENT})`); }
|
|
785
848
|
get name() { return this.ns.name; }
|
|
786
|
-
|
|
787
|
-
get value() {
|
|
849
|
+
get(key) {
|
|
788
850
|
const flat = this.ns.state.flat;
|
|
851
|
+
if (key !== undefined)
|
|
852
|
+
return flat.get(key);
|
|
789
853
|
if (this.#valueOf !== flat) {
|
|
790
854
|
this.#valueOf = flat;
|
|
791
855
|
this.#valueObj = Object.fromEntries(flat);
|
|
792
856
|
}
|
|
793
857
|
return this.#valueObj;
|
|
794
858
|
}
|
|
795
|
-
get(
|
|
796
|
-
|
|
797
|
-
get status() { return this.ns.state.status(this.ns.node.isConnected, this.#closed); }
|
|
798
|
-
/** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `value` shows it. */
|
|
859
|
+
get synced() { return this.status.get().synced; }
|
|
860
|
+
/** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `get()` shows it. */
|
|
799
861
|
async set(keyOrValues, value) {
|
|
800
862
|
const v = typeof keyOrValues === "string" ? await this.#write("put", { key: keyOrValues, value }) : await this.#write("put_many", { values: keyOrValues });
|
|
801
863
|
await this.#reached(v);
|
|
@@ -803,11 +865,11 @@ export class StoreHandleImpl {
|
|
|
803
865
|
async delete(key) { await this.#reached(await this.#write("delete", { key })); }
|
|
804
866
|
async clear() { await this.#reached(await this.#write("clear", {})); }
|
|
805
867
|
async destroy() { await this.#write("destroy", {}); }
|
|
806
|
-
/** The wire half of a write: the runtime's answer, the version it assigned. */
|
|
868
|
+
/** The wire half of a write: the runtime's answer, the version it assigned (between incarnations the task holds it until the handle binds again). */
|
|
807
869
|
async #write(op, args) {
|
|
808
870
|
if (this.#closed)
|
|
809
871
|
throw this.#ended();
|
|
810
|
-
const r = await this.ns.request(reply => ({ kind: "write", op, args, reply }));
|
|
872
|
+
const r = await this.ns.request(reply => ({ kind: "write", handle: this.#id, op, args, reply }));
|
|
811
873
|
const v = r?.version;
|
|
812
874
|
return typeof v === "number" ? v : typeof v === "bigint" ? Number(v) : 0;
|
|
813
875
|
}
|
|
@@ -830,7 +892,7 @@ export class StoreHandleImpl {
|
|
|
830
892
|
try {
|
|
831
893
|
for await (const batch of q) {
|
|
832
894
|
if (key === undefined)
|
|
833
|
-
yield this.
|
|
895
|
+
yield this.get();
|
|
834
896
|
else if (batch.some(c => c.key === key))
|
|
835
897
|
yield this.ns.state.flat.get(key);
|
|
836
898
|
}
|
|
@@ -840,12 +902,25 @@ export class StoreHandleImpl {
|
|
|
840
902
|
this.ns.state.unwatch(q);
|
|
841
903
|
}
|
|
842
904
|
}
|
|
843
|
-
/** The
|
|
844
|
-
|
|
905
|
+
/** The whole value after its next change (`store_gone` once this handle is closed). */
|
|
906
|
+
changed() {
|
|
907
|
+
if (this.#closed)
|
|
908
|
+
return Promise.reject(this.#ended());
|
|
909
|
+
const from = this.get();
|
|
910
|
+
const d = deferred();
|
|
911
|
+
const un = this.ns.state.subscribe(() => { const v = this.get(); if (v !== from) {
|
|
912
|
+
un();
|
|
913
|
+
this.#onClose.delete(gone);
|
|
914
|
+
d.resolve(v);
|
|
915
|
+
} });
|
|
916
|
+
const gone = () => { un(); this.#onClose.delete(gone); d.reject(this.#ended()); };
|
|
917
|
+
this.#onClose.add(gone);
|
|
918
|
+
return d.promise;
|
|
919
|
+
}
|
|
920
|
+
/** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
|
|
921
|
+
onChange(cb) { return this.ns.state.subscribe(() => cb(this.get())); }
|
|
845
922
|
/** Waits for the namespace to be in (the first snapshot applied, or absent). */
|
|
846
|
-
|
|
847
|
-
/** `join()` under the SDK's word (Publisher and Registration have `ready()` too). */
|
|
848
|
-
ready() { return this.join(); }
|
|
923
|
+
ready() { return this.ns.request(reply => ({ kind: "join", reply })); }
|
|
849
924
|
close() {
|
|
850
925
|
if (this.#closed)
|
|
851
926
|
return;
|
|
@@ -854,18 +929,29 @@ export class StoreHandleImpl {
|
|
|
854
929
|
for (const q of this.#feeds)
|
|
855
930
|
this.ns.state.unwatch(q);
|
|
856
931
|
this.#feeds.clear();
|
|
932
|
+
for (const l of [...this.#onClose]) {
|
|
933
|
+
try {
|
|
934
|
+
l();
|
|
935
|
+
}
|
|
936
|
+
catch (e) {
|
|
937
|
+
queueMicrotask(() => { throw e; });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
this.#onClose.clear();
|
|
941
|
+
this.#status.dispose();
|
|
857
942
|
if (--this.ns.handles === 0)
|
|
858
943
|
this.ns.post({ kind: "close" });
|
|
944
|
+
else
|
|
945
|
+
this.ns.post({ kind: "leave", handle: this.#id });
|
|
859
946
|
}
|
|
860
947
|
async [Symbol.asyncDispose]() { this.close(); }
|
|
861
948
|
}
|
|
862
949
|
/**
|
|
863
950
|
* `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
|
|
864
|
-
* it (no open written), with other options `invalid_input`; one exiting is waited out.
|
|
951
|
+
* it (no open written), with other options `invalid_input`; one exiting is waited out. Without a session
|
|
952
|
+
* (before the first, between two) the task waits for the next one, like after a drop; it resolves once in.
|
|
865
953
|
*/
|
|
866
954
|
export async function openStore(node, registry, name, opts, warn) {
|
|
867
|
-
if (!node.currentSession())
|
|
868
|
-
throw new SynapseError("not_connected");
|
|
869
955
|
for (;;) {
|
|
870
956
|
const live = registry.get(name);
|
|
871
957
|
if (live) {
|
|
@@ -877,7 +963,7 @@ export async function openStore(node, registry, name, opts, warn) {
|
|
|
877
963
|
throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
|
|
878
964
|
const h = new StoreHandleImpl(live);
|
|
879
965
|
try {
|
|
880
|
-
await h.
|
|
966
|
+
await h.ready();
|
|
881
967
|
}
|
|
882
968
|
catch (e) {
|
|
883
969
|
h.close();
|
|
@@ -890,7 +976,7 @@ export async function openStore(node, registry, name, opts, warn) {
|
|
|
890
976
|
const h = new StoreHandleImpl(ns);
|
|
891
977
|
startNamespace(ns, registry);
|
|
892
978
|
try {
|
|
893
|
-
await h.
|
|
979
|
+
await h.ready();
|
|
894
980
|
}
|
|
895
981
|
catch (e) {
|
|
896
982
|
h.close();
|
package/dist/synapse.d.ts
CHANGED
|
@@ -1,44 +1,58 @@
|
|
|
1
|
-
import { type Age, type
|
|
1
|
+
import { type Age, type ErrorBody, type ProjectSnapshot, type ServiceContract, type TopicContract } from "@synapse-sdk/core";
|
|
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,
|
|
6
|
-
|
|
7
|
-
export
|
|
5
|
+
import type { CallEvent, CallOptions, Catalog, ConnectOptions, ConnectTarget, ConnectionState, Envelope, Info, Metrics, Publisher, Registration, Run, ServeHandler, ServeOptions, SceneHandle, StoreHandle, StoreOptions, SubscribeOptions, Subscription, Tasks, View, ViewOptions } from "./types.ts";
|
|
6
|
+
import { type StateValue } from "./util.ts";
|
|
7
|
+
export { FIRST_CONNECT_GIVE_UP_MS } from "./node.ts";
|
|
8
8
|
export declare class Synapse implements AsyncDisposable {
|
|
9
9
|
#private;
|
|
10
10
|
warn: Warn;
|
|
11
|
+
/**
|
|
12
|
+
* The node, usable at once and `idle` (no target) until `connect()`: constructing opens no connection. Where to connect
|
|
13
|
+
* (the url, its token, the certificate pin) is the target's, given to `connect()`. Throws `invalid_input` for options
|
|
14
|
+
* that can never work (a bad `name`).
|
|
15
|
+
*/
|
|
11
16
|
constructor(opts: ConnectOptions);
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Connects the node to `target` (a string is the url), the one connect verb: the first connect, and how an HMI moves
|
|
19
|
+
* from one robot's runtime to another. The current session, if any, is torn down before `target` is dialed; every
|
|
20
|
+
* handle re-binds to the new session as across a reconnect. Identity fields `target` leaves out keep the node's.
|
|
21
|
+
*
|
|
22
|
+
* Operator intent is never lost under quick clicking: the newest call wins. The node works toward its latest target; a
|
|
23
|
+
* call superseded by a newer `connect()` or `disconnect()` resolves quietly. The target already declared declares
|
|
24
|
+
* nothing and settles with it, except from its own `failed`, where it is dialed again.
|
|
25
|
+
*
|
|
26
|
+
* Bounded (SDK §2): a retryable failure is tried again, paced by the reconnect policy, until `FIRST_CONNECT_GIVE_UP_MS`
|
|
27
|
+
* after the first attempt, a hard bound (an attempt still outstanding then is cut); `unauthorized` gives up at once.
|
|
28
|
+
* Resolves once a session to *this* target is connected (or a newer call superseded it). Rejects with this target's
|
|
29
|
+
* error when the node rests `failed` (still usable: `connect()` dials again), `invalid_input` at once for a malformed
|
|
30
|
+
* target, `not_connected` once the node has ended (`close()`).
|
|
31
|
+
*
|
|
32
|
+
* With no target: the declared target, if there is one (settles with it, as above), else `SYNAPSE_ENDPOINT` (Bun/Node;
|
|
33
|
+
* the runtime sets it for every task it starts); neither (or a browser) is `invalid_input` and the node stays `idle`.
|
|
34
|
+
*
|
|
35
|
+
* The outcome is handled: `node.connect(url);` without a handler is safe (`useConnection()` shows `failed`); `await` still rejects.
|
|
36
|
+
*/
|
|
37
|
+
connect(target?: ConnectTarget | string): Promise<this>;
|
|
38
|
+
/**
|
|
39
|
+
* Disconnects: the session, if any, is torn down and the node rests `idle` (no target), not ended. Every handle is kept,
|
|
40
|
+
* exactly as between two sessions, and a later `connect()` dials again. A `connect()` still pursuing is superseded (it
|
|
41
|
+
* resolves quietly). Resolves once the session is gone; already `idle`: at once. `not_connected` once the node has ended.
|
|
42
|
+
*/
|
|
43
|
+
disconnect(): Promise<void>;
|
|
44
|
+
/** The node's engine (the SDK's own modules and tests use it). */
|
|
16
45
|
get node(): Node;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
watchStatus(cb: (s: ConnectionState) => void): () => void;
|
|
21
|
-
/** Resolves on the next connected session (at once if connected); rejects once the node ended. */
|
|
22
|
-
whenConnected(): Promise<void>;
|
|
23
|
-
/** Declares a new target (broker, identity); resolves once connected to it (or rejects with why not). `null` disconnects for good. */
|
|
24
|
-
setTarget(target: ConnectTarget | null): Promise<void>;
|
|
25
|
-
/** Tells the runtime we are leaving, ends every handle, and resolves once the node has ended. */
|
|
26
|
-
/** During the first `connect()` it ends that attempt: the node ends `closed` and `connect()` rejects `not_connected`. */
|
|
46
|
+
/** Where the node stands (`ConnectionState`), a state value: `get()` now, `onChange` for each change, `changed()` for the next one, `watch()` for each one in a loop. */
|
|
47
|
+
readonly status: StateValue<ConnectionState>;
|
|
48
|
+
/** Tells the runtime we are leaving, ends every handle, and resolves once the node has ended (`closed`, for good). A `connect()` in flight rejects `not_connected`. */
|
|
27
49
|
close(): Promise<void>;
|
|
28
50
|
[Symbol.asyncDispose](): Promise<void>;
|
|
29
|
-
/** Resolves once the node has ended (closed, failed, or the policy gave up). */
|
|
30
|
-
get ended(): Promise<void>;
|
|
31
51
|
info(): Info;
|
|
32
52
|
metrics(): Metrics;
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
/** `promoted`/`demoted` notices (WP §3.5). */
|
|
36
|
-
onProjectNotice(cb: (n: Notice) => void): () => void;
|
|
53
|
+
/** Notices addressed to this node: `cb` per notice (an error body on `res` without a call, and a refused live override claim of this node's scene); returns the unsubscribe. */
|
|
54
|
+
notices(cb: (n: ErrorBody) => void): () => void;
|
|
37
55
|
nowUs(): number;
|
|
38
|
-
clock(): ClockInfo;
|
|
39
|
-
timeOffsets(): TimeOffsets;
|
|
40
|
-
/** A publisher stamp in our clock (WP §7.4), when the offsets allow it. */
|
|
41
|
-
toLocalUs(tUs: number, dom: string): LocalTime | undefined;
|
|
42
56
|
/** How old a message is now, in our clock. */
|
|
43
57
|
ageUs(env: {
|
|
44
58
|
tUs: number;
|
|
@@ -47,13 +61,20 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
47
61
|
/** A publisher of `topic`; resolves once the runtime admitted it on the current session (a refusal is asked again, paced, warned). */
|
|
48
62
|
publisher<TIn, TOut>(topic: TopicContract<TIn, TOut>): Promise<Publisher<TIn>>;
|
|
49
63
|
publisher<TIn = unknown>(topic: string): Promise<Publisher<TIn>>;
|
|
50
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* A subscription to `topic` (every publisher the catalog lists but ourselves), the stream: read it with `next()` or
|
|
66
|
+
* `for await`. Its follow task starts now. With `cb`, the callback form: the SDK runs the read loop and calls `cb` with
|
|
67
|
+
* each batch `next()` returns (a throwing `cb` is rethrown later; the loop goes on); the same subscription comes back
|
|
68
|
+
* (`unsubscribe()` ends it, `stats` counts it).
|
|
69
|
+
*/
|
|
51
70
|
subscribe<TIn, TOut>(topic: TopicContract<TIn, TOut>, opts?: SubscribeOptions): Subscription<TOut>;
|
|
71
|
+
subscribe<TIn, TOut>(topic: TopicContract<TIn, TOut>, cb: (batch: Envelope<TOut>[]) => void, opts?: SubscribeOptions): Subscription<TOut>;
|
|
52
72
|
subscribe<TOut = unknown>(topic: string, opts?: SubscribeOptions): Subscription<TOut>;
|
|
53
|
-
|
|
73
|
+
subscribe<TOut = unknown>(topic: string, cb: (batch: Envelope<TOut>[]) => void, opts?: SubscribeOptions): Subscription<TOut>;
|
|
74
|
+
/** The topic's latest value as a view (SDK §4.3); subscribed while someone listens. */
|
|
54
75
|
view<TIn, TOut, S = TOut>(topic: TopicContract<TIn, TOut>, opts?: ViewOptions<TOut, S>): View<S>;
|
|
55
76
|
view<TOut = unknown, S = TOut>(topic: string, opts?: ViewOptions<TOut, S>): View<S>;
|
|
56
|
-
/** Starts a call on the current session (SDK §5.2): `await` it for the result, or read `updates()` first. `not_connected
|
|
77
|
+
/** Starts a call on the current session (SDK §5.2): `await` it for the result, or read `updates()` first. With no session (before the first, between two) the Run is already failed `not_connected`: nothing was sent. */
|
|
57
78
|
call<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): Run<U, R>;
|
|
58
79
|
call<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): Run<U, R>;
|
|
59
80
|
/** The call's updates and result as one ordered stream; `break` cancels. */
|
|
@@ -64,26 +85,32 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
64
85
|
serve<I = unknown, U = unknown, R = unknown>(svc: string, handler: ServeHandler<I, U, R>, opts?: ServeOptions): Promise<Registration>;
|
|
65
86
|
/** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
|
|
66
87
|
store(name: string, opts?: StoreOptions): Promise<StoreHandle>;
|
|
67
|
-
/** The runtime's `config` namespace (operators write it). */
|
|
68
|
-
config(opts?: StoreOptions): Promise<StoreHandle>;
|
|
69
88
|
/** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
|
|
70
89
|
scene(): Promise<SceneHandle>;
|
|
71
|
-
/** The runtime's catalogs as the current session last read them
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
/** The runtime's catalogs as the current session last read them (empty between sessions), a state value: `get()` now, `onChange` for each change, `changed()` for the next one, `watch()` for each one in a loop. */
|
|
91
|
+
readonly catalog: StateValue<Catalog>;
|
|
92
|
+
/** The project state the current session last read (WP §3.5), a state value; `get()` is `undefined` between sessions or before the first snapshot. */
|
|
93
|
+
readonly project: StateValue<ProjectSnapshot | undefined>;
|
|
94
|
+
/**
|
|
95
|
+
* `synapse/project/activate {id}`: activates `id` when no project is active, or joins it when it is the active one;
|
|
96
|
+
* `project_busy` while another project is active (no takeover: deactivate it first). A web node is promoted to
|
|
97
|
+
* operator and resolves once the session with the new role is connected; an hmi stays `hmi` and resolves on the answer.
|
|
98
|
+
*/
|
|
79
99
|
activateProject(id: string): Promise<ProjectSnapshot>;
|
|
80
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* `synapse/project/deactivate {}`: any operator or hmi may deactivate, except a project an hmi activated
|
|
102
|
+
* (`active.activated_by_hmi`), which only an hmi may (`not_operator`). Every web operator is demoted; when that
|
|
103
|
+
* demotes this node, resolves once the demoted session is connected. An hmi is never demoted.
|
|
104
|
+
*/
|
|
81
105
|
deactivateProject(): Promise<ProjectSnapshot>;
|
|
82
|
-
/** `synapse/project/join {as: "operator"}`:
|
|
106
|
+
/** `synapse/project/join {as: "operator"}`: an operator of the active project, or `project_inactive`. A web node resolves once the promoted session is connected; an hmi on the answer. */
|
|
83
107
|
joinProject(as?: "operator"): Promise<ProjectSnapshot>;
|
|
84
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* `synapse/project/leave {}`: drops our standing. For an hmi membership is per session: this session only (it stays
|
|
110
|
+
* `hmi`, never closed). For a web node it removes our `user_id`'s web sessions only; when that demotes this node,
|
|
111
|
+
* resolves once the demoted session is connected.
|
|
112
|
+
*/
|
|
85
113
|
leaveProject(): Promise<ProjectSnapshot>;
|
|
86
|
-
getProject(): Promise<ProjectSnapshot>;
|
|
87
114
|
/**
|
|
88
115
|
* The robot's task declarations, what runs (`status`), what the runtime can run (`catalog`), and the verbs; the same
|
|
89
116
|
* object for the node's life. Each handle opens on its first touch and is the node's own (`node.close()` ends it).
|