@synapse-sdk/client 0.3.4 → 0.3.7
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 +5 -5
- package/dist/calls.js +19 -21
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/keyserver.d.ts +37 -0
- package/dist/keyserver.js +146 -0
- package/dist/node.d.ts +24 -2
- package/dist/node.js +65 -5
- package/dist/scene.d.ts +22 -11
- package/dist/scene.js +297 -218
- package/dist/session.d.ts +12 -2
- package/dist/session.js +39 -14
- package/dist/store.d.ts +12 -4
- package/dist/store.js +99 -69
- package/dist/synapse.d.ts +29 -7
- package/dist/synapse.js +44 -11
- package/dist/target.d.ts +113 -0
- package/dist/target.js +377 -0
- package/dist/types.d.ts +84 -25
- package/package.json +4 -3
package/dist/session.d.ts
CHANGED
|
@@ -89,14 +89,24 @@ export declare function describeError(e: unknown): string;
|
|
|
89
89
|
* GOAWAY looks like one).
|
|
90
90
|
*/
|
|
91
91
|
export declare function closeError(err: Error | null): SynapseError;
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* `https://host[:port]` → `http://host:port/certificate.sha256`, the runtime's plain-HTTP helper on the
|
|
94
|
+
* runtime's own port (443 when the URL names none, as the Rust SDK). Before 2026-10-17 the default
|
|
95
|
+
* port was lost and a relay URL was asked on port 80, which no relay answers: a 2 s stall per attempt.
|
|
96
|
+
*/
|
|
93
97
|
export declare function brokerCertHelperUrl(brokerUrl: string): string;
|
|
98
|
+
/** Whether `url` names a root (`/runtimes/<id>`, `/cluster/<c>/runtimes/<id>`): a relay, a deployment with a real certificate, never asked for a pin (`withRoot` makes the same split). */
|
|
99
|
+
export declare function namesRoot(url: string): boolean;
|
|
94
100
|
/**
|
|
95
101
|
* The runtime's URL when the caller gave none: `SYNAPSE_ENDPOINT` (Bun/Node; the runtime sets it for every task it starts).
|
|
96
102
|
* A browser has no environment: there, as with neither, `invalid_input`.
|
|
97
103
|
*/
|
|
98
104
|
export declare function urlOrEndpoint(url: string | undefined): string;
|
|
99
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* The certificate pin when the caller gave none (SDK §13): `SYNAPSE_BROKER_CERT_SHA256` (Bun/Node); else, for a
|
|
107
|
+
* runtime URL (no path: a self-signed certificate, pinned), its helper; a URL that names a root (a relay) is
|
|
108
|
+
* never asked and connects unpinned, against the system's roots. `undefined` = unpinned. Per attempt.
|
|
109
|
+
*/
|
|
100
110
|
export declare function discoverBrokerCertSha256(brokerUrl: string, opts?: {
|
|
101
111
|
timeoutMs?: number;
|
|
102
112
|
signal?: AbortSignal;
|
package/dist/session.js
CHANGED
|
@@ -332,8 +332,20 @@ export function closeError(err) {
|
|
|
332
332
|
return new SynapseError("unauthorized", { message: text.trim(), source: "broker", cause: err });
|
|
333
333
|
return new SynapseError("connection_lost", { message: text.trim() || err.name, source: "transport", cause: err });
|
|
334
334
|
}
|
|
335
|
-
/**
|
|
336
|
-
|
|
335
|
+
/**
|
|
336
|
+
* `https://host[:port]` → `http://host:port/certificate.sha256`, the runtime's plain-HTTP helper on the
|
|
337
|
+
* runtime's own port (443 when the URL names none, as the Rust SDK). Before 2026-10-17 the default
|
|
338
|
+
* port was lost and a relay URL was asked on port 80, which no relay answers: a 2 s stall per attempt.
|
|
339
|
+
*/
|
|
340
|
+
export function brokerCertHelperUrl(brokerUrl) { const u = new URL(brokerUrl); return `http://${u.hostname}:${u.port || (u.protocol === "http:" ? "80" : "443")}/certificate.sha256`; }
|
|
341
|
+
/** Whether `url` names a root (`/runtimes/<id>`, `/cluster/<c>/runtimes/<id>`): a relay, a deployment with a real certificate, never asked for a pin (`withRoot` makes the same split). */
|
|
342
|
+
export function namesRoot(url) { try {
|
|
343
|
+
const p = new URL(url).pathname;
|
|
344
|
+
return p !== "" && p !== "/";
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return false;
|
|
348
|
+
} }
|
|
337
349
|
/**
|
|
338
350
|
* The runtime's URL when the caller gave none: `SYNAPSE_ENDPOINT` (Bun/Node; the runtime sets it for every task it starts).
|
|
339
351
|
* A browser has no environment: there, as with neither, `invalid_input`.
|
|
@@ -347,22 +359,35 @@ export function urlOrEndpoint(url) {
|
|
|
347
359
|
throw new SynapseError("invalid_input", { message: "no url given and SYNAPSE_ENDPOINT is not set", source: "sdk" });
|
|
348
360
|
}
|
|
349
361
|
const SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
350
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* The certificate pin when the caller gave none (SDK §13): `SYNAPSE_BROKER_CERT_SHA256` (Bun/Node); else, for a
|
|
364
|
+
* runtime URL (no path: a self-signed certificate, pinned), its helper; a URL that names a root (a relay) is
|
|
365
|
+
* never asked and connects unpinned, against the system's roots. `undefined` = unpinned. Per attempt.
|
|
366
|
+
*/
|
|
351
367
|
export async function discoverBrokerCertSha256(brokerUrl, opts = {}) {
|
|
352
368
|
const env = globalThis.process?.env?.SYNAPSE_BROKER_CERT_SHA256?.trim();
|
|
353
369
|
if (env)
|
|
354
370
|
return env;
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
const res = await fetch(brokerCertHelperUrl(brokerUrl), { signal });
|
|
358
|
-
if (!res.ok)
|
|
359
|
-
return undefined;
|
|
360
|
-
const text = (await res.text()).trim();
|
|
361
|
-
return SHA256_HEX.test(text) ? text : undefined;
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
371
|
+
if (namesRoot(brokerUrl))
|
|
364
372
|
return undefined;
|
|
365
|
-
|
|
373
|
+
const timeoutMs = opts.timeoutMs ?? 2000;
|
|
374
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
|
|
375
|
+
const fetched = (async () => {
|
|
376
|
+
try {
|
|
377
|
+
const res = await fetch(brokerCertHelperUrl(brokerUrl), { signal });
|
|
378
|
+
if (!res.ok)
|
|
379
|
+
return undefined;
|
|
380
|
+
const text = (await res.text()).trim();
|
|
381
|
+
return SHA256_HEX.test(text) ? text : undefined;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
})();
|
|
387
|
+
// Bun does not abort a connect that gets no answer (measured 2026-10-17: a filtered port hung the dial for
|
|
388
|
+
// good); the race bounds the wait whatever the runtime does with the signal.
|
|
389
|
+
const r = await withTimeout(fetched, timeoutMs);
|
|
390
|
+
return r === TIMEOUT ? undefined : r;
|
|
366
391
|
}
|
|
367
392
|
/**
|
|
368
393
|
* `token_from_url`: `url` without its `jwt=` query parameters, and the last non-empty one (as the relays
|
|
@@ -429,7 +454,7 @@ export async function withRoot(url, signal) {
|
|
|
429
454
|
throw timeout.aborted ? new SynapseError("connection_lost", { message: `root helper at ${addr} did not answer`, source: "transport" }) : lost(describeError(e));
|
|
430
455
|
}
|
|
431
456
|
if (!isRuntimeRoot(root)) {
|
|
432
|
-
throw new SynapseError("invalid_input", { message: `${url} names no runtime (its /root helper returned ${JSON.stringify(root.slice(0, 80))}); give the full path, e.g. https
|
|
457
|
+
throw new SynapseError("invalid_input", { message: `${url} names no runtime (its /root helper returned ${JSON.stringify(root.slice(0, 80))}); give the full path, e.g. https://<relay>/cluster/<cluster>/runtimes/<runtime_id>`, source: "sdk" });
|
|
433
458
|
}
|
|
434
459
|
u.pathname = `/${root}`;
|
|
435
460
|
return u.toString();
|
package/dist/store.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const ENDED_CONNECTION_LOST = "connection_lost";
|
|
|
9
9
|
export declare const ENDED_CLIENT = "client";
|
|
10
10
|
/** A watcher this many batches behind is ended, never grown. */
|
|
11
11
|
export declare const STORE_FEED_CAP = 256;
|
|
12
|
+
/** How long an incarnation the open's reply named has to show up on the announcement cursor (no "live" marker in @moq/net's JS cursor). */
|
|
13
|
+
export declare const ANNOUNCE_WITHIN_MS = 1000;
|
|
12
14
|
/** The mirror and what the runtime said about the namespace; written by the task and its reader only. */
|
|
13
15
|
export declare class State {
|
|
14
16
|
#private;
|
|
@@ -24,7 +26,8 @@ export declare class State {
|
|
|
24
26
|
onDeath: StoreOnDeath | undefined;
|
|
25
27
|
absent: boolean;
|
|
26
28
|
loaded: boolean;
|
|
27
|
-
|
|
29
|
+
/** A diff skipped a version (not applied; the track is read again from its snapshot): the highest version seen then. Cleared once the copy is back at it (PLAN v0.34 §2.2). */
|
|
30
|
+
gap: number | undefined;
|
|
28
31
|
pending: number;
|
|
29
32
|
ended: string | undefined;
|
|
30
33
|
subscribe(l: () => void): () => void;
|
|
@@ -32,7 +35,11 @@ export declare class State {
|
|
|
32
35
|
/** A feed its reader has left: it is closed and gets nothing more. */
|
|
33
36
|
unwatch(q: AsyncQueue<StoreChange[]>): void;
|
|
34
37
|
applySnapshot(s: StoreSnapshot): void;
|
|
35
|
-
|
|
38
|
+
/**
|
|
39
|
+
* One diff, applied when it is the next version. Otherwise it is not applied (`false`): a gap, `synced` false until the
|
|
40
|
+
* copy is back at the version seen; the reader reads the track again from its snapshot (PLAN v0.34 §2.2).
|
|
41
|
+
*/
|
|
42
|
+
applyDiff(d: StoreDiff, warn: Warn): boolean;
|
|
36
43
|
/** Nothing of the incarnation is known until the runtime answers the next open. */
|
|
37
44
|
reset(): void;
|
|
38
45
|
opened(a: StoreAccess | undefined): void;
|
|
@@ -140,11 +147,12 @@ export declare class StoreHandleImpl implements StoreHandle {
|
|
|
140
147
|
destroy(): Promise<void>;
|
|
141
148
|
/** `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. */
|
|
142
149
|
watch(key?: string): AsyncGenerator<unknown, void, undefined>;
|
|
143
|
-
/** The whole value after its next change (`store_gone` once this handle is closed
|
|
150
|
+
/** The whole value after its next change; `changed(key)`: that key's value after its next change (`undefined` after a delete), as `watch(key)` yields it. `store_gone` once this handle is closed. Armed at the call. */
|
|
144
151
|
changed(): Promise<Record<string, unknown>>;
|
|
152
|
+
changed(key: string): Promise<unknown>;
|
|
145
153
|
/** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
|
|
146
154
|
onChange(cb: (value: Record<string, unknown>) => void): () => void;
|
|
147
|
-
/** Waits for the namespace to be in (the
|
|
155
|
+
/** Waits for the namespace to be in: the value current as of the open (the version the runtime answered applied), or the namespace absent. */
|
|
148
156
|
ready(): Promise<void>;
|
|
149
157
|
close(): void;
|
|
150
158
|
[Symbol.asyncDispose](): Promise<void>;
|
package/dist/store.js
CHANGED
|
@@ -21,7 +21,7 @@ const ENDED_STORE_GONE = "store_gone";
|
|
|
21
21
|
/** A watcher this many batches behind is ended, never grown. */
|
|
22
22
|
export const STORE_FEED_CAP = 256;
|
|
23
23
|
/** How long an incarnation the open's reply named has to show up on the announcement cursor (no "live" marker in @moq/net's JS cursor). */
|
|
24
|
-
const ANNOUNCE_WITHIN_MS = 1000;
|
|
24
|
+
export const ANNOUNCE_WITHIN_MS = 1000;
|
|
25
25
|
const isFinal = (reason) => reason === ENDED_DESTROYED;
|
|
26
26
|
const gone = (name, why) => new SynapseError("store_gone", `store ${JSON.stringify(name)}: ${why}`);
|
|
27
27
|
// ------------------------------------------------------------------------------------- state
|
|
@@ -39,7 +39,8 @@ export class State {
|
|
|
39
39
|
onDeath;
|
|
40
40
|
absent = false;
|
|
41
41
|
loaded = false;
|
|
42
|
-
|
|
42
|
+
/** A diff skipped a version (not applied; the track is read again from its snapshot): the highest version seen then. Cleared once the copy is back at it (PLAN v0.34 §2.2). */
|
|
43
|
+
gap;
|
|
43
44
|
pending = 0;
|
|
44
45
|
ended;
|
|
45
46
|
#listeners = new Set();
|
|
@@ -92,13 +93,22 @@ export class State {
|
|
|
92
93
|
this.onDeath = s.on_death;
|
|
93
94
|
this.absent = false;
|
|
94
95
|
this.loaded = true;
|
|
95
|
-
this
|
|
96
|
+
this.#heal();
|
|
96
97
|
this.#emit(records);
|
|
97
98
|
}
|
|
99
|
+
/** A gap is over once the copy is back at the highest version seen when it opened. */
|
|
100
|
+
#heal() { if (this.gap !== undefined && this.version >= this.gap)
|
|
101
|
+
this.gap = undefined; }
|
|
102
|
+
/**
|
|
103
|
+
* One diff, applied when it is the next version. Otherwise it is not applied (`false`): a gap, `synced` false until the
|
|
104
|
+
* copy is back at the version seen; the reader reads the track again from its snapshot (PLAN v0.34 §2.2).
|
|
105
|
+
*/
|
|
98
106
|
applyDiff(d, warn) {
|
|
99
107
|
if (d.version !== this.version + 1) {
|
|
100
|
-
warn(`store: diff out of sequence (have ${this.version}, got ${d.version})
|
|
101
|
-
this.gap =
|
|
108
|
+
warn(`store: diff out of sequence (have ${this.version}, got ${d.version}): not applied; reading the track again from its snapshot`);
|
|
109
|
+
this.gap = Math.max(this.gap ?? d.version, d.version);
|
|
110
|
+
this.#notify();
|
|
111
|
+
return false;
|
|
102
112
|
}
|
|
103
113
|
const records = [];
|
|
104
114
|
const flat = new Map(this.flat);
|
|
@@ -119,7 +129,9 @@ export class State {
|
|
|
119
129
|
}
|
|
120
130
|
this.flat = flat;
|
|
121
131
|
this.version = d.version;
|
|
132
|
+
this.#heal();
|
|
122
133
|
this.#emit(records);
|
|
134
|
+
return true;
|
|
123
135
|
}
|
|
124
136
|
#clear() {
|
|
125
137
|
const records = [...this.rows].map(([key, r]) => ({ kind: "delete", key, previousValue: r.v, by: undefined }));
|
|
@@ -135,7 +147,7 @@ export class State {
|
|
|
135
147
|
this.epoch = undefined;
|
|
136
148
|
this.version = 0;
|
|
137
149
|
this.loaded = false;
|
|
138
|
-
this.gap =
|
|
150
|
+
this.gap = undefined;
|
|
139
151
|
this.mode = undefined;
|
|
140
152
|
this.owner = undefined;
|
|
141
153
|
this.mine = false;
|
|
@@ -151,7 +163,7 @@ export class State {
|
|
|
151
163
|
this.epoch = undefined;
|
|
152
164
|
this.version = 0;
|
|
153
165
|
this.loaded = false;
|
|
154
|
-
this.gap =
|
|
166
|
+
this.gap = undefined;
|
|
155
167
|
this.mode = a?.mode;
|
|
156
168
|
this.owner = a?.owner ?? undefined;
|
|
157
169
|
this.mine = a?.mine ?? false;
|
|
@@ -177,7 +189,7 @@ export class State {
|
|
|
177
189
|
status(nodeConnected, closed = false) {
|
|
178
190
|
const ended = closed ? ENDED_CLIENT : this.ended;
|
|
179
191
|
const connected = ended === undefined && nodeConnected;
|
|
180
|
-
return { synced: connected && this.pending === 0 &&
|
|
192
|
+
return { synced: connected && this.pending === 0 && this.gap === undefined && (this.loaded || this.absent), connected, pending: this.pending, mode: this.mode, owner: this.owner, mine: this.mine, minRole: this.minRole, onDeath: this.onDeath, absent: this.absent, version: this.version, epoch: this.epoch, ended };
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
195
|
function sameValue(a, b) {
|
|
@@ -686,7 +698,8 @@ function announcedEpoch(base, prefix) {
|
|
|
686
698
|
/**
|
|
687
699
|
* Reads the incarnation the open bound (`want`; `undefined`: absent, the first one announced), writing
|
|
688
700
|
* the mirror and telling the task of each frame applied; returns how the track ended (`undefined`:
|
|
689
|
-
* no final frame — the session, or the broker, went; the task replays).
|
|
701
|
+
* no final frame — the session, or the broker, went; the task replays). A diff out of sequence is not applied: the
|
|
702
|
+
* subscription is let go and the track subscribed again, which delivers the open group from its snapshot (PLAN v0.34 §2.2).
|
|
690
703
|
*/
|
|
691
704
|
async function follow(ns, session, announced, want, signal) {
|
|
692
705
|
const { state, warn, name } = ns;
|
|
@@ -739,74 +752,86 @@ async function follow(ns, session, announced, want, signal) {
|
|
|
739
752
|
return undefined;
|
|
740
753
|
}
|
|
741
754
|
announced.close();
|
|
742
|
-
|
|
743
|
-
const s = sub;
|
|
755
|
+
const track = bc.track(STORE_TRACK);
|
|
744
756
|
// Closing the subscriber ends `recvGroup`, not a read on the group it already handed us: that group is closed too.
|
|
745
757
|
let g;
|
|
746
|
-
signal.addEventListener("abort", () => {
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (
|
|
751
|
-
|
|
758
|
+
signal.addEventListener("abort", () => { sub?.close(); g?.close(); }, { once: true });
|
|
759
|
+
// A gap: this subscription is let go and the track subscribed again, which delivers the open group from its
|
|
760
|
+
// snapshot (PLAN v0.34 §2.2; no counter, no timer).
|
|
761
|
+
subscribe: for (;;) {
|
|
762
|
+
if (signal.aborted)
|
|
763
|
+
return undefined;
|
|
764
|
+
try {
|
|
765
|
+
sub?.close();
|
|
766
|
+
}
|
|
767
|
+
catch { /* closed */ }
|
|
768
|
+
const s = sub = track.subscribe(latestSubscription(moqPriority(1)));
|
|
769
|
+
// The first group read is the open one, from its start: a snapshot, then the diffs.
|
|
752
770
|
for (;;) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
f = await g.readFrame();
|
|
756
|
-
}
|
|
757
|
-
catch {
|
|
758
|
-
break;
|
|
759
|
-
}
|
|
760
|
-
if (!f)
|
|
771
|
+
g = await s.recvGroup();
|
|
772
|
+
if (!g)
|
|
761
773
|
break;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
catch {
|
|
767
|
-
warn(`store ${name}: frame not decodable`);
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
let body;
|
|
771
|
-
try {
|
|
772
|
-
body = decodeBody(f.payload.subarray(hlen));
|
|
773
|
-
}
|
|
774
|
-
catch {
|
|
775
|
-
warn(`store ${name}: frame body not decodable`);
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
if (header.flags & FLAGS.END) {
|
|
779
|
-
const fin = body;
|
|
780
|
-
return fin && typeof fin.reason === "string" ? fin.reason : ENDED_STORE_GONE;
|
|
781
|
-
}
|
|
782
|
-
// A stopped reader no longer writes the mirror: it belongs to the next incarnation (or to none).
|
|
783
|
-
if (header.flags & FLAGS.KEY) {
|
|
784
|
-
const snap = body;
|
|
785
|
-
if (!snap || snap.epoch !== epoch) {
|
|
786
|
-
warn(`store ${name}: snapshot of another epoch dropped`);
|
|
787
|
-
continue;
|
|
774
|
+
for (;;) {
|
|
775
|
+
let f;
|
|
776
|
+
try {
|
|
777
|
+
f = await g.readFrame();
|
|
788
778
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
779
|
+
catch {
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
if (!f)
|
|
783
|
+
break;
|
|
784
|
+
let header, hlen;
|
|
785
|
+
try {
|
|
786
|
+
({ header, hlen } = decodeHeader(f.payload));
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
warn(`store ${name}: frame not decodable`);
|
|
799
790
|
continue;
|
|
800
791
|
}
|
|
801
|
-
|
|
792
|
+
let body;
|
|
793
|
+
try {
|
|
794
|
+
body = decodeBody(f.payload.subarray(hlen));
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
warn(`store ${name}: frame body not decodable`);
|
|
802
798
|
continue;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
799
|
+
}
|
|
800
|
+
if (header.flags & FLAGS.END) {
|
|
801
|
+
const fin = body;
|
|
802
|
+
return fin && typeof fin.reason === "string" ? fin.reason : ENDED_STORE_GONE;
|
|
803
|
+
}
|
|
804
|
+
// A stopped reader no longer writes the mirror: it belongs to the next incarnation (or to none).
|
|
805
|
+
if (header.flags & FLAGS.KEY) {
|
|
806
|
+
const snap = body;
|
|
807
|
+
if (!snap || snap.epoch !== epoch) {
|
|
808
|
+
warn(`store ${name}: snapshot of another epoch dropped`);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (signal.aborted)
|
|
812
|
+
continue;
|
|
813
|
+
if (state.ended === undefined)
|
|
814
|
+
state.applySnapshot(snap);
|
|
815
|
+
ns.post({ kind: "applied", epoch, version: snap.version });
|
|
816
|
+
}
|
|
817
|
+
else if (header.flags & FLAGS.DELTA) {
|
|
818
|
+
const diff = body;
|
|
819
|
+
if (!diff || typeof diff.version !== "number") {
|
|
820
|
+
warn(`store ${name}: diff not decodable`);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (signal.aborted)
|
|
824
|
+
continue;
|
|
825
|
+
if (state.ended === undefined && state.loaded && !state.applyDiff(diff, warn)) {
|
|
826
|
+
g.close();
|
|
827
|
+
continue subscribe;
|
|
828
|
+
}
|
|
829
|
+
ns.post({ kind: "applied", epoch, version: diff.version });
|
|
830
|
+
}
|
|
806
831
|
}
|
|
807
832
|
}
|
|
833
|
+
return undefined; // the track ended with no final frame: the session (or the broker) went
|
|
808
834
|
}
|
|
809
|
-
return undefined; // the track ended with no final frame: the session (or the broker) went
|
|
810
835
|
}
|
|
811
836
|
catch {
|
|
812
837
|
return undefined;
|
|
@@ -902,10 +927,15 @@ export class StoreHandleImpl {
|
|
|
902
927
|
this.ns.state.unwatch(q);
|
|
903
928
|
}
|
|
904
929
|
}
|
|
905
|
-
|
|
906
|
-
changed() {
|
|
930
|
+
changed(key) {
|
|
907
931
|
if (this.#closed)
|
|
908
932
|
return Promise.reject(this.#ended());
|
|
933
|
+
if (key !== undefined) {
|
|
934
|
+
// the first step of `watch(key)`, started now (its feed opens at the first `next()`), then left
|
|
935
|
+
const it = this.watch(key)[Symbol.asyncIterator]();
|
|
936
|
+
return it.next().then(r => { void it.return(undefined); if (r.done)
|
|
937
|
+
throw this.#ended(); return r.value; });
|
|
938
|
+
}
|
|
909
939
|
const from = this.get();
|
|
910
940
|
const d = deferred();
|
|
911
941
|
const un = this.ns.state.subscribe(() => { const v = this.get(); if (v !== from) {
|
|
@@ -919,7 +949,7 @@ export class StoreHandleImpl {
|
|
|
919
949
|
}
|
|
920
950
|
/** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
|
|
921
951
|
onChange(cb) { return this.ns.state.subscribe(() => cb(this.get())); }
|
|
922
|
-
/** Waits for the namespace to be in (the
|
|
952
|
+
/** Waits for the namespace to be in: the value current as of the open (the version the runtime answered applied), or the namespace absent. */
|
|
923
953
|
ready() { return this.ns.request(reply => ({ kind: "join", reply })); }
|
|
924
954
|
close() {
|
|
925
955
|
if (this.#closed)
|
package/dist/synapse.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Age, type ErrorBody, type ProjectSnapshot, type ServiceContract, t
|
|
|
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 {
|
|
5
|
+
import type { CallOptions, Catalog, ConnectOptions, ConnectTarget, ConnectionState, Discovery, DiscoveryState, Envelope, FetchToken, Info, Metrics, Publisher, Registration, Run, ServeHandler, ServeOptions, SceneHandle, StoreHandle, StoreOptions, SubscribeOptions, Subscription, Tasks, View, ViewOptions } from "./types.ts";
|
|
6
6
|
import { type StateValue } from "./util.ts";
|
|
7
7
|
export { FIRST_CONNECT_GIVE_UP_MS } from "./node.ts";
|
|
8
8
|
export declare class Synapse implements AsyncDisposable {
|
|
@@ -15,7 +15,8 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
15
15
|
*/
|
|
16
16
|
constructor(opts: ConnectOptions);
|
|
17
17
|
/**
|
|
18
|
-
* Connects the node to `target` (a string is the url
|
|
18
|
+
* Connects the node to `target` (a string is the url: a URL, or in Bun/Node a target such as `"infra-1@devel"`, see
|
|
19
|
+
* `ConnectTarget.url`), the one connect verb: the first connect, and how an HMI moves
|
|
19
20
|
* from one robot's runtime to another. The current session, if any, is torn down before `target` is dialed; every
|
|
20
21
|
* handle re-binds to the new session as across a reconnect. Identity fields `target` leaves out keep the node's.
|
|
21
22
|
*
|
|
@@ -41,6 +42,30 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
41
42
|
* resolves quietly). Resolves once the session is gone; already `idle`: at once. `not_connected` once the node has ended.
|
|
42
43
|
*/
|
|
43
44
|
disconnect(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Signs a person in for connecting by name (`connect("infra-1@devel")`) through the key server: `fetchToken` gives the
|
|
47
|
+
* person's credential and is called each time the key server is asked (never kept); `authUrl` is the key server
|
|
48
|
+
* (default `https://e9e413af.synapse.build`; an `http://` one is taken as it is, for a key server on your own machine while
|
|
49
|
+
* developing, never in production: the credential travels in the header). `setAuth(undefined)` signs out. Either way,
|
|
50
|
+
* what the node kept from the key server (your clusters, the cluster tokens) is forgotten and `discovery` is `idle` again.
|
|
51
|
+
*
|
|
52
|
+
* A live session is never touched: the sign-in is read only when the node dials, so signing out ends nothing now, and
|
|
53
|
+
* the next dial of a target named by name fails `unauthorized` (`failed`). With a person signed in, a target named by
|
|
54
|
+
* name is resolved through the key server also in Bun/Node, instead of the CLI's files. Call it before `connect()`.
|
|
55
|
+
*/
|
|
56
|
+
setAuth(fetchToken: FetchToken | undefined, opts?: {
|
|
57
|
+
authUrl?: string;
|
|
58
|
+
}): void;
|
|
59
|
+
/**
|
|
60
|
+
* Asks the key server for the signed-in person's clusters and their runtimes, sets `discovery` from the answer and
|
|
61
|
+
* resolves with it; one request at a time (a call while one is out joins it). Rejects `unauthorized` when nobody is
|
|
62
|
+
* signed in (`setAuth`; `discovery` stays `idle`) or the key server does not take the sign-in, `invalid_input` when it
|
|
63
|
+
* refuses the request, `connection_lost` when it cannot be reached or is busy (its sentence when it gave one). Without
|
|
64
|
+
* a sign-in it does not read the CLI's files.
|
|
65
|
+
*/
|
|
66
|
+
discover(): Promise<Discovery>;
|
|
67
|
+
/** What the key server said about the signed-in person (`discover()`), a state value: `get()` now, `onChange` for each change, `changed()` for the next one, `watch()` for each one in a loop. Connecting by name reads the same answer. */
|
|
68
|
+
readonly discovery: StateValue<DiscoveryState>;
|
|
44
69
|
/** The node's engine (the SDK's own modules and tests use it). */
|
|
45
70
|
get node(): Node;
|
|
46
71
|
/** 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. */
|
|
@@ -74,18 +99,15 @@ export declare class Synapse implements AsyncDisposable {
|
|
|
74
99
|
/** The topic's latest value as a view (SDK §4.3); subscribed while someone listens. */
|
|
75
100
|
view<TIn, TOut, S = TOut>(topic: TopicContract<TIn, TOut>, opts?: ViewOptions<TOut, S>): View<S>;
|
|
76
101
|
view<TOut = unknown, S = TOut>(topic: string, opts?: ViewOptions<TOut, S>): View<S>;
|
|
77
|
-
/** Starts a call on the current session (SDK §5.2): `await` it for the result,
|
|
102
|
+
/** Starts a call on the current session (SDK §5.2): `await` it for the result, read its updates (`for await (const u of run)`, `await run.next()`), or pass `onUpdate`. With no session (before the first, between two) the Run is already failed `not_connected`: nothing was sent. */
|
|
78
103
|
call<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): Run<U, R>;
|
|
79
104
|
call<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): Run<U, R>;
|
|
80
|
-
/** The call's updates and result as one ordered stream; `break` cancels. */
|
|
81
|
-
callStream<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): AsyncGenerator<CallEvent<U, R>, void, undefined>;
|
|
82
|
-
callStream<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): AsyncGenerator<CallEvent<U, R>, void, undefined>;
|
|
83
105
|
/** Registers `handler` as the provider of `svc`; resolves once the runtime lists us on the current session. One provider per service path per node. */
|
|
84
106
|
serve<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, handler: ServeHandler<TInput, U, R>, opts?: ServeOptions): Promise<Registration>;
|
|
85
107
|
serve<I = unknown, U = unknown, R = unknown>(svc: string, handler: ServeHandler<I, U, R>, opts?: ServeOptions): Promise<Registration>;
|
|
86
108
|
/** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
|
|
87
109
|
store(name: string, opts?: StoreOptions): Promise<StoreHandle>;
|
|
88
|
-
/** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
|
|
110
|
+
/** The active project's scene (PLAN v0.24 §5); resolves attached (current as of the attach: at the version the runtime answered), or at once `detached` when no project is active. */
|
|
89
111
|
scene(): Promise<SceneHandle>;
|
|
90
112
|
/** 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
113
|
readonly catalog: StateValue<Catalog>;
|
package/dist/synapse.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// own tasks, told about sessions by the loop; every fact about the world is read from the current
|
|
4
4
|
// `Session` (the runtime's statement) at the moment it is asked for, never copied here.
|
|
5
5
|
import { SynapseError, ageOf, fecSymbolSizeFor, isServiceContract, nowUs, resolveTopic, serviceName, } from "@synapse-sdk/core";
|
|
6
|
-
import {
|
|
6
|
+
import { failedRun, startOn } from "./calls.js";
|
|
7
7
|
import { Node, createNode, endpointDial, resolveTarget } from "./node.js";
|
|
8
|
+
import { DEFAULT_AUTH_URL, notSignedIn } from "./keyserver.js";
|
|
8
9
|
import { ServiceRegistration } from "./serve.js";
|
|
9
10
|
import { openScene } from "./scene.js";
|
|
10
11
|
import { Namespace, StoreHandleImpl, attachStore, openStore } from "./store.js";
|
|
@@ -25,18 +26,21 @@ function descKey(d) {
|
|
|
25
26
|
return JSON.stringify({ delivery: d.delivery, content_type: d.content_type, schema_hash: d.schema_hash ?? null, fec: d.fec ?? null });
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
|
-
* A fact of the node as a state value (`node.status`, `node.catalog`, `node.project`): `read` is the fact
|
|
29
|
-
* object while it is unchanged. It changes with the node's link
|
|
30
|
-
* that `watched` names; a callback is rebound to the new
|
|
29
|
+
* A fact of the node as a state value (`node.status`, `node.catalog`, `node.project`, `node.discovery`): `read` is the fact
|
|
30
|
+
* now, the same object while it is unchanged. It changes with the node's link, with the node's own values `kept` names,
|
|
31
|
+
* and, while connected, with the current session's values that `watched` names; a callback is rebound to the new
|
|
32
|
+
* session's values on every link change.
|
|
31
33
|
*/
|
|
32
34
|
class NodeCell {
|
|
33
35
|
node;
|
|
34
36
|
read;
|
|
35
37
|
watched;
|
|
36
|
-
|
|
38
|
+
kept;
|
|
39
|
+
constructor(node, read, watched, kept = []) {
|
|
37
40
|
this.node = node;
|
|
38
41
|
this.read = read;
|
|
39
42
|
this.watched = watched;
|
|
43
|
+
this.kept = kept;
|
|
40
44
|
}
|
|
41
45
|
get() { return this.read(); }
|
|
42
46
|
/** `cb` with the value on each change of what it is read from; returns the remover. */
|
|
@@ -51,7 +55,9 @@ class NodeCell {
|
|
|
51
55
|
};
|
|
52
56
|
bind();
|
|
53
57
|
const unLink = this.node.link.subscribe(() => { bind(); listener(); });
|
|
54
|
-
|
|
58
|
+
const unKept = this.kept.map(c => c.subscribe(listener));
|
|
59
|
+
return () => { unLink(); for (const u of unKept)
|
|
60
|
+
u(); for (const u of un)
|
|
55
61
|
u(); };
|
|
56
62
|
}
|
|
57
63
|
/** The value after its next change; once the node ends, the value then, whether or not it changed (nothing will change any more). */
|
|
@@ -104,9 +110,11 @@ export class Synapse {
|
|
|
104
110
|
return lastCatalog;
|
|
105
111
|
}, s => [s.topics, s.services]);
|
|
106
112
|
this.project = new NodeCell(n, () => n.currentSession()?.project.get(), s => [s.project]);
|
|
113
|
+
this.discovery = new NodeCell(n, () => n.discovery.get(), () => [], [n.discovery]);
|
|
107
114
|
}
|
|
108
115
|
/**
|
|
109
|
-
* Connects the node to `target` (a string is the url
|
|
116
|
+
* Connects the node to `target` (a string is the url: a URL, or in Bun/Node a target such as `"infra-1@devel"`, see
|
|
117
|
+
* `ConnectTarget.url`), the one connect verb: the first connect, and how an HMI moves
|
|
110
118
|
* from one robot's runtime to another. The current session, if any, is torn down before `target` is dialed; every
|
|
111
119
|
* handle re-binds to the new session as across a reconnect. Identity fields `target` leaves out keep the node's.
|
|
112
120
|
*
|
|
@@ -149,6 +157,34 @@ export class Synapse {
|
|
|
149
157
|
* resolves quietly). Resolves once the session is gone; already `idle`: at once. `not_connected` once the node has ended.
|
|
150
158
|
*/
|
|
151
159
|
disconnect() { return this.#node.declare(undefined); }
|
|
160
|
+
// -------------------------------------------------------------------------------- sign-in
|
|
161
|
+
/**
|
|
162
|
+
* Signs a person in for connecting by name (`connect("infra-1@devel")`) through the key server: `fetchToken` gives the
|
|
163
|
+
* person's credential and is called each time the key server is asked (never kept); `authUrl` is the key server
|
|
164
|
+
* (default `https://e9e413af.synapse.build`; an `http://` one is taken as it is, for a key server on your own machine while
|
|
165
|
+
* developing, never in production: the credential travels in the header). `setAuth(undefined)` signs out. Either way,
|
|
166
|
+
* what the node kept from the key server (your clusters, the cluster tokens) is forgotten and `discovery` is `idle` again.
|
|
167
|
+
*
|
|
168
|
+
* A live session is never touched: the sign-in is read only when the node dials, so signing out ends nothing now, and
|
|
169
|
+
* the next dial of a target named by name fails `unauthorized` (`failed`). With a person signed in, a target named by
|
|
170
|
+
* name is resolved through the key server also in Bun/Node, instead of the CLI's files. Call it before `connect()`.
|
|
171
|
+
*/
|
|
172
|
+
setAuth(fetchToken, opts = {}) { this.#node.setAuth(fetchToken, opts.authUrl ?? DEFAULT_AUTH_URL); }
|
|
173
|
+
/**
|
|
174
|
+
* Asks the key server for the signed-in person's clusters and their runtimes, sets `discovery` from the answer and
|
|
175
|
+
* resolves with it; one request at a time (a call while one is out joins it). Rejects `unauthorized` when nobody is
|
|
176
|
+
* signed in (`setAuth`; `discovery` stays `idle`) or the key server does not take the sign-in, `invalid_input` when it
|
|
177
|
+
* refuses the request, `connection_lost` when it cannot be reached or is busy (its sentence when it gave one). Without
|
|
178
|
+
* a sign-in it does not read the CLI's files.
|
|
179
|
+
*/
|
|
180
|
+
discover() {
|
|
181
|
+
const auth = this.#node.auth;
|
|
182
|
+
const p = auth ? auth.discover() : Promise.reject(notSignedIn());
|
|
183
|
+
p.catch(() => { });
|
|
184
|
+
return p;
|
|
185
|
+
}
|
|
186
|
+
/** What the key server said about the signed-in person (`discover()`), a state value: `get()` now, `onChange` for each change, `changed()` for the next one, `watch()` for each one in a loop. Connecting by name reads the same answer. */
|
|
187
|
+
discovery;
|
|
152
188
|
/** The node's engine (the SDK's own modules and tests use it). */
|
|
153
189
|
get node() { return this.#node; }
|
|
154
190
|
#session() { return this.#node.currentSession(); }
|
|
@@ -240,9 +276,6 @@ export class Synapse {
|
|
|
240
276
|
const contract = isServiceContract(svc) ? svc : undefined;
|
|
241
277
|
return startOn(this.node, s, serviceName(svc), args, opts, contract);
|
|
242
278
|
}
|
|
243
|
-
callStream(svc, args = {}, opts = {}) {
|
|
244
|
-
return callStream(this.call(svc, args, opts));
|
|
245
|
-
}
|
|
246
279
|
async serve(svc, handler, opts = {}) {
|
|
247
280
|
const n = this.node;
|
|
248
281
|
const name = serviceName(svc);
|
|
@@ -273,7 +306,7 @@ export class Synapse {
|
|
|
273
306
|
store(name, opts = {}) { return openStore(this.node, this.#stores, name, opts, this.warn); }
|
|
274
307
|
// ---------------------------------------------------------------------------------- scene
|
|
275
308
|
#scenes = { current: undefined };
|
|
276
|
-
/** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
|
|
309
|
+
/** The active project's scene (PLAN v0.24 §5); resolves attached (current as of the attach: at the version the runtime answered), or at once `detached` when no project is active. */
|
|
277
310
|
scene() { return openScene(this.node, this.#scenes, this.warn); }
|
|
278
311
|
// -------------------------------------------------------------------------------- catalog
|
|
279
312
|
/** 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. */
|