@synapse-sdk/client 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Card, ClientType, Delivery, FecParams, Role, ServiceRef, StoreMode, StoreOnDeath, StoreRow, TimeDomain, TopicRef, TrackDesc, Tlv, JsonSchema } from "@synapse-sdk/core";
2
2
  import type { SynapseError, TasksRun } from "@synapse-sdk/core";
3
+ import type { StateValue } from "./util.ts";
3
4
  export type { Card, ClientType, Delivery, FecParams, Role, ServiceRef, StoreMode, StoreOnDeath, StoreRow, TimeDomain, TopicRef, TrackDesc };
4
5
  /** What the reconnect policy is asked before every attempt. */
5
6
  export interface RetryContext {
@@ -12,31 +13,21 @@ export interface ReconnectPolicy {
12
13
  nextRetryDelayMs(ctx: RetryContext): number | null;
13
14
  }
14
15
  export type Transport = "auto" | "webtransport" | "websocket";
16
+ /** The node's own options (`new Synapse(opts)`): who it is and how it behaves. Where it connects is a `ConnectTarget`, given to `connect()`. */
15
17
  export interface ConnectOptions {
16
- /**
17
- * The runtime, `https://host:4470` (the path to dial is learned from the runtime at every attempt). Through a relay: the relay with
18
- * the runtime's path, `https://relay:4443/runtimes/<runtime_id>`, and `token`. A `jwt=` in the URL is taken as the token.
19
- * Absent: `SYNAPSE_ENDPOINT` (Bun/Node; the runtime sets it for every task it starts); a browser has no environment,
20
- * so there it is `invalid_input`.
21
- */
22
- url?: string;
23
- /** The relay token that lets this node through a relay; none for a runtime dialed directly. Never printed. */
24
- token?: string;
25
18
  /** This node's type (WP §3.1): bounds the role it may exercise; the runtime grants the role. */
26
19
  type: ClientType;
27
20
  /** This node's name `[a-z0-9_-]{1,64}`; minted (`<type>-<6 hex>`) when absent and kept across reconnects. */
28
21
  name?: string;
29
- /** Shared across one human's clients so they are promoted and demoted together (WP §3.5); a `web`/`hmi` node without one mints one. */
22
+ /** Shared across one human's web clients so they are promoted and demoted together (WP §3.5; an hmi is never promoted or demoted); a `web`/`hmi` node without one mints one. */
30
23
  userId?: string;
31
24
  /** Reserved for signed credentials. */
32
25
  credential?: string;
33
- /** Pin of the broker's self-signed certificate (SHA-256 hex). Absent: `SYNAPSE_BROKER_CERT_SHA256`, then the broker's helper, then unpinned. */
34
- brokerCertSha256?: string;
35
26
  /** Time-domain id override (WP §7.3); default per runtime. */
36
27
  domId?: string;
37
28
  /** Standing project intent (WP §3.5), sent with the connect request. */
38
29
  project?: "operator";
39
- /** `null` = no reconnect: a dropped session ends the node `closed`. Default: 100 ms → 10 s, ×2, jitter, unlimited. */
30
+ /** `null` = no reconnect: a dropped session leaves the node `failed` (resting; `connect()` dials again). Default: 100 ms → 10 s, ×2, jitter, unlimited. */
40
31
  reconnect?: ReconnectPolicy | null;
41
32
  transport?: Transport;
42
33
  connectTimeoutMs?: number;
@@ -45,18 +36,34 @@ export interface ConnectOptions {
45
36
  /** Bun/Node: close cleanly on SIGINT/SIGTERM (default true). */
46
37
  signals?: boolean;
47
38
  }
48
- /** Where the node should be connected: the argument of `setTarget` (fields left out keep the node's current ones). */
39
+ /** Where the node should be connected: the argument of `connect()` (identity fields left out keep the node's current ones). */
49
40
  export interface ConnectTarget {
41
+ /**
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.
44
+ */
50
45
  url: string;
46
+ /** The type to declare there; left out keeps the node's current one. */
51
47
  type?: ClientType;
48
+ /** The name to declare there; left out keeps the node's current one. */
52
49
  name?: string;
50
+ /** The user id to declare there; left out keeps the node's current one. */
53
51
  userId?: string;
52
+ /** The credential to present there; left out keeps the node's current one. Never printed. */
54
53
  credential?: string;
54
+ /** Pin of *this* broker's self-signed certificate (SHA-256 hex). Absent: `SYNAPSE_BROKER_CERT_SHA256`, then the broker's helper, then unpinned (per attempt). */
55
55
  brokerCertSha256?: string;
56
- /** The relay token for *this* target; left out = no token (a token for another runtime would be refused). */
56
+ /** The relay token for *this* target; left out = no token (a token for another runtime would be refused). Never printed. */
57
57
  token?: string;
58
58
  }
59
+ /**
60
+ * Where the node stands. `idle`: no target (constructed and not yet `connect()`ed, or after `disconnect()`). `failed`:
61
+ * the dialing gave up (`unauthorized`, the bounded `connect()` timed out, the reconnect policy declined); the node rests
62
+ * there and a later `connect()` dials again. `closed`: after `close()`, for good.
63
+ */
59
64
  export type ConnectionState = {
65
+ status: "idle";
66
+ } | {
60
67
  status: "connecting";
61
68
  attempt: number;
62
69
  } | {
@@ -205,12 +212,15 @@ export interface ViewOptions<T, S> extends SubscribeOptions {
205
212
  staleTime?: number;
206
213
  select?: (v: T) => S;
207
214
  }
208
- /** A topic's value now, as a cell: `get()` any time, `subscribe` for changes (the framework bridge), `changed()` for the next one; subscribed from creation until `close()`. */
215
+ /** A topic's value now, as a state value: `get()` any time, `onChange` for each change (the framework bridge), `changed()` for the next one, `watch()` for each one in a loop; subscribed from creation until `close()`. */
209
216
  export interface View<S> extends AsyncDisposable {
210
217
  get(): ViewState<S>;
211
- subscribe(listener: () => void): () => void;
218
+ /** `cb` with the state on each change; returns the remover. */
219
+ onChange(cb: (state: ViewState<S>) => void): () => void;
212
220
  ready(): Promise<void>;
213
221
  changed(): Promise<ViewState<S>>;
222
+ /** The state after each change (state, not history: changes in between fold into the latest); ends when the view is closed or the node ends. */
223
+ watch(): AsyncIterable<ViewState<S>>;
214
224
  close(): void;
215
225
  }
216
226
  export type RunStatus = "pending" | "running" | "succeeded" | "failed" | "canceled";
@@ -306,18 +316,25 @@ export type StoreChange = {
306
316
  by: string | undefined;
307
317
  };
308
318
  /**
309
- * A namespace as a value kept current (SDK §9): `value`/`get` read it, `set`/`delete` resolve once the
310
- * value shows the write, `synced` says whether it is current, `watch()` follows changes. The runtime
311
- * decides every write (`not_owner`, `unauthorized`, `store_gone`).
319
+ * A namespace as a value kept current (SDK §9): `get()` reads it, `set`/`delete` resolve once the
320
+ * value shows the write, `synced` says whether it is current, `onChange`/`watch()` follow changes. The runtime
321
+ * decides every write (`not_owner`, `unauthorized`, `store_gone`). A write made while the handle is
322
+ * between incarnations (its intent being replayed, e.g. after a project activation) waits for the
323
+ * handle to bind again and is decided then; it is refused `store_gone` only if the handle ends first.
312
324
  */
313
325
  export interface StoreHandle extends AsyncDisposable {
314
326
  readonly name: string;
315
327
  /** The whole value, what the runtime has (the same object while nothing changed). */
316
- readonly value: Record<string, unknown>;
328
+ get(): Record<string, unknown>;
329
+ /** One key's value (`undefined` when absent). */
317
330
  get(key: string): unknown;
331
+ /** The one boolean a node author checks: `status.get().synced`. */
318
332
  readonly synced: boolean;
319
- readonly status: StoreStatus;
320
- /** `set(key, value)`, or `set({ ... })` for several keys as one atomic write; resolves once `value` shows it. */
333
+ /** The handle's own state, a state value: `get()` now (the same object while unchanged), `onChange` for each change, `changed()` for the next. */
334
+ readonly status: StateValue<StoreStatus>;
335
+ /** Resolves once the first value is in (the first snapshot applied, or the namespace absent). */
336
+ ready(): Promise<void>;
337
+ /** `set(key, value)`, or `set({ ... })` for several keys as one atomic write; resolves once `get()` shows it. */
321
338
  set(key: string, value: unknown): Promise<void>;
322
339
  set(values: Record<string, unknown>): Promise<void>;
323
340
  delete(key: string): Promise<void>;
@@ -326,8 +343,10 @@ export interface StoreHandle extends AsyncDisposable {
326
343
  destroy(): Promise<void>;
327
344
  /** The whole value after each change; with `key`, that key's value each time it changes (`undefined` after a delete). */
328
345
  watch(key?: string): AsyncIterable<unknown>;
329
- /** The framework bridge (React): called on every change of the value or the status. */
330
- subscribe(listener: () => void): () => void;
346
+ /** The whole value after its next change; `store_gone` once this handle is closed. */
347
+ changed(): Promise<Record<string, unknown>>;
348
+ /** `cb` with the whole value on every change of the value or the status (the framework bridge); returns the remover. */
349
+ onChange(cb: (value: Record<string, unknown>) => void): () => void;
331
350
  close(): void;
332
351
  }
333
352
  export type Vec3 = [number, number, number];
@@ -341,6 +360,11 @@ export interface Xform {
341
360
  }
342
361
  /** Which plane an entity lives in: `authored` (the project's), `calibration` (the robot's), `live` (never persisted). */
343
362
  export type SceneLayer = "authored" | "calibration" | "live";
363
+ /**
364
+ * What an entity is. Any string is a kind; these are offered for completion: `frame` and `robot` are the two the runtime
365
+ * gives meaning to, `model` and `scan` the common others.
366
+ */
367
+ export type SceneKind = "frame" | "robot" | "model" | "scan" | (string & {});
344
368
  /** Each flag holds only when it is `true`. `locked` refuses patch/reparent/rename/despawn (a flags-only patch passes). */
345
369
  export interface SceneFlags {
346
370
  locked?: boolean;
@@ -414,8 +438,11 @@ export interface Hardpoint {
414
438
  export interface SceneEntity {
415
439
  /** A ULID; never changes. */
416
440
  id: string;
417
- kind: string;
418
- /** Unique across the authored and calibration planes. */
441
+ kind: SceneKind;
442
+ /**
443
+ * 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.
445
+ */
419
446
  name?: string;
420
447
  /** The parent's id; `null` is the world root. */
421
448
  parent: string | null;
@@ -450,7 +477,7 @@ export interface SceneEntity {
450
477
  [key: string]: unknown;
451
478
  }
452
479
  /**
453
- * An id's live row (`scene.entity(id, { plane: "live" })`), each key present only when the row has it:
480
+ * An id's live row (`view.live.get()`), each key present only when the row has it:
454
481
  * `entity` is a live-layer entity's upsert row; `pose`, `joints`, `state` are the lanes; `stamp` is the
455
482
  * newest lane stamp (the runtime's clock).
456
483
  */
@@ -476,15 +503,96 @@ export interface SceneTreeNode {
476
503
  export type SceneTree = SceneTreeNode[];
477
504
  /**
478
505
  * A value of the scene, kept current: `get()` the value now (the same object until it changes),
479
- * `subscribe` for each change (the framework bridge), `changed()` for the next one. It reads the
480
- * handle's copy of the scene (no subscription of its own) and ends with the handle.
506
+ * `onChange` for each change (the framework bridge), `changed()` for the next one, `watch()` for each one in a
507
+ * loop. It reads the handle's copy of the scene (no subscription of its own) and ends with the handle.
481
508
  */
482
509
  export interface SceneView<T> {
483
510
  get(): T;
484
- subscribe(listener: () => void): () => void;
485
- /** Resolves with the value after its next change (with the value as it is when the handle closes). */
511
+ /** `cb` with the value on each change; returns the remover. */
512
+ onChange(cb: (value: T) => void): () => void;
513
+ /** The value after each change (state, not history: changes in between fold into the latest); ends with the handle. */
514
+ watch(): AsyncIterable<T>;
515
+ /** Resolves with the value after its next change relative to the value at the call: a change already readable when it is called is not its change (with the value as it is when the handle closes). */
486
516
  changed(): Promise<T>;
487
517
  }
518
+ /** Which entity an entity view follows: an id (that entity only), or a name (whichever entity holds it now, on any plane). */
519
+ export type SceneEntityRef = string | {
520
+ name: string;
521
+ };
522
+ /**
523
+ * One entity as a view (`scene.entity(id)`, `scene.entity({ name })`), shaped like a store handle: `get()` is the
524
+ * merged entity now (authored with its live overrides; `undefined` when there is none), `get(component)` one of its
525
+ * components; `watch()` follows it. It is a `SceneView` too (`onChange`, `changed()`), so it fits wherever a view
526
+ * does. The scene's two planes are its two namespaces: `authored` (`get`, `set`: the entity as declared, one patch)
527
+ * and `live` (`get`: the live row with its `stamp`; `set`: the lanes; `remove`: the row). They have no `watch`,
528
+ * `onChange` or `changed` of their own: the view's cover the entity, whichever plane changed.
529
+ *
530
+ * A view by name follows the name: when the entity holding it is despawned and another takes the name, the view
531
+ * reads the new one (a view by id reads `undefined` from then on).
532
+ */
533
+ export interface SceneEntityView extends SceneView<SceneEntity | undefined> {
534
+ /**
535
+ * The id of the entity the view names now: a view by id (what `upsert` returns), its id; a view by name, the id
536
+ * holding the name now (none holds it: `not_found`, `data: { name }`).
537
+ */
538
+ readonly id: string;
539
+ /** The merged entity now; `undefined` when there is none (absent, or the handle detached or closed). */
540
+ get(): SceneEntity | undefined;
541
+ /** One component of the merged entity now (`get("xform")`); `undefined` when the entity or the component is absent. */
542
+ get<K extends keyof SceneEntity & string>(component: K): SceneEntity[K] | undefined;
543
+ /** The entity's authored plane: one object for the view's life. */
544
+ readonly authored: EntityAuthored;
545
+ /** The entity's live plane: one object for the view's life. */
546
+ readonly live: EntityLive;
547
+ /**
548
+ * The entity after each change (`undefined` once it is gone); with `component`, that component each time it
549
+ * changes. It is state, not history: changes in between are folded, the latest wins. Ends with the handle.
550
+ */
551
+ watch(): AsyncIterable<SceneEntity | undefined>;
552
+ watch<K extends keyof SceneEntity & string>(component: K): AsyncIterable<SceneEntity[K] | undefined>;
553
+ }
554
+ /** An entity view's authored plane (`view.authored`). Reads as the view does: nothing once the handle is detached or closed. */
555
+ export interface EntityAuthored {
556
+ /** The entity as declared (the authored entity, or a live entity's upsert row), without the live lanes; its object changes only when that value does. */
557
+ get(): SceneEntity | undefined;
558
+ get<K extends keyof SceneEntity & string>(component: K): SceneEntity[K] | undefined;
559
+ /**
560
+ * `set(component, value)`, or `set({ ...components })` for several: one `patch` of the authored entity the view
561
+ * names now (by name: the id holding the name at the call), resolved once the entities show it; the runtime's
562
+ * refusal is the error. A view by id sends the patch as `scene.authored.patch(id, …)` does (an id the runtime does
563
+ * not have: its `not_found`). A view by name whose name no entity holds has no id to send to: `not_found`
564
+ * (`data: { name }`), nothing sent. Detached: `project_inactive`.
565
+ */
566
+ set<K extends keyof SceneEntity & string>(component: K, value: SceneEntity[K]): Promise<void>;
567
+ set(components: Partial<SceneEntity>): Promise<void>;
568
+ }
569
+ /** An entity view's live plane (`view.live`). Reads as the view does: nothing once the handle is detached or closed. */
570
+ export interface EntityLive {
571
+ /** The entity's live row ({@link SceneEntityLive}, its `stamp` included); `undefined` without one. */
572
+ get(): SceneEntityLive | undefined;
573
+ get<K extends keyof SceneEntityLive & string>(component: K): SceneEntityLive[K] | undefined;
574
+ /**
575
+ * Writes the live lanes of the entity the view names now, as one `scene.live.batch` (synchronous, like `publish`):
576
+ * `xform: { t, q }` the pose lane (both required, nothing else), `joints` the joints lane, `state` the state lane; any
577
+ * other component is `invalid_input`. `lifetimeMs`: the override expires that long after its last write (the entity's
578
+ * authored values show again); a live entity keeps the lifetime it was upserted with. The first write for an id this
579
+ * node has not claimed asks the claim (see `scene.live.batch`), so a single write lands. A view by name whose name no
580
+ * entity holds: `not_found`. Not attached: dropped and counted (`status.get().detachedDrops`). Closed, or the node
581
+ * ended: `not_connected`.
582
+ */
583
+ set(components: {
584
+ xform?: {
585
+ t: Vec3;
586
+ q: Quat;
587
+ };
588
+ joints?: Record<string, number>;
589
+ state?: Record<string, unknown>;
590
+ }, opts?: {
591
+ lifetimeMs?: number;
592
+ }): void;
593
+ /** Releases this node's claim of the entity the view names now (`scene.live.remove(id)`, the call): the runtime retracts its live row. */
594
+ remove(): Promise<void>;
595
+ }
488
596
  /** `attached`: the scene of the active project is in; `detached`: none is (no project active, a session gap, between incarnations); `closed`: for good. */
489
597
  export type SceneState = "attached" | "detached" | "closed";
490
598
  export interface SceneStatus {
@@ -496,10 +604,12 @@ export interface SceneStatus {
496
604
  /** The authored plane's version and hash as this handle has them (0 and "" while detached). */
497
605
  version: number;
498
606
  hash: string;
499
- /** `live()` batches dropped because the handle was not attached. */
607
+ /** Live writes (`scene.live.batch`, `view.live.set`) dropped because the handle was not attached. */
500
608
  detachedDrops: number;
609
+ /** Lane rows dropped because their id's claim was refused (rows parked while it was asked, and rows written while refused). */
610
+ unclaimedDrops: number;
501
611
  }
502
- /** An authored change as the runtime applied it (the `watchChanges` feed). */
612
+ /** An authored change as the runtime applied it (a diff op of the scene's `authored` track). */
503
613
  export type SceneDiffOp = {
504
614
  op: "spawn";
505
615
  entity: SceneEntity;
@@ -563,27 +673,27 @@ export type SceneOp = {
563
673
  */
564
674
  export type SceneSpawnOptions = {
565
675
  name: string;
566
- parent?: string | null;
676
+ parent?: string | SceneEntityView | null;
567
677
  id?: string;
568
678
  } & Partial<Omit<SceneEntity, "id" | "kind" | "name" | "parent" | "order" | "meta">>;
569
- /** Where `reparent` puts the entity among its new siblings (default: after the last). */
679
+ /** Where `reparent` puts the entity among its new siblings (default: after the last): a sibling by its id or its view. */
570
680
  export type ScenePlace = {
571
- before: string;
681
+ before: string | SceneEntityView;
572
682
  } | {
573
- after: string;
683
+ after: string | SceneEntityView;
574
684
  };
575
- /** One authoring batch being built (`txn`): sent as one atomic `apply`. */
685
+ /** One authoring batch being built (`txn`): sent as one atomic `apply`. An entity is named by its id or by a view of it (`upsert`'s result). */
576
686
  export interface SceneTxn {
577
687
  /** Spawns an entity of `kind`; returns its id. */
578
- spawn(kind: string, opts: SceneSpawnOptions): string;
579
- /** Sets components of `id` (an object merges into an object one level deep). */
580
- patch(id: string, set: Partial<SceneEntity>): void;
581
- rename(id: string, name: string): void;
582
- reparent(id: string, parent: string | null, place?: ScenePlace): void;
583
- despawn(id: string, opts?: {
688
+ spawn(kind: SceneKind, opts: SceneSpawnOptions): string;
689
+ /** Sets components of the entity (an object merges into an object one level deep). */
690
+ patch(entity: string | SceneEntityView, set: Partial<SceneEntity>): void;
691
+ rename(entity: string | SceneEntityView, name: string): void;
692
+ reparent(entity: string | SceneEntityView, parent: string | SceneEntityView | null, place?: ScenePlace): void;
693
+ despawn(entity: string | SceneEntityView, opts?: {
584
694
  cascade?: boolean;
585
695
  }): void;
586
- setLocked(id: string, locked: boolean): void;
696
+ setLocked(entity: string | SceneEntityView, locked: boolean): void;
587
697
  /** The ops as they will be sent. */
588
698
  readonly ops: readonly SceneOp[];
589
699
  }
@@ -600,77 +710,131 @@ export interface LiveState {
600
710
  id: string;
601
711
  s: Record<string, unknown>;
602
712
  }
603
- /** One live batch: live entities (`layer: "live"`), lane overrides, removals; `lifetimeMs` makes the override rows it touches expire. */
713
+ /**
714
+ * One live batch: data only, the lanes (`poses`, `joints`, `state`). `lifetimeMs` makes the override rows the batch
715
+ * touches expire. Claims are calls, never rows: `scene.live.upsert` and `scene.live.remove`.
716
+ */
604
717
  export interface LiveBatch {
605
- upsert?: SceneEntity[];
606
718
  poses?: LivePose[];
607
719
  joints?: LiveJoints[];
608
720
  state?: LiveState[];
609
- remove?: string[];
610
721
  lifetimeMs?: number;
722
+ upsert?: never;
723
+ remove?: never;
611
724
  }
612
- /** A live row removed: the writer removed it, its lifetime ran out, or its writer left. */
725
+ /** A live row removed: the writer removed it, its lifetime ran out, its writer left, or its entity (or a parent) was despawned (the claim is released). */
613
726
  export interface LiveRemoved {
614
727
  id: string;
615
- by: "explicit" | "timeout" | "writer_gone";
728
+ by: "explicit" | "timeout" | "writer_gone" | "despawned";
729
+ }
730
+ /**
731
+ * The authored plane's verbs (`scene.authored`): the editor's (`spawn`, `patch`, `rename`, `reparent`, `despawn`,
732
+ * `setLocked`, `txn`, `apply`) and the node author's `upsert`. Every write goes to the runtime, which decides it, and
733
+ * resolves once the entities show it; a refusal is the runtime's error. Detached: `project_inactive`. An entity is
734
+ * named by its id or by a view of it.
735
+ */
736
+ export interface SceneAuthored {
737
+ /** One atomic batch; `base`: the version its structural ops assume. */
738
+ apply(ops: SceneOp[], base?: number): Promise<void>;
739
+ /** The ops `build` makes, as one atomic batch. */
740
+ txn(build: (t: SceneTxn) => void): Promise<void>;
741
+ /** Spawns an entity; its id, once the entities show it. */
742
+ spawn(kind: SceneKind, opts: SceneSpawnOptions): Promise<string>;
743
+ patch(entity: string | SceneEntityView, set: Partial<SceneEntity>): Promise<void>;
744
+ /**
745
+ * 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.
752
+ */
753
+ upsert(kind: SceneKind, opts: SceneSpawnOptions & {
754
+ id?: never;
755
+ }): Promise<SceneEntityView>;
756
+ rename(entity: string | SceneEntityView, name: string): Promise<void>;
757
+ reparent(entity: string | SceneEntityView, parent: string | SceneEntityView | null, place?: ScenePlace): Promise<void>;
758
+ despawn(entity: string | SceneEntityView, opts?: {
759
+ cascade?: boolean;
760
+ }): Promise<void>;
761
+ setLocked(entity: string | SceneEntityView, locked: boolean): Promise<void>;
762
+ }
763
+ /**
764
+ * The live plane's verbs (`scene.live`). Decisions are calls, data is streams (PLAN v0.31): a claim is the call
765
+ * `synapse/scene/live/upsert` (`upsert`), a release the call `synapse/scene/live/remove` (`remove`); the lanes go on this
766
+ * node's live track (made on the first write of each session), synchronously like `publish` (`batch`, `view.live.set`).
767
+ * The runtime decides identity and ownership; nothing here reads the entities to decide what to send.
768
+ */
769
+ export interface SceneLive {
770
+ /**
771
+ * One batch of lanes (the sim's one call per tick); returns at once. A row goes out only for an id this node claimed in
772
+ * the bound incarnation: a live entity of its `upsert`, or the override of an entity of the scene, which the first row
773
+ * of its id asks for (the call `upsert {id}`, once). Until the answer the latest row of each lane of that id is kept
774
+ * (latest wins) and sent once the claim is granted, so a single write lands; a refusal drops them (counted in
775
+ * `status.get().unclaimedDrops` with the rows written while refused), is a node notice (`node.notices`) and is asked
776
+ * again, paced, by a later row. The runtime stamps the rows; a row it does not apply comes back on `watchLiveDenied`.
777
+ * Not attached: dropped and counted (`status.get().detachedDrops`). Closed: `not_connected`. Never sent again.
778
+ */
779
+ batch(batch: LiveBatch): void;
780
+ /**
781
+ * A live entity of this node's: the call `synapse/scene/live/upsert {kind, name?, parent, lifetime_ms?, ...components}`;
782
+ * resolves with its view (by the id the runtime answered: minted for a new one; a name this node already claimed sends
783
+ * that claim's id). Rejects with the runtime's refusal as it came (`name_taken {name, id, holder}`, `not_owner`,
784
+ * `conflict`, `invalid_tree`, `project_inactive`, `unauthorized`, ...): nothing is kept. Granted, it is this node's claim:
785
+ * claimed again, same id, on every attach (a new session, a project switch, a scene restart; a refused re-claim is
786
+ * asked again, paced, with a warning), until `remove`. A session that goes before the answer: asked at the next
787
+ * attach. Detached: `project_inactive`; the node ending: `not_connected`. An `id` component: `invalid_input`
788
+ * (`data: { component: "id" }`), nothing sent.
789
+ */
790
+ upsert(kind: SceneKind, opts?: Omit<SceneSpawnOptions, "name" | "id"> & {
791
+ name?: string;
792
+ id?: never;
793
+ }, live?: {
794
+ lifetimeMs?: number;
795
+ }): Promise<SceneEntityView>;
796
+ /** Releases this node's claim of the entity: the call `synapse/scene/live/remove {id}` (the runtime retracts its row); the claim is not made again. Not held in the bound incarnation: resolves at once. */
797
+ remove(entity: string | SceneEntityView): Promise<void>;
616
798
  }
617
799
  /**
618
800
  * The active project's scene as this node sees it (PLAN v0.24 §5): authored ⊕ live, kept current.
619
- * Views and feeds read the handle's copy of the scene; every write goes to the runtime, which
620
- * decides it, and resolves once the entities show it.
801
+ * Views and feeds read the handle's copy of the scene; the writes are in `authored` (the authored plane) and `live`
802
+ * (this node's live rows).
621
803
  */
622
804
  export interface SceneHandle extends AsyncDisposable {
623
805
  /** The entities now, authored with the live overrides applied; the same object while nothing changed. */
624
- readonly entities: Readonly<Record<string, SceneEntity>>;
806
+ get(): Readonly<Record<string, SceneEntity>>;
625
807
  /**
626
- * One entity as a view (`undefined` when it is not in the scene). `plane` reads one layer alone and
627
- * the view changes only when that layer's value for the id does: `authored` is the entity as
628
- * declared without the live lanes (the authored entity, or a live entity's upsert row); `live` is
629
- * the id's live row (`SceneEntityLive`), `undefined` without one. Without `plane`: merged.
808
+ * One entity as a view ({@link SceneEntityView}), by id or by name (`entity({ name: "tool" })`: the entity holding
809
+ * the name now): merged, authored with its live overrides; one plane alone through the view (`view.authored`, `view.live`).
630
810
  */
631
- entity(id: string, opts?: {
632
- plane?: "authored";
633
- }): SceneView<SceneEntity | undefined>;
634
- entity(id: string, opts: {
635
- plane: "live";
636
- }): SceneView<SceneEntityLive | undefined>;
637
- entity(id: string, opts?: {
638
- plane?: "authored" | "live";
639
- }): SceneView<SceneEntity | SceneEntityLive | undefined>;
811
+ entity(ref: SceneEntityRef): SceneEntityView;
640
812
  /** The hierarchy as a view. */
641
813
  tree(): SceneView<SceneTree>;
642
814
  /** The entities for which `where` holds (in id order), as a view. */
643
815
  query(where: (e: SceneEntity) => boolean): SceneView<SceneEntity[]>;
644
- readonly status: SceneStatus;
645
- /** The authored changes, one batch per applied change. */
646
- watchChanges(cb: (batch: SceneDiffOp[]) => void): () => void;
647
- watchChanges(): AsyncIterableIterator<SceneDiffOp[]>;
816
+ /** The handle's own state, a state value: `get()` now (the same object while unchanged), `onChange` for each change, `changed()` for the next. */
817
+ readonly status: StateValue<SceneStatus>;
818
+ /** The one boolean a node author checks: `status.get().synced`. */
819
+ 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. */
821
+ ready(): Promise<void>;
822
+ /** The entities after their next change (state, not history); `not_connected` once the handle is closed. */
823
+ changed(): Promise<Readonly<Record<string, SceneEntity>>>;
824
+ /** The entities after each change (state, not history: changes in between fold into the latest); ends with the handle. */
825
+ watch(): AsyncIterable<Readonly<Record<string, SceneEntity>>>;
826
+ /** `cb` with the entities on every change of the entities or the status (the framework bridge); returns the remover. */
827
+ onChange(cb: (entities: Readonly<Record<string, SceneEntity>>) => void): () => void;
648
828
  /** The live rows removed, one batch per runtime tick that removed some. */
649
829
  watchLiveRemovals(cb: (batch: LiveRemoved[]) => void): () => void;
650
830
  watchLiveRemovals(): AsyncIterableIterator<LiveRemoved[]>;
651
831
  /** The ids of this node's live writes the runtime dropped because another writer owns them, one batch per denied batch. */
652
832
  watchLiveDenied(cb: (ids: string[]) => void): () => void;
653
833
  watchLiveDenied(): AsyncIterableIterator<string[]>;
654
- /** The handle's state at each change; ends after `closed`. */
655
- watchState(cb: (state: SceneState) => void): () => void;
656
- watchState(): AsyncIterableIterator<SceneState>;
657
- /** One atomic batch; `base`: the version its structural ops assume. Resolves once the entities show it; a refusal is the runtime's error. Detached: `project_inactive`. */
658
- apply(ops: SceneOp[], base?: number): Promise<void>;
659
- /** The ops `build` makes, as one atomic batch. */
660
- txn(build: (t: SceneTxn) => void): Promise<void>;
661
- /** Spawns an entity; its id, once the entities show it. */
662
- spawn(kind: string, opts: SceneSpawnOptions): Promise<string>;
663
- patch(id: string, set: Partial<SceneEntity>): Promise<void>;
664
- rename(id: string, name: string): Promise<void>;
665
- reparent(id: string, parent: string | null, place?: ScenePlace): Promise<void>;
666
- despawn(id: string, opts?: {
667
- cascade?: boolean;
668
- }): Promise<void>;
669
- setLocked(id: string, locked: boolean): Promise<void>;
670
- /** Writes one live batch on this node's live track; returns at once (like `publish`). Not attached: dropped and counted (`status.detachedDrops`). */
671
- live(batch: LiveBatch): void;
672
- /** The framework bridge (React): called on every change of the entities or the status. */
673
- subscribe(listener: () => void): () => void;
834
+ /** The authored plane's verbs. */
835
+ readonly authored: SceneAuthored;
836
+ /** This node's live writes. */
837
+ readonly live: SceneLive;
674
838
  /** Ends this handle: its verbs fail `not_connected`, its views and feeds end; the node's other scene handles go on. */
675
839
  close(): void;
676
840
  }
@@ -748,21 +912,28 @@ export interface TaskCatalogEntry {
748
912
  /** A value the runtime keeps current that this node only reads: it has no writes. */
749
913
  export interface ReadonlyStoreHandle<V> {
750
914
  /** The value now; the same object while nothing changed, and an unchanged part keeps its object when another part changes. */
751
- readonly value: V;
915
+ get(): V;
752
916
  /** `true` while the value is current. */
753
917
  readonly synced: boolean;
754
918
  /** Resolves once the value is current (the first time). */
755
919
  ready(): Promise<void>;
756
920
  /** The whole value after each change. It is state, not history: a reader slower than the changes sees the latest value. */
757
921
  watch(): AsyncIterable<V>;
758
- /** The framework bridge (React): called on every change of the value or of `synced`. */
759
- subscribe(listener: () => void): () => void;
922
+ /** `cb` with the value on every change of the value or of `synced` (the framework bridge); returns the remover. */
923
+ onChange(cb: (value: V) => void): () => void;
760
924
  }
761
925
  /** The robot's task declarations, `instance` → declaration (operators write them). The node owns this handle: it has no `close` or `destroy`. */
762
- export interface TaskDeclarations extends Omit<StoreHandle, "name" | "value" | "get" | "set" | "watch" | "destroy" | "close" | typeof Symbol.asyncDispose> {
763
- readonly value: Readonly<Record<string, TaskDeclaration>>;
926
+ export interface TaskDeclarations extends Omit<StoreHandle, "name" | "get" | "set" | "watch" | "destroy" | "close" | typeof Symbol.asyncDispose> {
927
+ /** Every declaration, `instance` → declaration. */
928
+ get(): Readonly<Record<string, TaskDeclaration>>;
764
929
  get(instance: string): TaskDeclaration | undefined;
765
- /** `set(instance, declaration)`, or `set({ ... })` for several as one atomic write; waits for the value to be current first, resolves once `value` shows the write. */
930
+ /**
931
+ * `set(declaration)`: a new declaration under an instance the SDK mints (a ulid), resolving with that instance;
932
+ * `set(instance, declaration)` writes the one named; `set({ ... })` several as one atomic write. One object is a
933
+ * declaration when its `ref` is a string, else a map `instance` → declaration. Each waits for the value to be current
934
+ * first and resolves once `get()` shows the write.
935
+ */
936
+ set(declaration: TaskDeclaration): Promise<string>;
766
937
  set(instance: string, declaration: TaskDeclaration): Promise<void>;
767
938
  set(declarations: Record<string, TaskDeclaration>): Promise<void>;
768
939
  /** The whole value after each change; with `instance`, that declaration each time it changes (`undefined` after a delete). */
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
+ }