@synapse-sdk/client 0.3.4 → 0.3.6

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/session.js CHANGED
@@ -429,7 +429,7 @@ export async function withRoot(url, signal) {
429
429
  throw timeout.aborted ? new SynapseError("connection_lost", { message: `root helper at ${addr} did not answer`, source: "transport" }) : lost(describeError(e));
430
430
  }
431
431
  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://relay:4443/runtimes/<runtime_id>`, source: "sdk" });
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://<relay>/cluster/<cluster>/runtimes/<runtime_id>`, source: "sdk" });
433
433
  }
434
434
  u.pathname = `/${root}`;
435
435
  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
- gap: boolean;
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
- applyDiff(d: StoreDiff, warn: Warn): void;
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 first snapshot applied, or absent). */
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
- gap = false;
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.gap = false;
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}); not synced until the next snapshot`);
101
- this.gap = true;
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 = false;
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 = false;
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 && !this.gap && (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 };
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
- sub = bc.track(STORE_TRACK).subscribe(latestSubscription(moqPriority(1)));
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", () => { s.close(); g?.close(); }, { once: true });
747
- // The first group read is the open one, from its start: a snapshot, then the diffs.
748
- for (;;) {
749
- g = await s.recvGroup();
750
- if (!g)
751
- break;
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
- let f;
754
- try {
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
- let header, hlen;
763
- try {
764
- ({ header, hlen } = decodeHeader(f.payload));
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
- if (signal.aborted)
790
- continue;
791
- if (state.ended === undefined)
792
- state.applySnapshot(snap);
793
- ns.post({ kind: "applied", epoch, version: snap.version });
794
- }
795
- else if (header.flags & FLAGS.DELTA) {
796
- const diff = body;
797
- if (!diff || typeof diff.version !== "number") {
798
- warn(`store ${name}: diff not decodable`);
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
- if (signal.aborted)
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
- if (state.ended === undefined && state.loaded)
804
- state.applyDiff(diff, warn);
805
- ns.post({ kind: "applied", epoch, version: diff.version });
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
- /** The whole value after its next change (`store_gone` once this handle is closed). */
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 first snapshot applied, or absent). */
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 { 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";
5
+ import type { 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
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 {
@@ -74,18 +74,15 @@ export declare class Synapse implements AsyncDisposable {
74
74
  /** The topic's latest value as a view (SDK §4.3); subscribed while someone listens. */
75
75
  view<TIn, TOut, S = TOut>(topic: TopicContract<TIn, TOut>, opts?: ViewOptions<TOut, S>): View<S>;
76
76
  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, or read `updates()` first. With no session (before the first, between two) the Run is already failed `not_connected`: nothing was sent. */
77
+ /** 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
78
  call<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): Run<U, R>;
79
79
  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
80
  /** 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
81
  serve<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, handler: ServeHandler<TInput, U, R>, opts?: ServeOptions): Promise<Registration>;
85
82
  serve<I = unknown, U = unknown, R = unknown>(svc: string, handler: ServeHandler<I, U, R>, opts?: ServeOptions): Promise<Registration>;
86
83
  /** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
87
84
  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. */
85
+ /** 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
86
  scene(): Promise<SceneHandle>;
90
87
  /** 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
88
  readonly catalog: StateValue<Catalog>;
package/dist/synapse.js CHANGED
@@ -3,7 +3,7 @@
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 { callStream, failedRun, startOn } from "./calls.js";
6
+ import { failedRun, startOn } from "./calls.js";
7
7
  import { Node, createNode, endpointDial, resolveTarget } from "./node.js";
8
8
  import { ServiceRegistration } from "./serve.js";
9
9
  import { openScene } from "./scene.js";
@@ -240,9 +240,6 @@ export class Synapse {
240
240
  const contract = isServiceContract(svc) ? svc : undefined;
241
241
  return startOn(this.node, s, serviceName(svc), args, opts, contract);
242
242
  }
243
- callStream(svc, args = {}, opts = {}) {
244
- return callStream(this.call(svc, args, opts));
245
- }
246
243
  async serve(svc, handler, opts = {}) {
247
244
  const n = this.node;
248
245
  const name = serviceName(svc);
@@ -273,7 +270,7 @@ export class Synapse {
273
270
  store(name, opts = {}) { return openStore(this.node, this.#stores, name, opts, this.warn); }
274
271
  // ---------------------------------------------------------------------------------- scene
275
272
  #scenes = { current: undefined };
276
- /** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
273
+ /** 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
274
  scene() { return openScene(this.node, this.#scenes, this.warn); }
278
275
  // -------------------------------------------------------------------------------- catalog
279
276
  /** 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. */
package/dist/types.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface ConnectOptions {
40
40
  export interface ConnectTarget {
41
41
  /**
42
42
  * The runtime, `https://host:4470` (the path to dial is learned from the runtime at every attempt). Through a relay: the relay with
43
- * the runtime's path, `https://relay:4443/runtimes/<runtime_id>`, and `token`. A `jwt=` in the URL is taken as the token.
43
+ * the runtime's path, `https://<relay>/cluster/<cluster>/runtimes/<runtime_id>`, and `token`. A `jwt=` in the URL is taken as the token.
44
44
  */
45
45
  url: string;
46
46
  /** The type to declare there; left out keeps the node's current one. */
@@ -230,27 +230,32 @@ export interface CallOptions<U = unknown> {
230
230
  /** Local: fail `idle_timeout` (and cancel) when no frame arrives for this long. */
231
231
  idleTimeoutMs?: number;
232
232
  signal?: AbortSignal;
233
+ /** The callback form: the SDK reads the updates and calls `onUpdate(u)` for each (the run's loop then has none); a throwing `onUpdate` is rethrown later and the reading goes on. */
233
234
  onUpdate?: (u: U) => void;
234
235
  }
235
- /** A started call (WP §8.3): its updates, its result, and cancel. */
236
- /** `await run` is `await run.result`. */
237
- export interface Run<U, R> extends AsyncDisposable, PromiseLike<R> {
236
+ /**
237
+ * A started call (WP §8.3). Its progress is a stream, the run itself a promise:
238
+ * `for await (const u of run)` the updates until the run ended, `await run.next()` one update
239
+ * (`undefined` once the run ended), or `onUpdate` in the call options (the SDK reads them);
240
+ * `await run` (or `.then`/`.catch`) the result, rejecting with the `SynapseError` of a
241
+ * `failed` or `canceled` run; `cancel()`.
242
+ */
243
+ export interface Run<U, R> extends AsyncDisposable, PromiseLike<R>, AsyncIterable<U> {
238
244
  readonly id: string;
239
245
  readonly status: RunStatus;
240
- /** The next progress update; `undefined` once the terminal frame arrived (then `result`). */
241
- nextUpdate(): Promise<U | undefined>;
242
- updates(): AsyncIterable<U>;
246
+ /** The next progress update; `undefined` once the run ended (then `await run` settles at once). */
247
+ next(): Promise<U | undefined>;
248
+ /** `await run` / `.then` as on a promise (returns a `Promise`, so `.then(...).catch(...)` chains). */
249
+ then<A = R, B = never>(onfulfilled?: ((value: R) => A | PromiseLike<A>) | null, onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null): Promise<A | B>;
250
+ /** The result as a plain promise (what `await run` waits for). */
243
251
  readonly result: Promise<R>;
252
+ /** `.catch` as on a promise: `onRejected` gets the run's `SynapseError` when it failed or was canceled. */
253
+ catch<B = never>(onRejected?: ((e: unknown) => B | PromiseLike<B>) | null): Promise<R | B>;
254
+ /** `.finally` as on a promise: `onFinally` runs once the run ended, whichever way. */
255
+ finally(onFinally?: (() => void) | null): Promise<R>;
244
256
  /** Cooperative cancel: tells the broker, resolves on its `canceled` frame. */
245
257
  cancel(reason?: string): Promise<void>;
246
258
  }
247
- export type CallEvent<U, R> = {
248
- type: "update";
249
- data: U;
250
- } | {
251
- type: "result";
252
- data: R;
253
- };
254
259
  export interface ServeContext<U> {
255
260
  readonly call: string;
256
261
  readonly caller: string;
@@ -332,7 +337,7 @@ export interface StoreHandle extends AsyncDisposable {
332
337
  readonly synced: boolean;
333
338
  /** The handle's own state, a state value: `get()` now (the same object while unchanged), `onChange` for each change, `changed()` for the next. */
334
339
  readonly status: StateValue<StoreStatus>;
335
- /** Resolves once the first value is in (the first snapshot applied, or the namespace absent). */
340
+ /** Resolves once the value is in: current as of the open (the version the runtime answered then applied), or the namespace absent. */
336
341
  ready(): Promise<void>;
337
342
  /** `set(key, value)`, or `set({ ... })` for several keys as one atomic write; resolves once `get()` shows it. */
338
343
  set(key: string, value: unknown): Promise<void>;
@@ -343,8 +348,9 @@ export interface StoreHandle extends AsyncDisposable {
343
348
  destroy(): Promise<void>;
344
349
  /** The whole value after each change; with `key`, that key's value each time it changes (`undefined` after a delete). */
345
350
  watch(key?: string): AsyncIterable<unknown>;
346
- /** The whole value after its next change; `store_gone` once this handle is closed. */
351
+ /** The whole value after its next change; with `key`, that key's value after its next change (`undefined` after a delete). `store_gone` once this handle is closed. */
347
352
  changed(): Promise<Record<string, unknown>>;
353
+ changed(key: string): Promise<unknown>;
348
354
  /** `cb` with the whole value on every change of the value or the status (the framework bridge); returns the remover. */
349
355
  onChange(cb: (value: Record<string, unknown>) => void): () => void;
350
356
  close(): void;
@@ -441,7 +447,7 @@ export interface SceneEntity {
441
447
  kind: SceneKind;
442
448
  /**
443
449
  * Unique across every plane (authored, calibration, live): a write that would give an entity a name another entity
444
- * holds is refused `name_taken`, its `data.holder` the id holding the name.
450
+ * holds is refused `name_taken`, its `data.holder` the id holding the name (`data.kind`, `data.layer`: the holder's).
445
451
  */
446
452
  name?: string;
447
453
  /** The parent's id; `null` is the world root. */
@@ -550,6 +556,9 @@ export interface SceneEntityView extends SceneView<SceneEntity | undefined> {
550
556
  */
551
557
  watch(): AsyncIterable<SceneEntity | undefined>;
552
558
  watch<K extends keyof SceneEntity & string>(component: K): AsyncIterable<SceneEntity[K] | undefined>;
559
+ /** The entity after its next change; with `component`, that component after its next change (`undefined` once gone). The value now once the handle ends. */
560
+ changed(): Promise<SceneEntity | undefined>;
561
+ changed<K extends keyof SceneEntity & string>(component: K): Promise<SceneEntity[K] | undefined>;
553
562
  }
554
563
  /** An entity view's authored plane (`view.authored`). Reads as the view does: nothing once the handle is detached or closed. */
555
564
  export interface EntityAuthored {
@@ -743,12 +752,14 @@ export interface SceneAuthored {
743
752
  patch(entity: string | SceneEntityView, set: Partial<SceneEntity>): Promise<void>;
744
753
  /**
745
754
  * A builder's one verb, keyed by `name`; resolves with the entity's view (by id: that entity) once the entities show
746
- * it. No authored entity of that name: `spawn(kind, { name, parent, ...components })`. One of `kind`: its authored
747
- * value is compared with what `patch(id, components)` would make of it (one-level merge; live overrides never
748
- * count): equal, nothing is sent; else a patch of the components that differ. `parent` is used by the spawn only: an
749
- * existing entity is not reparented. One of another kind, or a name a live entity holds: the spawn is sent and the
750
- * runtime answers (`name_taken`, `data.holder`, while the name is held; the copy here may be stale). An `id`
751
- * component: `invalid_input` (`data: { component: "id" }`), nothing sent.
755
+ * it. The id is the runtime's answer, never the copy's: `spawn(kind, { name, parent, ...components })` (a fresh id) is
756
+ * sent first; the runtime refusing it `name_taken` names the holder and its kind (`data.holder`, `data.kind`,
757
+ * `data.layer`). A holder of this kind in the authored or calibration layer is patched with the components that differ
758
+ * from what the copy shows of it (one-level merge; live overrides never count; all of them when the copy does not show
759
+ * it yet; equal, nothing more is sent): the copy only trims the patch, it never decides. A holder of another kind, a
760
+ * live one, or an answer without them: the `name_taken` as it came. `parent` is used by the spawn only: an existing
761
+ * entity is not reparented. Any other refusal as it came (`not_found` on the patch: the entity went meanwhile; upsert
762
+ * again). An `id` component: `invalid_input` (`data: { component: "id" }`), nothing sent.
752
763
  */
753
764
  upsert(kind: SceneKind, opts: SceneSpawnOptions & {
754
765
  id?: never;
@@ -817,7 +828,7 @@ export interface SceneHandle extends AsyncDisposable {
817
828
  readonly status: StateValue<SceneStatus>;
818
829
  /** The one boolean a node author checks: `status.get().synced`. */
819
830
  readonly synced: boolean;
820
- /** Resolves once the handle is attached (at once if it is); `not_connected` once the handle is closed. No timeout: pass an `AbortSignal` to what awaits it, or race it. */
831
+ /** Resolves once the handle is attached (at once if it is): the copy current as of the attach, at the version the runtime answered when the scene was opened, or past it; `not_connected` once the handle is closed. No timeout: pass an `AbortSignal` to what awaits it, or race it. */
821
832
  ready(): Promise<void>;
822
833
  /** The entities after their next change (state, not history); `not_connected` once the handle is closed. */
823
834
  changed(): Promise<Readonly<Record<string, SceneEntity>>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapse-sdk/client",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Synapse SDK client: connect a node to the runtime; topics, services, calls, store, video.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -14,7 +14,7 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@synapse-sdk/core": "0.3.4",
17
+ "@synapse-sdk/core": "0.3.6",
18
18
  "@moq/net": "0.4.2"
19
19
  },
20
20
  "peerDependencies": {