@synapse-sdk/client 0.3.3 → 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/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>/cluster/<cluster>/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";
@@ -220,27 +230,32 @@ export interface CallOptions<U = unknown> {
220
230
  /** Local: fail `idle_timeout` (and cancel) when no frame arrives for this long. */
221
231
  idleTimeoutMs?: number;
222
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. */
223
234
  onUpdate?: (u: U) => void;
224
235
  }
225
- /** A started call (WP §8.3): its updates, its result, and cancel. */
226
- /** `await run` is `await run.result`. */
227
- 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> {
228
244
  readonly id: string;
229
245
  readonly status: RunStatus;
230
- /** The next progress update; `undefined` once the terminal frame arrived (then `result`). */
231
- nextUpdate(): Promise<U | undefined>;
232
- 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). */
233
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>;
234
256
  /** Cooperative cancel: tells the broker, resolves on its `canceled` frame. */
235
257
  cancel(reason?: string): Promise<void>;
236
258
  }
237
- export type CallEvent<U, R> = {
238
- type: "update";
239
- data: U;
240
- } | {
241
- type: "result";
242
- data: R;
243
- };
244
259
  export interface ServeContext<U> {
245
260
  readonly call: string;
246
261
  readonly caller: string;
@@ -306,18 +321,25 @@ export type StoreChange = {
306
321
  by: string | undefined;
307
322
  };
308
323
  /**
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`).
324
+ * A namespace as a value kept current (SDK §9): `get()` reads it, `set`/`delete` resolve once the
325
+ * value shows the write, `synced` says whether it is current, `onChange`/`watch()` follow changes. The runtime
326
+ * decides every write (`not_owner`, `unauthorized`, `store_gone`). A write made while the handle is
327
+ * between incarnations (its intent being replayed, e.g. after a project activation) waits for the
328
+ * handle to bind again and is decided then; it is refused `store_gone` only if the handle ends first.
312
329
  */
313
330
  export interface StoreHandle extends AsyncDisposable {
314
331
  readonly name: string;
315
332
  /** The whole value, what the runtime has (the same object while nothing changed). */
316
- readonly value: Record<string, unknown>;
333
+ get(): Record<string, unknown>;
334
+ /** One key's value (`undefined` when absent). */
317
335
  get(key: string): unknown;
336
+ /** The one boolean a node author checks: `status.get().synced`. */
318
337
  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. */
338
+ /** The handle's own state, a state value: `get()` now (the same object while unchanged), `onChange` for each change, `changed()` for the next. */
339
+ readonly status: StateValue<StoreStatus>;
340
+ /** Resolves once the value is in: current as of the open (the version the runtime answered then applied), or the namespace absent. */
341
+ ready(): Promise<void>;
342
+ /** `set(key, value)`, or `set({ ... })` for several keys as one atomic write; resolves once `get()` shows it. */
321
343
  set(key: string, value: unknown): Promise<void>;
322
344
  set(values: Record<string, unknown>): Promise<void>;
323
345
  delete(key: string): Promise<void>;
@@ -326,8 +348,11 @@ export interface StoreHandle extends AsyncDisposable {
326
348
  destroy(): Promise<void>;
327
349
  /** The whole value after each change; with `key`, that key's value each time it changes (`undefined` after a delete). */
328
350
  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;
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. */
352
+ changed(): Promise<Record<string, unknown>>;
353
+ changed(key: string): Promise<unknown>;
354
+ /** `cb` with the whole value on every change of the value or the status (the framework bridge); returns the remover. */
355
+ onChange(cb: (value: Record<string, unknown>) => void): () => void;
331
356
  close(): void;
332
357
  }
333
358
  export type Vec3 = [number, number, number];
@@ -341,6 +366,11 @@ export interface Xform {
341
366
  }
342
367
  /** Which plane an entity lives in: `authored` (the project's), `calibration` (the robot's), `live` (never persisted). */
343
368
  export type SceneLayer = "authored" | "calibration" | "live";
369
+ /**
370
+ * What an entity is. Any string is a kind; these are offered for completion: `frame` and `robot` are the two the runtime
371
+ * gives meaning to, `model` and `scan` the common others.
372
+ */
373
+ export type SceneKind = "frame" | "robot" | "model" | "scan" | (string & {});
344
374
  /** Each flag holds only when it is `true`. `locked` refuses patch/reparent/rename/despawn (a flags-only patch passes). */
345
375
  export interface SceneFlags {
346
376
  locked?: boolean;
@@ -414,8 +444,11 @@ export interface Hardpoint {
414
444
  export interface SceneEntity {
415
445
  /** A ULID; never changes. */
416
446
  id: string;
417
- kind: string;
418
- /** Unique across the authored and calibration planes. */
447
+ kind: SceneKind;
448
+ /**
449
+ * Unique across every plane (authored, calibration, live): a write that would give an entity a name another entity
450
+ * holds is refused `name_taken`, its `data.holder` the id holding the name (`data.kind`, `data.layer`: the holder's).
451
+ */
419
452
  name?: string;
420
453
  /** The parent's id; `null` is the world root. */
421
454
  parent: string | null;
@@ -450,7 +483,7 @@ export interface SceneEntity {
450
483
  [key: string]: unknown;
451
484
  }
452
485
  /**
453
- * An id's live row (`scene.entity(id, { plane: "live" })`), each key present only when the row has it:
486
+ * An id's live row (`view.live.get()`), each key present only when the row has it:
454
487
  * `entity` is a live-layer entity's upsert row; `pose`, `joints`, `state` are the lanes; `stamp` is the
455
488
  * newest lane stamp (the runtime's clock).
456
489
  */
@@ -476,15 +509,99 @@ export interface SceneTreeNode {
476
509
  export type SceneTree = SceneTreeNode[];
477
510
  /**
478
511
  * 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.
512
+ * `onChange` for each change (the framework bridge), `changed()` for the next one, `watch()` for each one in a
513
+ * loop. It reads the handle's copy of the scene (no subscription of its own) and ends with the handle.
481
514
  */
482
515
  export interface SceneView<T> {
483
516
  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). */
517
+ /** `cb` with the value on each change; returns the remover. */
518
+ onChange(cb: (value: T) => void): () => void;
519
+ /** The value after each change (state, not history: changes in between fold into the latest); ends with the handle. */
520
+ watch(): AsyncIterable<T>;
521
+ /** 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
522
  changed(): Promise<T>;
487
523
  }
524
+ /** Which entity an entity view follows: an id (that entity only), or a name (whichever entity holds it now, on any plane). */
525
+ export type SceneEntityRef = string | {
526
+ name: string;
527
+ };
528
+ /**
529
+ * One entity as a view (`scene.entity(id)`, `scene.entity({ name })`), shaped like a store handle: `get()` is the
530
+ * merged entity now (authored with its live overrides; `undefined` when there is none), `get(component)` one of its
531
+ * components; `watch()` follows it. It is a `SceneView` too (`onChange`, `changed()`), so it fits wherever a view
532
+ * does. The scene's two planes are its two namespaces: `authored` (`get`, `set`: the entity as declared, one patch)
533
+ * and `live` (`get`: the live row with its `stamp`; `set`: the lanes; `remove`: the row). They have no `watch`,
534
+ * `onChange` or `changed` of their own: the view's cover the entity, whichever plane changed.
535
+ *
536
+ * A view by name follows the name: when the entity holding it is despawned and another takes the name, the view
537
+ * reads the new one (a view by id reads `undefined` from then on).
538
+ */
539
+ export interface SceneEntityView extends SceneView<SceneEntity | undefined> {
540
+ /**
541
+ * The id of the entity the view names now: a view by id (what `upsert` returns), its id; a view by name, the id
542
+ * holding the name now (none holds it: `not_found`, `data: { name }`).
543
+ */
544
+ readonly id: string;
545
+ /** The merged entity now; `undefined` when there is none (absent, or the handle detached or closed). */
546
+ get(): SceneEntity | undefined;
547
+ /** One component of the merged entity now (`get("xform")`); `undefined` when the entity or the component is absent. */
548
+ get<K extends keyof SceneEntity & string>(component: K): SceneEntity[K] | undefined;
549
+ /** The entity's authored plane: one object for the view's life. */
550
+ readonly authored: EntityAuthored;
551
+ /** The entity's live plane: one object for the view's life. */
552
+ readonly live: EntityLive;
553
+ /**
554
+ * The entity after each change (`undefined` once it is gone); with `component`, that component each time it
555
+ * changes. It is state, not history: changes in between are folded, the latest wins. Ends with the handle.
556
+ */
557
+ watch(): AsyncIterable<SceneEntity | undefined>;
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>;
562
+ }
563
+ /** An entity view's authored plane (`view.authored`). Reads as the view does: nothing once the handle is detached or closed. */
564
+ export interface EntityAuthored {
565
+ /** 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. */
566
+ get(): SceneEntity | undefined;
567
+ get<K extends keyof SceneEntity & string>(component: K): SceneEntity[K] | undefined;
568
+ /**
569
+ * `set(component, value)`, or `set({ ...components })` for several: one `patch` of the authored entity the view
570
+ * names now (by name: the id holding the name at the call), resolved once the entities show it; the runtime's
571
+ * refusal is the error. A view by id sends the patch as `scene.authored.patch(id, …)` does (an id the runtime does
572
+ * not have: its `not_found`). A view by name whose name no entity holds has no id to send to: `not_found`
573
+ * (`data: { name }`), nothing sent. Detached: `project_inactive`.
574
+ */
575
+ set<K extends keyof SceneEntity & string>(component: K, value: SceneEntity[K]): Promise<void>;
576
+ set(components: Partial<SceneEntity>): Promise<void>;
577
+ }
578
+ /** An entity view's live plane (`view.live`). Reads as the view does: nothing once the handle is detached or closed. */
579
+ export interface EntityLive {
580
+ /** The entity's live row ({@link SceneEntityLive}, its `stamp` included); `undefined` without one. */
581
+ get(): SceneEntityLive | undefined;
582
+ get<K extends keyof SceneEntityLive & string>(component: K): SceneEntityLive[K] | undefined;
583
+ /**
584
+ * Writes the live lanes of the entity the view names now, as one `scene.live.batch` (synchronous, like `publish`):
585
+ * `xform: { t, q }` the pose lane (both required, nothing else), `joints` the joints lane, `state` the state lane; any
586
+ * other component is `invalid_input`. `lifetimeMs`: the override expires that long after its last write (the entity's
587
+ * authored values show again); a live entity keeps the lifetime it was upserted with. The first write for an id this
588
+ * node has not claimed asks the claim (see `scene.live.batch`), so a single write lands. A view by name whose name no
589
+ * entity holds: `not_found`. Not attached: dropped and counted (`status.get().detachedDrops`). Closed, or the node
590
+ * ended: `not_connected`.
591
+ */
592
+ set(components: {
593
+ xform?: {
594
+ t: Vec3;
595
+ q: Quat;
596
+ };
597
+ joints?: Record<string, number>;
598
+ state?: Record<string, unknown>;
599
+ }, opts?: {
600
+ lifetimeMs?: number;
601
+ }): void;
602
+ /** Releases this node's claim of the entity the view names now (`scene.live.remove(id)`, the call): the runtime retracts its live row. */
603
+ remove(): Promise<void>;
604
+ }
488
605
  /** `attached`: the scene of the active project is in; `detached`: none is (no project active, a session gap, between incarnations); `closed`: for good. */
489
606
  export type SceneState = "attached" | "detached" | "closed";
490
607
  export interface SceneStatus {
@@ -496,10 +613,12 @@ export interface SceneStatus {
496
613
  /** The authored plane's version and hash as this handle has them (0 and "" while detached). */
497
614
  version: number;
498
615
  hash: string;
499
- /** `live()` batches dropped because the handle was not attached. */
616
+ /** Live writes (`scene.live.batch`, `view.live.set`) dropped because the handle was not attached. */
500
617
  detachedDrops: number;
618
+ /** Lane rows dropped because their id's claim was refused (rows parked while it was asked, and rows written while refused). */
619
+ unclaimedDrops: number;
501
620
  }
502
- /** An authored change as the runtime applied it (the `watchChanges` feed). */
621
+ /** An authored change as the runtime applied it (a diff op of the scene's `authored` track). */
503
622
  export type SceneDiffOp = {
504
623
  op: "spawn";
505
624
  entity: SceneEntity;
@@ -563,27 +682,27 @@ export type SceneOp = {
563
682
  */
564
683
  export type SceneSpawnOptions = {
565
684
  name: string;
566
- parent?: string | null;
685
+ parent?: string | SceneEntityView | null;
567
686
  id?: string;
568
687
  } & Partial<Omit<SceneEntity, "id" | "kind" | "name" | "parent" | "order" | "meta">>;
569
- /** Where `reparent` puts the entity among its new siblings (default: after the last). */
688
+ /** Where `reparent` puts the entity among its new siblings (default: after the last): a sibling by its id or its view. */
570
689
  export type ScenePlace = {
571
- before: string;
690
+ before: string | SceneEntityView;
572
691
  } | {
573
- after: string;
692
+ after: string | SceneEntityView;
574
693
  };
575
- /** One authoring batch being built (`txn`): sent as one atomic `apply`. */
694
+ /** 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
695
  export interface SceneTxn {
577
696
  /** 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?: {
697
+ spawn(kind: SceneKind, opts: SceneSpawnOptions): string;
698
+ /** Sets components of the entity (an object merges into an object one level deep). */
699
+ patch(entity: string | SceneEntityView, set: Partial<SceneEntity>): void;
700
+ rename(entity: string | SceneEntityView, name: string): void;
701
+ reparent(entity: string | SceneEntityView, parent: string | SceneEntityView | null, place?: ScenePlace): void;
702
+ despawn(entity: string | SceneEntityView, opts?: {
584
703
  cascade?: boolean;
585
704
  }): void;
586
- setLocked(id: string, locked: boolean): void;
705
+ setLocked(entity: string | SceneEntityView, locked: boolean): void;
587
706
  /** The ops as they will be sent. */
588
707
  readonly ops: readonly SceneOp[];
589
708
  }
@@ -600,77 +719,133 @@ export interface LiveState {
600
719
  id: string;
601
720
  s: Record<string, unknown>;
602
721
  }
603
- /** One live batch: live entities (`layer: "live"`), lane overrides, removals; `lifetimeMs` makes the override rows it touches expire. */
722
+ /**
723
+ * One live batch: data only, the lanes (`poses`, `joints`, `state`). `lifetimeMs` makes the override rows the batch
724
+ * touches expire. Claims are calls, never rows: `scene.live.upsert` and `scene.live.remove`.
725
+ */
604
726
  export interface LiveBatch {
605
- upsert?: SceneEntity[];
606
727
  poses?: LivePose[];
607
728
  joints?: LiveJoints[];
608
729
  state?: LiveState[];
609
- remove?: string[];
610
730
  lifetimeMs?: number;
731
+ upsert?: never;
732
+ remove?: never;
611
733
  }
612
- /** A live row removed: the writer removed it, its lifetime ran out, or its writer left. */
734
+ /** 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
735
  export interface LiveRemoved {
614
736
  id: string;
615
- by: "explicit" | "timeout" | "writer_gone";
737
+ by: "explicit" | "timeout" | "writer_gone" | "despawned";
738
+ }
739
+ /**
740
+ * The authored plane's verbs (`scene.authored`): the editor's (`spawn`, `patch`, `rename`, `reparent`, `despawn`,
741
+ * `setLocked`, `txn`, `apply`) and the node author's `upsert`. Every write goes to the runtime, which decides it, and
742
+ * resolves once the entities show it; a refusal is the runtime's error. Detached: `project_inactive`. An entity is
743
+ * named by its id or by a view of it.
744
+ */
745
+ export interface SceneAuthored {
746
+ /** One atomic batch; `base`: the version its structural ops assume. */
747
+ apply(ops: SceneOp[], base?: number): Promise<void>;
748
+ /** The ops `build` makes, as one atomic batch. */
749
+ txn(build: (t: SceneTxn) => void): Promise<void>;
750
+ /** Spawns an entity; its id, once the entities show it. */
751
+ spawn(kind: SceneKind, opts: SceneSpawnOptions): Promise<string>;
752
+ patch(entity: string | SceneEntityView, set: Partial<SceneEntity>): Promise<void>;
753
+ /**
754
+ * A builder's one verb, keyed by `name`; resolves with the entity's view (by id: that entity) once the entities show
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.
763
+ */
764
+ upsert(kind: SceneKind, opts: SceneSpawnOptions & {
765
+ id?: never;
766
+ }): Promise<SceneEntityView>;
767
+ rename(entity: string | SceneEntityView, name: string): Promise<void>;
768
+ reparent(entity: string | SceneEntityView, parent: string | SceneEntityView | null, place?: ScenePlace): Promise<void>;
769
+ despawn(entity: string | SceneEntityView, opts?: {
770
+ cascade?: boolean;
771
+ }): Promise<void>;
772
+ setLocked(entity: string | SceneEntityView, locked: boolean): Promise<void>;
773
+ }
774
+ /**
775
+ * The live plane's verbs (`scene.live`). Decisions are calls, data is streams (PLAN v0.31): a claim is the call
776
+ * `synapse/scene/live/upsert` (`upsert`), a release the call `synapse/scene/live/remove` (`remove`); the lanes go on this
777
+ * node's live track (made on the first write of each session), synchronously like `publish` (`batch`, `view.live.set`).
778
+ * The runtime decides identity and ownership; nothing here reads the entities to decide what to send.
779
+ */
780
+ export interface SceneLive {
781
+ /**
782
+ * 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
783
+ * the bound incarnation: a live entity of its `upsert`, or the override of an entity of the scene, which the first row
784
+ * of its id asks for (the call `upsert {id}`, once). Until the answer the latest row of each lane of that id is kept
785
+ * (latest wins) and sent once the claim is granted, so a single write lands; a refusal drops them (counted in
786
+ * `status.get().unclaimedDrops` with the rows written while refused), is a node notice (`node.notices`) and is asked
787
+ * again, paced, by a later row. The runtime stamps the rows; a row it does not apply comes back on `watchLiveDenied`.
788
+ * Not attached: dropped and counted (`status.get().detachedDrops`). Closed: `not_connected`. Never sent again.
789
+ */
790
+ batch(batch: LiveBatch): void;
791
+ /**
792
+ * A live entity of this node's: the call `synapse/scene/live/upsert {kind, name?, parent, lifetime_ms?, ...components}`;
793
+ * resolves with its view (by the id the runtime answered: minted for a new one; a name this node already claimed sends
794
+ * that claim's id). Rejects with the runtime's refusal as it came (`name_taken {name, id, holder}`, `not_owner`,
795
+ * `conflict`, `invalid_tree`, `project_inactive`, `unauthorized`, ...): nothing is kept. Granted, it is this node's claim:
796
+ * claimed again, same id, on every attach (a new session, a project switch, a scene restart; a refused re-claim is
797
+ * asked again, paced, with a warning), until `remove`. A session that goes before the answer: asked at the next
798
+ * attach. Detached: `project_inactive`; the node ending: `not_connected`. An `id` component: `invalid_input`
799
+ * (`data: { component: "id" }`), nothing sent.
800
+ */
801
+ upsert(kind: SceneKind, opts?: Omit<SceneSpawnOptions, "name" | "id"> & {
802
+ name?: string;
803
+ id?: never;
804
+ }, live?: {
805
+ lifetimeMs?: number;
806
+ }): Promise<SceneEntityView>;
807
+ /** 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. */
808
+ remove(entity: string | SceneEntityView): Promise<void>;
616
809
  }
617
810
  /**
618
811
  * 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.
812
+ * Views and feeds read the handle's copy of the scene; the writes are in `authored` (the authored plane) and `live`
813
+ * (this node's live rows).
621
814
  */
622
815
  export interface SceneHandle extends AsyncDisposable {
623
816
  /** The entities now, authored with the live overrides applied; the same object while nothing changed. */
624
- readonly entities: Readonly<Record<string, SceneEntity>>;
817
+ get(): Readonly<Record<string, SceneEntity>>;
625
818
  /**
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.
819
+ * One entity as a view ({@link SceneEntityView}), by id or by name (`entity({ name: "tool" })`: the entity holding
820
+ * the name now): merged, authored with its live overrides; one plane alone through the view (`view.authored`, `view.live`).
630
821
  */
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>;
822
+ entity(ref: SceneEntityRef): SceneEntityView;
640
823
  /** The hierarchy as a view. */
641
824
  tree(): SceneView<SceneTree>;
642
825
  /** The entities for which `where` holds (in id order), as a view. */
643
826
  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[]>;
827
+ /** The handle's own state, a state value: `get()` now (the same object while unchanged), `onChange` for each change, `changed()` for the next. */
828
+ readonly status: StateValue<SceneStatus>;
829
+ /** The one boolean a node author checks: `status.get().synced`. */
830
+ readonly synced: boolean;
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. */
832
+ ready(): Promise<void>;
833
+ /** The entities after their next change (state, not history); `not_connected` once the handle is closed. */
834
+ changed(): Promise<Readonly<Record<string, SceneEntity>>>;
835
+ /** The entities after each change (state, not history: changes in between fold into the latest); ends with the handle. */
836
+ watch(): AsyncIterable<Readonly<Record<string, SceneEntity>>>;
837
+ /** `cb` with the entities on every change of the entities or the status (the framework bridge); returns the remover. */
838
+ onChange(cb: (entities: Readonly<Record<string, SceneEntity>>) => void): () => void;
648
839
  /** The live rows removed, one batch per runtime tick that removed some. */
649
840
  watchLiveRemovals(cb: (batch: LiveRemoved[]) => void): () => void;
650
841
  watchLiveRemovals(): AsyncIterableIterator<LiveRemoved[]>;
651
842
  /** The ids of this node's live writes the runtime dropped because another writer owns them, one batch per denied batch. */
652
843
  watchLiveDenied(cb: (ids: string[]) => void): () => void;
653
844
  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;
845
+ /** The authored plane's verbs. */
846
+ readonly authored: SceneAuthored;
847
+ /** This node's live writes. */
848
+ readonly live: SceneLive;
674
849
  /** Ends this handle: its verbs fail `not_connected`, its views and feeds end; the node's other scene handles go on. */
675
850
  close(): void;
676
851
  }
@@ -748,21 +923,28 @@ export interface TaskCatalogEntry {
748
923
  /** A value the runtime keeps current that this node only reads: it has no writes. */
749
924
  export interface ReadonlyStoreHandle<V> {
750
925
  /** The value now; the same object while nothing changed, and an unchanged part keeps its object when another part changes. */
751
- readonly value: V;
926
+ get(): V;
752
927
  /** `true` while the value is current. */
753
928
  readonly synced: boolean;
754
929
  /** Resolves once the value is current (the first time). */
755
930
  ready(): Promise<void>;
756
931
  /** The whole value after each change. It is state, not history: a reader slower than the changes sees the latest value. */
757
932
  watch(): AsyncIterable<V>;
758
- /** The framework bridge (React): called on every change of the value or of `synced`. */
759
- subscribe(listener: () => void): () => void;
933
+ /** `cb` with the value on every change of the value or of `synced` (the framework bridge); returns the remover. */
934
+ onChange(cb: (value: V) => void): () => void;
760
935
  }
761
936
  /** 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>>;
937
+ export interface TaskDeclarations extends Omit<StoreHandle, "name" | "get" | "set" | "watch" | "destroy" | "close" | typeof Symbol.asyncDispose> {
938
+ /** Every declaration, `instance` → declaration. */
939
+ get(): Readonly<Record<string, TaskDeclaration>>;
764
940
  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. */
941
+ /**
942
+ * `set(declaration)`: a new declaration under an instance the SDK mints (a ulid), resolving with that instance;
943
+ * `set(instance, declaration)` writes the one named; `set({ ... })` several as one atomic write. One object is a
944
+ * declaration when its `ref` is a string, else a map `instance` → declaration. Each waits for the value to be current
945
+ * first and resolves once `get()` shows the write.
946
+ */
947
+ set(declaration: TaskDeclaration): Promise<string>;
766
948
  set(instance: string, declaration: TaskDeclaration): Promise<void>;
767
949
  set(declarations: Record<string, TaskDeclaration>): Promise<void>;
768
950
  /** The whole value after each change; with `instance`, that declaration each time it changes (`undefined` after a delete). */