@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/util.d.ts CHANGED
@@ -5,6 +5,19 @@ export interface Deferred<T> {
5
5
  settled: boolean;
6
6
  }
7
7
  export declare function deferred<T>(): Deferred<T>;
8
+ /**
9
+ * A state value (`node.status`, `node.catalog`, `node.project`, the `status` of the store, scene and tasks handles):
10
+ * `get()` the value now (the same object while it is unchanged), `onChange` for each change (the framework bridge;
11
+ * `cb` gets the value; returns the remover), `changed()` for the value after the next change (at once with the value
12
+ * now once the node has ended), `watch()` for the value after each change in a loop (ends when the node, or the
13
+ * handle, ends). Not exported from the package: no user builds one.
14
+ */
15
+ export interface StateValue<T> {
16
+ get(): T;
17
+ onChange(cb: (value: T) => void): () => void;
18
+ changed(): Promise<T>;
19
+ watch(): AsyncIterable<T>;
20
+ }
8
21
  /** A value plus listeners. `changed()` resolves on the next `set`. */
9
22
  export declare class Cell<T> {
10
23
  #private;
@@ -16,6 +29,8 @@ export declare class Cell<T> {
16
29
  changed(): Promise<T>;
17
30
  get listenerCount(): number;
18
31
  }
32
+ /** A callback's throw, rethrown on its own microtask: the caller's loop goes on and the error still surfaces. */
33
+ export declare const rethrowLater: (e: unknown) => void;
19
34
  export declare const DONE: unique symbol;
20
35
  /**
21
36
  * Bounded FIFO with one consumer. `push` never blocks: past `capacity` the oldest item is dropped
@@ -65,3 +80,31 @@ export declare class Ring {
65
80
  push(v: number): void;
66
81
  percentile(p: number): number | undefined;
67
82
  }
83
+ /**
84
+ * The loop form of a state value (`watch()`), built from its `onChange` and `get`: the value after each change, compared
85
+ * by reference (a state value keeps its object while unchanged), changes in between folded into the latest (state, not
86
+ * history). It ends when it is told of a change once `ended()` holds; each state value tells its listeners when it ends.
87
+ * Leaving the loop removes the listener.
88
+ */
89
+ export declare function watchState<T>(onChange: (cb: () => void) => () => void, get: () => T, ended: () => boolean): AsyncGenerator<T, void, undefined>;
90
+ /**
91
+ * A handle's own state as a state value (`store.status`, `scene.status`; the `StateValue` shape): `get()` is `read()`
92
+ * kept the same object while every field is equal (`shallowEqual`), `onChange` calls back only when that object
93
+ * changes, `changed()` resolves with the next one (at once with the value now once `ended()`), `watch()` is the loop
94
+ * form. `source` is what tells of a possible change: the handle's own notifications.
95
+ */
96
+ export declare class StatusCell<T> {
97
+ #private;
98
+ private readonly read;
99
+ private readonly source;
100
+ private readonly ended;
101
+ constructor(read: () => T, source: (l: () => void) => () => void, ended: () => boolean);
102
+ get(): T;
103
+ /** `cb` with the value on each change; once the handle has ended (or this state value was disposed) nothing will change: a no-op remover. */
104
+ onChange(cb: (value: T) => void): () => void;
105
+ /** The handle closed: every upstream registration is removed (after the listeners were told of the close); new subscriptions are refused. */
106
+ dispose(): void;
107
+ changed(): Promise<T>;
108
+ /** The value after each change; ends with the handle. */
109
+ watch(): AsyncIterable<T>;
110
+ }
package/dist/util.js CHANGED
@@ -1,4 +1,4 @@
1
- // Small primitives shared by the client: a value cell with change notification, a bounded async
1
+ // Small primitives shared by the client: a value kept current with change notification, a bounded async
2
2
  // queue, deferreds, sleeps that honour AbortSignals, and TanStack-style structural sharing.
3
3
  export function deferred() {
4
4
  const d = { settled: false };
@@ -34,6 +34,8 @@ export class Cell {
34
34
  changed() { this.#waiters ??= deferred(); return this.#waiters.promise; }
35
35
  get listenerCount() { return this.#listeners.size; }
36
36
  }
37
+ /** A callback's throw, rethrown on its own microtask: the caller's loop goes on and the error still surfaces. */
38
+ export const rethrowLater = (e) => { queueMicrotask(() => { throw e; }); };
37
39
  export const DONE = Symbol("done");
38
40
  /**
39
41
  * Bounded FIFO with one consumer. `push` never blocks: past `capacity` the oldest item is dropped
@@ -225,3 +227,97 @@ export class Ring {
225
227
  return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
226
228
  }
227
229
  }
230
+ /**
231
+ * The loop form of a state value (`watch()`), built from its `onChange` and `get`: the value after each change, compared
232
+ * by reference (a state value keeps its object while unchanged), changes in between folded into the latest (state, not
233
+ * history). It ends when it is told of a change once `ended()` holds; each state value tells its listeners when it ends.
234
+ * Leaving the loop removes the listener.
235
+ */
236
+ export async function* watchState(onChange, get, ended) {
237
+ if (ended())
238
+ return;
239
+ let changed = false;
240
+ let wake;
241
+ const un = onChange(() => { changed = true; wake?.resolve(); });
242
+ let last = get();
243
+ try {
244
+ for (;;) {
245
+ while (!changed) {
246
+ wake = deferred();
247
+ await wake.promise;
248
+ wake = undefined;
249
+ }
250
+ changed = false;
251
+ if (ended())
252
+ return;
253
+ const v = get();
254
+ if (v === last)
255
+ continue;
256
+ last = v;
257
+ yield v;
258
+ }
259
+ }
260
+ finally {
261
+ un();
262
+ }
263
+ }
264
+ /**
265
+ * A handle's own state as a state value (`store.status`, `scene.status`; the `StateValue` shape): `get()` is `read()`
266
+ * kept the same object while every field is equal (`shallowEqual`), `onChange` calls back only when that object
267
+ * changes, `changed()` resolves with the next one (at once with the value now once `ended()`), `watch()` is the loop
268
+ * form. `source` is what tells of a possible change: the handle's own notifications.
269
+ */
270
+ export class StatusCell {
271
+ read;
272
+ source;
273
+ ended;
274
+ #last;
275
+ /** The upstream registrations of the listeners now subscribed: `dispose()` removes them all. */
276
+ #subs = new Set();
277
+ #disposed = false;
278
+ constructor(read, source, ended) {
279
+ this.read = read;
280
+ this.source = source;
281
+ this.ended = ended;
282
+ }
283
+ get() {
284
+ const v = this.read();
285
+ if (this.#last === undefined || !shallowEqual(this.#last, v))
286
+ this.#last = v;
287
+ return this.#last;
288
+ }
289
+ /** `cb` with the value on each change; once the handle has ended (or this state value was disposed) nothing will change: a no-op remover. */
290
+ onChange(cb) {
291
+ if (this.#disposed || this.ended())
292
+ return () => { };
293
+ let from = this.get();
294
+ const un = this.source(() => {
295
+ const v = this.get();
296
+ if (v === from)
297
+ return;
298
+ from = v;
299
+ cb(v);
300
+ });
301
+ this.#subs.add(un);
302
+ return () => { if (this.#subs.delete(un))
303
+ un(); };
304
+ }
305
+ /** The handle closed: every upstream registration is removed (after the listeners were told of the close); new subscriptions are refused. */
306
+ dispose() {
307
+ this.#disposed = true;
308
+ const subs = [...this.#subs];
309
+ this.#subs.clear();
310
+ for (const un of subs)
311
+ un();
312
+ }
313
+ changed() {
314
+ const from = this.get();
315
+ if (this.#disposed || this.ended())
316
+ return Promise.resolve(from);
317
+ return new Promise(resolve => {
318
+ const un = this.onChange(v => { un(); resolve(v); });
319
+ });
320
+ }
321
+ /** The value after each change; ends with the handle. */
322
+ watch() { return watchState(cb => this.onChange(cb), () => this.get(), () => this.#disposed || this.ended()); }
323
+ }
package/dist/view.d.ts CHANGED
@@ -11,9 +11,11 @@ export declare class TopicView<T, S> implements View<S> {
11
11
  ready(): Promise<void>;
12
12
  /** Resolves with the state after the next change; a closed view resolves with its final state. */
13
13
  changed(): Promise<ViewState<S>>;
14
- /** Called on every state change; the returned function removes the listener. */
15
- subscribe(listener: () => void): () => void;
16
- /** Ends the view for good: the subscription is closed, listeners are dropped, waiters settle, the registry entry is released. */
14
+ /** `cb` with the state on every change; the returned function removes it. Told once more when the view closes. */
15
+ onChange(cb: (state: ViewState<S>) => void): () => void;
16
+ /** The state after each change; ends when the view is closed or the node ends. */
17
+ watch(): AsyncIterable<ViewState<S>>;
18
+ /** Ends the view for good: the subscription is closed, listeners are told once and dropped, waiters settle, the registry entry is released. */
17
19
  close(): void;
18
20
  [Symbol.asyncDispose](): Promise<void>;
19
21
  }
package/dist/view.js CHANGED
@@ -1,10 +1,10 @@
1
- // A view (SDK §4.3): a topic's value now, as a cell for frameworks — `get()` any time, `subscribe`
2
- // for changes, `changed()` for the next one, `ready()` for the first. State, not history: of every
1
+ // A view (SDK §4.3): a topic's value now, as a state value — `get()` any time, `onChange` for
2
+ // changes, `changed()` for the next one, `watch()` for each one in a loop, `ready()` for the first. State, not history: of every
3
3
  // batch the subscription returns the view keeps the newest envelope (by its publisher's stamp),
4
4
  // compared with structural sharing so `get()` is reference-stable while the selected value is
5
5
  // deep-equal. The view is subscribed from its creation until `close()`, like the Rust `View`.
6
6
  import { SynapseError } from "@synapse-sdk/core";
7
- import { deferred, replaceEqualDeep } from "./util.js";
7
+ import { deferred, replaceEqualDeep, watchState } from "./util.js";
8
8
  export class TopicView {
9
9
  node;
10
10
  opts;
@@ -43,7 +43,7 @@ export class TopicView {
43
43
  const s = this.#state;
44
44
  if (s.status === "live" || s.status === "stale")
45
45
  d.resolve();
46
- else if (s.connection === "closed" || s.connection === "failed" || this.#closed)
46
+ else if (s.connection === "closed" || this.#closed)
47
47
  d.reject(new SynapseError("not_connected", "the node ended before the view got a value"));
48
48
  }
49
49
  /** Resolves with the state after the next change; a closed view resolves with its final state. */
@@ -58,13 +58,15 @@ export class TopicView {
58
58
  this.#changed = undefined;
59
59
  c?.resolve(this.#state);
60
60
  }
61
- /** Called on every state change; the returned function removes the listener. */
62
- subscribe(listener) {
61
+ /** `cb` with the state on every change; the returned function removes it. Told once more when the view closes. */
62
+ onChange(cb) {
63
63
  if (this.#closed)
64
64
  return () => { };
65
- this.#listeners.add(listener);
66
- return () => { this.#listeners.delete(listener); };
65
+ this.#listeners.add(cb);
66
+ return () => { this.#listeners.delete(cb); };
67
67
  }
68
+ /** The state after each change; ends when the view is closed or the node ends. */
69
+ watch() { return watchState(cb => this.onChange(cb), () => this.#state, () => this.#closed || this.node.isEnded); }
68
70
  async #read() {
69
71
  for (;;) {
70
72
  const batch = await this.#sub.next();
@@ -121,11 +123,11 @@ export class TopicView {
121
123
  return;
122
124
  this.#state = { ...s, ...patch };
123
125
  for (const l of [...this.#listeners])
124
- l();
126
+ l(this.#state);
125
127
  this.#settleChanged();
126
128
  this.#settleReady();
127
129
  }
128
- /** Ends the view for good: the subscription is closed, listeners are dropped, waiters settle, the registry entry is released. */
130
+ /** Ends the view for good: the subscription is closed, listeners are told once and dropped, waiters settle, the registry entry is released. */
129
131
  close() {
130
132
  if (this.#closed)
131
133
  return;
@@ -136,7 +138,16 @@ export class TopicView {
136
138
  clearTimeout(this.#staleTimer);
137
139
  this.#staleTimer = undefined;
138
140
  }
141
+ const ls = [...this.#listeners];
139
142
  this.#listeners.clear();
143
+ for (const l of ls) {
144
+ try {
145
+ l(this.#state);
146
+ }
147
+ catch (e) {
148
+ queueMicrotask(() => { throw e; });
149
+ }
150
+ }
140
151
  this.#settleChanged();
141
152
  this.#settleReady();
142
153
  this.onClose();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapse-sdk/client",
3
- "version": "0.3.0",
3
+ "version": "0.3.4",
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.0",
17
+ "@synapse-sdk/core": "0.3.4",
18
18
  "@moq/net": "0.4.2"
19
19
  },
20
20
  "peerDependencies": {