@synapse-sdk/client 0.3.0 → 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node.d.ts +62 -24
- package/dist/node.js +208 -68
- 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 +74 -11
- package/dist/session.js +279 -59
- package/dist/store.d.ts +31 -10
- package/dist/store.js +140 -28
- package/dist/synapse.d.ts +77 -46
- package/dist/synapse.js +193 -114
- package/dist/tasks.d.ts +37 -0
- package/dist/tasks.js +146 -0
- package/dist/topics.d.ts +23 -6
- package/dist/topics.js +59 -22
- package/dist/types.d.ts +405 -93
- 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/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,30 +125,42 @@ 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
|
-
|
|
148
|
+
ready(): Promise<void>;
|
|
137
149
|
close(): void;
|
|
138
150
|
[Symbol.asyncDispose](): Promise<void>;
|
|
139
151
|
}
|
|
140
152
|
/**
|
|
141
153
|
* `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
|
|
142
|
-
* 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.
|
|
143
156
|
*/
|
|
144
157
|
export declare function openStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): Promise<StoreHandleImpl>;
|
|
158
|
+
/**
|
|
159
|
+
* The synchronous form of `openStore`, for a handle the node itself holds (`node.tasks`): a handle on
|
|
160
|
+
* the namespace's task, joined if one runs, else started now; the value is in once `ready()`
|
|
161
|
+
* resolves. A task that is ending (its last handle closed) is waited out before the new one starts:
|
|
162
|
+
* the new one is in the registry at once (a `store()` meanwhile joins it), its requests wait in its
|
|
163
|
+
* mailbox.
|
|
164
|
+
*/
|
|
165
|
+
export declare function attachStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): StoreHandleImpl;
|
|
145
166
|
export {};
|
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
|
-
import { FLAGS,
|
|
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;
|
|
@@ -224,7 +226,8 @@ export function startNamespace(ns, registry) { void run(ns, registry); }
|
|
|
224
226
|
async function run(ns, registry) {
|
|
225
227
|
const { name, opts, state, warn, node } = ns;
|
|
226
228
|
const post = (e) => { ns.post(e); };
|
|
227
|
-
|
|
229
|
+
/** A robot-scoped namespace belongs to the robot: a project switch leaves it as it is. */
|
|
230
|
+
const robotScoped = storeRobotScoped(name);
|
|
228
231
|
let session;
|
|
229
232
|
let phase = { kind: "idle" };
|
|
230
233
|
const ph = () => phase;
|
|
@@ -238,6 +241,9 @@ async function run(ns, registry) {
|
|
|
238
241
|
let unwatchProject;
|
|
239
242
|
const joining = [];
|
|
240
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";
|
|
241
247
|
const leaveIncarnation = () => { for (const w of versionWaiters.splice(0))
|
|
242
248
|
w.reply.reject(gone(name, `the handle left the incarnation before version ${w.version}`)); };
|
|
243
249
|
const stopPhase = () => {
|
|
@@ -310,7 +316,7 @@ async function run(ns, registry) {
|
|
|
310
316
|
state.reset();
|
|
311
317
|
unwatchProject?.();
|
|
312
318
|
unwatchProject = undefined;
|
|
313
|
-
if (s && !
|
|
319
|
+
if (s && !robotScoped) {
|
|
314
320
|
const feed = s.project;
|
|
315
321
|
const current = feed.get();
|
|
316
322
|
if (known === undefined)
|
|
@@ -357,9 +363,12 @@ async function run(ns, registry) {
|
|
|
357
363
|
const done = follow(ns, s, announced, access?.epoch, stop.signal);
|
|
358
364
|
phase = { kind: "following", attempt, stop, done };
|
|
359
365
|
void done.then(reason => post({ kind: "followEnded", attempt, reason }));
|
|
360
|
-
|
|
366
|
+
// Absent: honestly in (nothing to load), and nothing to write to (the parked writes are refused).
|
|
367
|
+
if (!access) {
|
|
361
368
|
for (const j of joining.splice(0))
|
|
362
369
|
j.resolve();
|
|
370
|
+
refuseParked(ABSENT);
|
|
371
|
+
}
|
|
363
372
|
return;
|
|
364
373
|
}
|
|
365
374
|
announced.close();
|
|
@@ -388,6 +397,9 @@ async function run(ns, registry) {
|
|
|
388
397
|
if (state.loaded && state.epoch === epoch && state.version >= wanted)
|
|
389
398
|
for (const j of joining.splice(0))
|
|
390
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();
|
|
391
403
|
};
|
|
392
404
|
const join = (reply) => {
|
|
393
405
|
if (phase.kind === "waitingProject") {
|
|
@@ -425,14 +437,53 @@ async function run(ns, registry) {
|
|
|
425
437
|
again(REFOLLOW_PACE_MS);
|
|
426
438
|
}
|
|
427
439
|
};
|
|
428
|
-
|
|
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) => {
|
|
429
446
|
if (state.ended !== undefined) {
|
|
430
447
|
reply.reject(gone(name, `the handle ended (${state.ended})`));
|
|
431
448
|
return;
|
|
432
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) => {
|
|
433
484
|
const epoch = state.epoch;
|
|
434
485
|
if (epoch === undefined) {
|
|
435
|
-
reply.reject(
|
|
486
|
+
reply.reject(gone(name, "no incarnation is bound"));
|
|
436
487
|
return;
|
|
437
488
|
}
|
|
438
489
|
if (!session) {
|
|
@@ -533,7 +584,10 @@ async function run(ns, registry) {
|
|
|
533
584
|
waitVersion(ev.epoch, ev.version, ev.reply);
|
|
534
585
|
break;
|
|
535
586
|
case "write":
|
|
536
|
-
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);
|
|
537
591
|
break;
|
|
538
592
|
case "writeDone":
|
|
539
593
|
if (writeDone(ev.op, ev.result, ev.reply)) {
|
|
@@ -542,7 +596,7 @@ async function run(ns, registry) {
|
|
|
542
596
|
}
|
|
543
597
|
break;
|
|
544
598
|
case "projectChanged":
|
|
545
|
-
if (!
|
|
599
|
+
if (!robotScoped)
|
|
546
600
|
projectChanged(ev.now);
|
|
547
601
|
break;
|
|
548
602
|
case "paced":
|
|
@@ -566,6 +620,7 @@ async function run(ns, registry) {
|
|
|
566
620
|
for (const j of joining.splice(0))
|
|
567
621
|
j.reject(e);
|
|
568
622
|
leaveIncarnation();
|
|
623
|
+
refuseParked(`the handle ended (${state.ended ?? reason ?? ENDED_CLIENT})`);
|
|
569
624
|
{
|
|
570
625
|
const p = ph();
|
|
571
626
|
if (p.kind === "following" || p.kind === "paced")
|
|
@@ -579,7 +634,7 @@ async function run(ns, registry) {
|
|
|
579
634
|
if (ev.kind === "writeDone")
|
|
580
635
|
writeDone(ev.op, ev.result, ev.reply);
|
|
581
636
|
else if (ev.kind === "write")
|
|
582
|
-
write(ev.op, ev.args, ev.reply);
|
|
637
|
+
write(ev.handle, ev.op, ev.args, ev.reply);
|
|
583
638
|
else if (ev.kind === "opened") {
|
|
584
639
|
const p = ph();
|
|
585
640
|
if (p.kind === "opening" && p.o.attempt === ev.attempt) {
|
|
@@ -770,31 +825,39 @@ async function follow(ns, session, announced, want, signal) {
|
|
|
770
825
|
export class StoreHandleImpl {
|
|
771
826
|
ns;
|
|
772
827
|
#closed = false;
|
|
828
|
+
/** This handle's id in its namespace: its parked writes are refused when it closes. */
|
|
829
|
+
#id;
|
|
773
830
|
/** Resolved when this handle closes: its version waits end. */
|
|
774
831
|
#left = deferred();
|
|
775
832
|
/** This handle's change feeds: closed with it. */
|
|
776
833
|
#feeds = new Set();
|
|
777
834
|
#valueObj;
|
|
778
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;
|
|
779
841
|
constructor(ns) {
|
|
780
842
|
this.ns = ns;
|
|
781
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);
|
|
782
846
|
}
|
|
783
847
|
#ended() { return gone(this.ns.name, `the handle ended (${ENDED_CLIENT})`); }
|
|
784
848
|
get name() { return this.ns.name; }
|
|
785
|
-
|
|
786
|
-
get value() {
|
|
849
|
+
get(key) {
|
|
787
850
|
const flat = this.ns.state.flat;
|
|
851
|
+
if (key !== undefined)
|
|
852
|
+
return flat.get(key);
|
|
788
853
|
if (this.#valueOf !== flat) {
|
|
789
854
|
this.#valueOf = flat;
|
|
790
855
|
this.#valueObj = Object.fromEntries(flat);
|
|
791
856
|
}
|
|
792
857
|
return this.#valueObj;
|
|
793
858
|
}
|
|
794
|
-
get(
|
|
795
|
-
|
|
796
|
-
get status() { return this.ns.state.status(this.ns.node.isConnected, this.#closed); }
|
|
797
|
-
/** `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. */
|
|
798
861
|
async set(keyOrValues, value) {
|
|
799
862
|
const v = typeof keyOrValues === "string" ? await this.#write("put", { key: keyOrValues, value }) : await this.#write("put_many", { values: keyOrValues });
|
|
800
863
|
await this.#reached(v);
|
|
@@ -802,11 +865,11 @@ export class StoreHandleImpl {
|
|
|
802
865
|
async delete(key) { await this.#reached(await this.#write("delete", { key })); }
|
|
803
866
|
async clear() { await this.#reached(await this.#write("clear", {})); }
|
|
804
867
|
async destroy() { await this.#write("destroy", {}); }
|
|
805
|
-
/** 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). */
|
|
806
869
|
async #write(op, args) {
|
|
807
870
|
if (this.#closed)
|
|
808
871
|
throw this.#ended();
|
|
809
|
-
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 }));
|
|
810
873
|
const v = r?.version;
|
|
811
874
|
return typeof v === "number" ? v : typeof v === "bigint" ? Number(v) : 0;
|
|
812
875
|
}
|
|
@@ -829,7 +892,7 @@ export class StoreHandleImpl {
|
|
|
829
892
|
try {
|
|
830
893
|
for await (const batch of q) {
|
|
831
894
|
if (key === undefined)
|
|
832
|
-
yield this.
|
|
895
|
+
yield this.get();
|
|
833
896
|
else if (batch.some(c => c.key === key))
|
|
834
897
|
yield this.ns.state.flat.get(key);
|
|
835
898
|
}
|
|
@@ -839,10 +902,25 @@ export class StoreHandleImpl {
|
|
|
839
902
|
this.ns.state.unwatch(q);
|
|
840
903
|
}
|
|
841
904
|
}
|
|
842
|
-
/** The
|
|
843
|
-
|
|
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())); }
|
|
844
922
|
/** Waits for the namespace to be in (the first snapshot applied, or absent). */
|
|
845
|
-
|
|
923
|
+
ready() { return this.ns.request(reply => ({ kind: "join", reply })); }
|
|
846
924
|
close() {
|
|
847
925
|
if (this.#closed)
|
|
848
926
|
return;
|
|
@@ -851,18 +929,29 @@ export class StoreHandleImpl {
|
|
|
851
929
|
for (const q of this.#feeds)
|
|
852
930
|
this.ns.state.unwatch(q);
|
|
853
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();
|
|
854
942
|
if (--this.ns.handles === 0)
|
|
855
943
|
this.ns.post({ kind: "close" });
|
|
944
|
+
else
|
|
945
|
+
this.ns.post({ kind: "leave", handle: this.#id });
|
|
856
946
|
}
|
|
857
947
|
async [Symbol.asyncDispose]() { this.close(); }
|
|
858
948
|
}
|
|
859
949
|
/**
|
|
860
950
|
* `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
|
|
861
|
-
* 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.
|
|
862
953
|
*/
|
|
863
954
|
export async function openStore(node, registry, name, opts, warn) {
|
|
864
|
-
if (!node.currentSession())
|
|
865
|
-
throw new SynapseError("not_connected");
|
|
866
955
|
for (;;) {
|
|
867
956
|
const live = registry.get(name);
|
|
868
957
|
if (live) {
|
|
@@ -874,7 +963,7 @@ export async function openStore(node, registry, name, opts, warn) {
|
|
|
874
963
|
throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
|
|
875
964
|
const h = new StoreHandleImpl(live);
|
|
876
965
|
try {
|
|
877
|
-
await h.
|
|
966
|
+
await h.ready();
|
|
878
967
|
}
|
|
879
968
|
catch (e) {
|
|
880
969
|
h.close();
|
|
@@ -887,7 +976,7 @@ export async function openStore(node, registry, name, opts, warn) {
|
|
|
887
976
|
const h = new StoreHandleImpl(ns);
|
|
888
977
|
startNamespace(ns, registry);
|
|
889
978
|
try {
|
|
890
|
-
await h.
|
|
979
|
+
await h.ready();
|
|
891
980
|
}
|
|
892
981
|
catch (e) {
|
|
893
982
|
h.close();
|
|
@@ -896,3 +985,26 @@ export async function openStore(node, registry, name, opts, warn) {
|
|
|
896
985
|
return h;
|
|
897
986
|
}
|
|
898
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* The synchronous form of `openStore`, for a handle the node itself holds (`node.tasks`): a handle on
|
|
990
|
+
* the namespace's task, joined if one runs, else started now; the value is in once `ready()`
|
|
991
|
+
* resolves. A task that is ending (its last handle closed) is waited out before the new one starts:
|
|
992
|
+
* the new one is in the registry at once (a `store()` meanwhile joins it), its requests wait in its
|
|
993
|
+
* mailbox.
|
|
994
|
+
*/
|
|
995
|
+
export function attachStore(node, registry, name, opts, warn) {
|
|
996
|
+
const live = registry.get(name);
|
|
997
|
+
if (live && live.handles > 0) {
|
|
998
|
+
if (live.opts.access !== opts.access || live.opts.onDeath !== opts.onDeath)
|
|
999
|
+
throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
|
|
1000
|
+
return new StoreHandleImpl(live);
|
|
1001
|
+
}
|
|
1002
|
+
const ns = new Namespace(name, opts, node, warn);
|
|
1003
|
+
registry.set(name, ns);
|
|
1004
|
+
const h = new StoreHandleImpl(ns);
|
|
1005
|
+
if (live)
|
|
1006
|
+
void live.exited.promise.then(() => startNamespace(ns, registry));
|
|
1007
|
+
else
|
|
1008
|
+
startNamespace(ns, registry);
|
|
1009
|
+
return h;
|
|
1010
|
+
}
|
package/dist/synapse.d.ts
CHANGED
|
@@ -1,45 +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
|
-
readonly opts: ConnectOptions;
|
|
11
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
|
+
*/
|
|
12
16
|
constructor(opts: ConnectOptions);
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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). */
|
|
17
45
|
get node(): Node;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
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`. */
|
|
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`. */
|
|
28
49
|
close(): Promise<void>;
|
|
29
50
|
[Symbol.asyncDispose](): Promise<void>;
|
|
30
|
-
/** Resolves once the node has ended (closed, failed, or the policy gave up). */
|
|
31
|
-
get ended(): Promise<void>;
|
|
32
51
|
info(): Info;
|
|
33
52
|
metrics(): Metrics;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/** `promoted`/`demoted` notices (WP §3.5). */
|
|
37
|
-
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;
|
|
38
55
|
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
56
|
/** How old a message is now, in our clock. */
|
|
44
57
|
ageUs(env: {
|
|
45
58
|
tUs: number;
|
|
@@ -48,13 +61,20 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
48
61
|
/** A publisher of `topic`; resolves once the runtime admitted it on the current session (a refusal is asked again, paced, warned). */
|
|
49
62
|
publisher<TIn, TOut>(topic: TopicContract<TIn, TOut>): Promise<Publisher<TIn>>;
|
|
50
63
|
publisher<TIn = unknown>(topic: string): Promise<Publisher<TIn>>;
|
|
51
|
-
/**
|
|
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
|
+
*/
|
|
52
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>;
|
|
53
72
|
subscribe<TOut = unknown>(topic: string, opts?: SubscribeOptions): Subscription<TOut>;
|
|
54
|
-
|
|
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. */
|
|
55
75
|
view<TIn, TOut, S = TOut>(topic: TopicContract<TIn, TOut>, opts?: ViewOptions<TOut, S>): View<S>;
|
|
56
76
|
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
|
|
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. */
|
|
58
78
|
call<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): Run<U, R>;
|
|
59
79
|
call<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): Run<U, R>;
|
|
60
80
|
/** The call's updates and result as one ordered stream; `break` cancels. */
|
|
@@ -65,25 +85,36 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
65
85
|
serve<I = unknown, U = unknown, R = unknown>(svc: string, handler: ServeHandler<I, U, R>, opts?: ServeOptions): Promise<Registration>;
|
|
66
86
|
/** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
|
|
67
87
|
store(name: string, opts?: StoreOptions): Promise<StoreHandle>;
|
|
68
|
-
/** The runtime's `config` namespace (operators write it). */
|
|
69
|
-
config(opts?: StoreOptions): Promise<StoreHandle>;
|
|
70
88
|
/** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
|
|
71
89
|
scene(): Promise<SceneHandle>;
|
|
72
|
-
/** The runtime's catalogs as the current session last read them
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
*/
|
|
80
99
|
activateProject(id: string): Promise<ProjectSnapshot>;
|
|
81
|
-
/**
|
|
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
|
+
*/
|
|
82
105
|
deactivateProject(): Promise<ProjectSnapshot>;
|
|
83
|
-
/** `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. */
|
|
84
107
|
joinProject(as?: "operator"): Promise<ProjectSnapshot>;
|
|
85
|
-
/**
|
|
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
|
+
*/
|
|
86
113
|
leaveProject(): Promise<ProjectSnapshot>;
|
|
87
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The robot's task declarations, what runs (`status`), what the runtime can run (`catalog`), and the verbs; the same
|
|
116
|
+
* object for the node's life. Each handle opens on its first touch and is the node's own (`node.close()` ends it).
|
|
117
|
+
*/
|
|
118
|
+
get tasks(): Tasks;
|
|
88
119
|
}
|
|
89
120
|
export type { Envelope, StoreHandleImpl };
|