@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/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { Card, ClientType, Delivery, FecParams, MediaFrame, Role, ServiceRef, StoreMode, StoreOnDeath, StoreRow, TimeDomain, TopicRef, TrackDesc, Tlv, JsonSchema } from "@synapse-sdk/core";
2
- import type { SynapseError } from "@synapse-sdk/core";
1
+ import type { Card, ClientType, Delivery, FecParams, Role, ServiceRef, StoreMode, StoreOnDeath, StoreRow, TimeDomain, TopicRef, TrackDesc, Tlv, JsonSchema } from "@synapse-sdk/core";
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,24 +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
- /** `https://host:port/<root>`. */
17
- url: string;
18
18
  /** This node's type (WP §3.1): bounds the role it may exercise; the runtime grants the role. */
19
19
  type: ClientType;
20
20
  /** This node's name `[a-z0-9_-]{1,64}`; minted (`<type>-<6 hex>`) when absent and kept across reconnects. */
21
21
  name?: string;
22
- /** 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. */
23
23
  userId?: string;
24
24
  /** Reserved for signed credentials. */
25
25
  credential?: string;
26
- /** Pin of the broker's self-signed certificate (SHA-256 hex). Absent: `SYNAPSE_BROKER_CERT_SHA256`, then the broker's helper, then unpinned. */
27
- brokerCertSha256?: string;
28
26
  /** Time-domain id override (WP §7.3); default per runtime. */
29
27
  domId?: string;
30
- /** Standing project intent (`&project=`, WP §3.5). */
28
+ /** Standing project intent (WP §3.5), sent with the connect request. */
31
29
  project?: "operator";
32
- /** `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. */
33
31
  reconnect?: ReconnectPolicy | null;
34
32
  transport?: Transport;
35
33
  connectTimeoutMs?: number;
@@ -38,16 +36,34 @@ export interface ConnectOptions {
38
36
  /** Bun/Node: close cleanly on SIGINT/SIGTERM (default true). */
39
37
  signals?: boolean;
40
38
  }
41
- /** 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). */
42
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
+ */
43
45
  url: string;
46
+ /** The type to declare there; left out keeps the node's current one. */
44
47
  type?: ClientType;
48
+ /** The name to declare there; left out keeps the node's current one. */
45
49
  name?: string;
50
+ /** The user id to declare there; left out keeps the node's current one. */
46
51
  userId?: string;
52
+ /** The credential to present there; left out keeps the node's current one. Never printed. */
47
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). */
48
55
  brokerCertSha256?: string;
56
+ /** The relay token for *this* target; left out = no token (a token for another runtime would be refused). Never printed. */
57
+ token?: string;
49
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
+ */
50
64
  export type ConnectionState = {
65
+ status: "idle";
66
+ } | {
51
67
  status: "connecting";
52
68
  attempt: number;
53
69
  } | {
@@ -79,7 +95,9 @@ export interface Info {
79
95
  dom: TimeDomain;
80
96
  brokerDom: string;
81
97
  brokerVersion: string; /** The robot the runtime serves (fixed for the runtime's life; empty between sessions). */
82
- robot: string;
98
+ robot: string; /** The runtime's name in its cluster, from the current session's hello (empty between sessions). */
99
+ runtimeName: string; /** The runtime's id, from the current session's hello (empty between sessions): kept across the runtime's restarts. */
100
+ runtimeId: string;
83
101
  url: string;
84
102
  }
85
103
  /** A `promoted`/`demoted` notice on the session (WP §3.5). */
@@ -93,7 +111,7 @@ export type Notice = {
93
111
  project: string | null;
94
112
  };
95
113
  export interface PublishOptions {
96
- /** The frame's `t_us` (this node's clock); default now. */
114
+ /** The frame's `t_us` (this node's clock): the capture time, if you have one; default now. */
97
115
  tUs?: number;
98
116
  flags?: number;
99
117
  }
@@ -121,12 +139,13 @@ export interface Publisher<TIn> extends AsyncDisposable {
121
139
  readonly verdict: Verdict;
122
140
  /** Resolves once admitted and announced on the current session; `not_connected` once the node ended. A refusal is asked again, paced and warned about. */
123
141
  ready(): Promise<void>;
124
- /** Synchronous: writes to the installed track, or drops and counts while unbound (SDK §12.4). */
142
+ /**
143
+ * Synchronous: writes to the installed track, or drops and counts while unbound (SDK §12.4). A
144
+ * msgpack topic takes a value; a raw topic takes bytes, sent as given; on a `video/*` topic the
145
+ * bytes are one access unit (Annex-B for `video/h264`), sent as a `MEDIA` frame whose
146
+ * `KEY`/`CONFIG`/`RECOVERY` flags are read off the bitstream.
147
+ */
125
148
  publish(value: TIn, opts?: PublishOptions): void;
126
- /** Raw topics: any frame type, flags and TLVs (WP §4.1); `MEDIA` frames via `publishMedia`. */
127
- publishFrame(type: number, flags: number, tUs: number, tlvs: Tlv[], body: Uint8Array): void;
128
- /** Raw topics: a media access unit (`MEDIA`, flags from the frame, `t_us` = capture time, `STAGES` when stamped; a `sentUs` of 0 is stamped at the write). */
129
- publishMedia(frame: MediaFrame): void;
130
149
  stats(): PublisherStats;
131
150
  close(): void;
132
151
  }
@@ -141,6 +160,12 @@ export interface Envelope<T> {
141
160
  flags: number;
142
161
  type: number;
143
162
  tlvs: Tlv[];
163
+ /** Flag `KEY`: an IDR, a decoder may start here (video). */
164
+ key: boolean;
165
+ /** Flag `CONFIG`: the access unit carries SPS/PPS (video). */
166
+ config: boolean;
167
+ /** Flag `RECOVERY`: a recovery point, a gradual start point (video). */
168
+ recovery: boolean;
144
169
  }
145
170
  export interface SubscribeOptions {
146
171
  /** Deliver at most one message per interval, newest wins (skipped counted `coalesced`). */
@@ -187,12 +212,15 @@ export interface ViewOptions<T, S> extends SubscribeOptions {
187
212
  staleTime?: number;
188
213
  select?: (v: T) => S;
189
214
  }
190
- /** 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()`. */
191
216
  export interface View<S> extends AsyncDisposable {
192
217
  get(): ViewState<S>;
193
- subscribe(listener: () => void): () => void;
218
+ /** `cb` with the state on each change; returns the remover. */
219
+ onChange(cb: (state: ViewState<S>) => void): () => void;
194
220
  ready(): Promise<void>;
195
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>>;
196
224
  close(): void;
197
225
  }
198
226
  export type RunStatus = "pending" | "running" | "succeeded" | "failed" | "canceled";
@@ -288,18 +316,25 @@ export type StoreChange = {
288
316
  by: string | undefined;
289
317
  };
290
318
  /**
291
- * A namespace as a value kept current (SDK §9): `value`/`get` read it, `set`/`delete` resolve once the
292
- * value shows the write, `synced` says whether it is current, `watch()` follows changes. The runtime
293
- * 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.
294
324
  */
295
325
  export interface StoreHandle extends AsyncDisposable {
296
326
  readonly name: string;
297
327
  /** The whole value, what the runtime has (the same object while nothing changed). */
298
- readonly value: Record<string, unknown>;
328
+ get(): Record<string, unknown>;
329
+ /** One key's value (`undefined` when absent). */
299
330
  get(key: string): unknown;
331
+ /** The one boolean a node author checks: `status.get().synced`. */
300
332
  readonly synced: boolean;
301
- readonly status: StoreStatus;
302
- /** `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. */
303
338
  set(key: string, value: unknown): Promise<void>;
304
339
  set(values: Record<string, unknown>): Promise<void>;
305
340
  delete(key: string): Promise<void>;
@@ -308,8 +343,10 @@ export interface StoreHandle extends AsyncDisposable {
308
343
  destroy(): Promise<void>;
309
344
  /** The whole value after each change; with `key`, that key's value each time it changes (`undefined` after a delete). */
310
345
  watch(key?: string): AsyncIterable<unknown>;
311
- /** The framework bridge (React): called on every change of the value or the status. */
312
- 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;
313
350
  close(): void;
314
351
  }
315
352
  export type Vec3 = [number, number, number];
@@ -323,6 +360,11 @@ export interface Xform {
323
360
  }
324
361
  /** Which plane an entity lives in: `authored` (the project's), `calibration` (the robot's), `live` (never persisted). */
325
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 & {});
326
368
  /** Each flag holds only when it is `true`. `locked` refuses patch/reparent/rename/despawn (a flags-only patch passes). */
327
369
  export interface SceneFlags {
328
370
  locked?: boolean;
@@ -396,8 +438,11 @@ export interface Hardpoint {
396
438
  export interface SceneEntity {
397
439
  /** A ULID; never changes. */
398
440
  id: string;
399
- kind: string;
400
- /** 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
+ */
401
446
  name?: string;
402
447
  /** The parent's id; `null` is the world root. */
403
448
  parent: string | null;
@@ -417,6 +462,9 @@ export interface SceneEntity {
417
462
  /** Presentation/status the live plane sets per entity. */
418
463
  state?: Record<string, unknown>;
419
464
  hardpoints?: Hardpoint[];
465
+ /** The entity's tasks (`instance` → declaration): what the runtime runs for it while the project's tasks run. */
466
+ tasks?: Record<string, TaskDeclaration>;
467
+ /** Passed through untouched; the runtime does not read it (`tasks` supersedes it). */
420
468
  behaviors?: Record<string, unknown>;
421
469
  possession?: Record<string, unknown>;
422
470
  labels?: string[];
@@ -429,7 +477,7 @@ export interface SceneEntity {
429
477
  [key: string]: unknown;
430
478
  }
431
479
  /**
432
- * 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:
433
481
  * `entity` is a live-layer entity's upsert row; `pose`, `joints`, `state` are the lanes; `stamp` is the
434
482
  * newest lane stamp (the runtime's clock).
435
483
  */
@@ -455,15 +503,96 @@ export interface SceneTreeNode {
455
503
  export type SceneTree = SceneTreeNode[];
456
504
  /**
457
505
  * A value of the scene, kept current: `get()` the value now (the same object until it changes),
458
- * `subscribe` for each change (the framework bridge), `changed()` for the next one. It reads the
459
- * 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.
460
508
  */
461
509
  export interface SceneView<T> {
462
510
  get(): T;
463
- subscribe(listener: () => void): () => void;
464
- /** 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). */
465
516
  changed(): Promise<T>;
466
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
+ }
467
596
  /** `attached`: the scene of the active project is in; `detached`: none is (no project active, a session gap, between incarnations); `closed`: for good. */
468
597
  export type SceneState = "attached" | "detached" | "closed";
469
598
  export interface SceneStatus {
@@ -475,10 +604,12 @@ export interface SceneStatus {
475
604
  /** The authored plane's version and hash as this handle has them (0 and "" while detached). */
476
605
  version: number;
477
606
  hash: string;
478
- /** `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. */
479
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;
480
611
  }
481
- /** 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). */
482
613
  export type SceneDiffOp = {
483
614
  op: "spawn";
484
615
  entity: SceneEntity;
@@ -542,27 +673,27 @@ export type SceneOp = {
542
673
  */
543
674
  export type SceneSpawnOptions = {
544
675
  name: string;
545
- parent?: string | null;
676
+ parent?: string | SceneEntityView | null;
546
677
  id?: string;
547
678
  } & Partial<Omit<SceneEntity, "id" | "kind" | "name" | "parent" | "order" | "meta">>;
548
- /** 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. */
549
680
  export type ScenePlace = {
550
- before: string;
681
+ before: string | SceneEntityView;
551
682
  } | {
552
- after: string;
683
+ after: string | SceneEntityView;
553
684
  };
554
- /** 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). */
555
686
  export interface SceneTxn {
556
687
  /** Spawns an entity of `kind`; returns its id. */
557
- spawn(kind: string, opts: SceneSpawnOptions): string;
558
- /** Sets components of `id` (an object merges into an object one level deep). */
559
- patch(id: string, set: Partial<SceneEntity>): void;
560
- rename(id: string, name: string): void;
561
- reparent(id: string, parent: string | null, place?: ScenePlace): void;
562
- 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?: {
563
694
  cascade?: boolean;
564
695
  }): void;
565
- setLocked(id: string, locked: boolean): void;
696
+ setLocked(entity: string | SceneEntityView, locked: boolean): void;
566
697
  /** The ops as they will be sent. */
567
698
  readonly ops: readonly SceneOp[];
568
699
  }
@@ -579,80 +710,261 @@ export interface LiveState {
579
710
  id: string;
580
711
  s: Record<string, unknown>;
581
712
  }
582
- /** 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
+ */
583
717
  export interface LiveBatch {
584
- upsert?: SceneEntity[];
585
718
  poses?: LivePose[];
586
719
  joints?: LiveJoints[];
587
720
  state?: LiveState[];
588
- remove?: string[];
589
721
  lifetimeMs?: number;
722
+ upsert?: never;
723
+ remove?: never;
590
724
  }
591
- /** 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). */
592
726
  export interface LiveRemoved {
593
727
  id: string;
594
- 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>;
595
798
  }
596
799
  /**
597
800
  * The active project's scene as this node sees it (PLAN v0.24 §5): authored ⊕ live, kept current.
598
- * Views and feeds read the handle's copy of the scene; every write goes to the runtime, which
599
- * 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).
600
803
  */
601
804
  export interface SceneHandle extends AsyncDisposable {
602
805
  /** The entities now, authored with the live overrides applied; the same object while nothing changed. */
603
- readonly entities: Readonly<Record<string, SceneEntity>>;
806
+ get(): Readonly<Record<string, SceneEntity>>;
604
807
  /**
605
- * One entity as a view (`undefined` when it is not in the scene). `plane` reads one layer alone and
606
- * the view changes only when that layer's value for the id does: `authored` is the entity as
607
- * declared without the live lanes (the authored entity, or a live entity's upsert row); `live` is
608
- * 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`).
609
810
  */
610
- entity(id: string, opts?: {
611
- plane?: "authored";
612
- }): SceneView<SceneEntity | undefined>;
613
- entity(id: string, opts: {
614
- plane: "live";
615
- }): SceneView<SceneEntityLive | undefined>;
616
- entity(id: string, opts?: {
617
- plane?: "authored" | "live";
618
- }): SceneView<SceneEntity | SceneEntityLive | undefined>;
811
+ entity(ref: SceneEntityRef): SceneEntityView;
619
812
  /** The hierarchy as a view. */
620
813
  tree(): SceneView<SceneTree>;
621
814
  /** The entities for which `where` holds (in id order), as a view. */
622
815
  query(where: (e: SceneEntity) => boolean): SceneView<SceneEntity[]>;
623
- readonly status: SceneStatus;
624
- /** The authored changes, one batch per applied change. */
625
- watchChanges(cb: (batch: SceneDiffOp[]) => void): () => void;
626
- 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;
627
828
  /** The live rows removed, one batch per runtime tick that removed some. */
628
829
  watchLiveRemovals(cb: (batch: LiveRemoved[]) => void): () => void;
629
830
  watchLiveRemovals(): AsyncIterableIterator<LiveRemoved[]>;
630
831
  /** The ids of this node's live writes the runtime dropped because another writer owns them, one batch per denied batch. */
631
832
  watchLiveDenied(cb: (ids: string[]) => void): () => void;
632
833
  watchLiveDenied(): AsyncIterableIterator<string[]>;
633
- /** The handle's state at each change; ends after `closed`. */
634
- watchState(cb: (state: SceneState) => void): () => void;
635
- watchState(): AsyncIterableIterator<SceneState>;
636
- /** 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`. */
637
- apply(ops: SceneOp[], base?: number): Promise<void>;
638
- /** The ops `build` makes, as one atomic batch. */
639
- txn(build: (t: SceneTxn) => void): Promise<void>;
640
- /** Spawns an entity; its id, once the entities show it. */
641
- spawn(kind: string, opts: SceneSpawnOptions): Promise<string>;
642
- patch(id: string, set: Partial<SceneEntity>): Promise<void>;
643
- rename(id: string, name: string): Promise<void>;
644
- reparent(id: string, parent: string | null, place?: ScenePlace): Promise<void>;
645
- despawn(id: string, opts?: {
646
- cascade?: boolean;
647
- }): Promise<void>;
648
- setLocked(id: string, locked: boolean): Promise<void>;
649
- /** Writes one live batch on this node's live track; returns at once (like `publish`). Not attached: dropped and counted (`status.detachedDrops`). */
650
- live(batch: LiveBatch): void;
651
- /** The framework bridge (React): called on every change of the entities or the status. */
652
- subscribe(listener: () => void): () => void;
834
+ /** The authored plane's verbs. */
835
+ readonly authored: SceneAuthored;
836
+ /** This node's live writes. */
837
+ readonly live: SceneLive;
653
838
  /** Ends this handle: its verbs fail `not_connected`, its views and feeds end; the node's other scene handles go on. */
654
839
  close(): void;
655
840
  }
841
+ /**
842
+ * A declaration: what the runtime runs under an instance name (the robot's: a key of
843
+ * `tasks.declarations`; a project's: an entry of an entity's `tasks`). Whether it has a process to
844
+ * run is the catalog's: a task whose spec has nothing to run is passive (counted, never started).
845
+ */
846
+ export interface TaskDeclaration {
847
+ /** The task's address, as the task tool lists it (`<workspace>:<pkg>:<task>`); never split. */
848
+ ref: string;
849
+ /** `false`: left out of the next run. */
850
+ enabled?: boolean;
851
+ /** Passed to the task: as flags, positionals or environment variables per its spec, and whole as `SYNAPSE_CONFIG` (JSON). */
852
+ config?: Record<string, unknown>;
853
+ /** The catalog's `schemaHash` this declaration was written against; another one fails the run. */
854
+ schema?: string;
855
+ }
856
+ /** How a task's last process ended: its exit code, the signal that ended it, or neither. Times are Unix ms. */
857
+ export type TaskExit = {
858
+ code: number;
859
+ at: number;
860
+ } | {
861
+ signal: number;
862
+ at: number;
863
+ } | {
864
+ at: number;
865
+ };
866
+ /** One running task, as the runtime shows it. */
867
+ export interface TaskStatus {
868
+ scope: "robot" | "project";
869
+ /** The task's address (its declaration's `ref`). */
870
+ addr: string;
871
+ instance: string;
872
+ /** Project tasks: the entity's id. */
873
+ entity?: string;
874
+ /** `down`: its process exited and its restart is pending. */
875
+ state: "up" | "down";
876
+ /** Unix ms of the last change between `up` and `down`. */
877
+ since: number;
878
+ /** How many times its process was started again after an exit. */
879
+ restarts: number;
880
+ /** How its last process ended. */
881
+ exit?: TaskExit;
882
+ /** The file its stdout and stderr go to. */
883
+ log: string;
884
+ }
885
+ /** A scope's run and its tasks. */
886
+ export interface TasksRunStatus<T> {
887
+ /** `undefined`: the runtime has not said yet (not synced, or between sessions). */
888
+ state: "starting" | "running" | "stopping" | "stopped" | undefined;
889
+ /** Unix ms of the last change of `state`. */
890
+ since?: number;
891
+ /** Why it stopped or failed: `project_switch`, or an error code (`preflight_failed`, `launch_failed`, ...). */
892
+ reason?: string;
893
+ tasks: T;
894
+ }
895
+ /** What runs, both scopes: the robot's tasks by instance, the project's by entity id, then instance. */
896
+ export interface TasksStatus {
897
+ robot: TasksRunStatus<Readonly<Record<string, TaskStatus>>>;
898
+ project: TasksRunStatus<Readonly<Record<string, Readonly<Record<string, TaskStatus>>>>>;
899
+ }
900
+ /** One task the runtime knows (from the task tool's workspaces). Fields the task's manifest lacks are absent. */
901
+ export interface TaskCatalogEntry {
902
+ addr: string;
903
+ workspace: string;
904
+ pkg?: string;
905
+ name?: string;
906
+ kind?: string;
907
+ tags?: string[];
908
+ inputs?: JsonSchema;
909
+ schemaHash?: string;
910
+ definedBy?: string;
911
+ }
912
+ /** A value the runtime keeps current that this node only reads: it has no writes. */
913
+ export interface ReadonlyStoreHandle<V> {
914
+ /** The value now; the same object while nothing changed, and an unchanged part keeps its object when another part changes. */
915
+ get(): V;
916
+ /** `true` while the value is current. */
917
+ readonly synced: boolean;
918
+ /** Resolves once the value is current (the first time). */
919
+ ready(): Promise<void>;
920
+ /** The whole value after each change. It is state, not history: a reader slower than the changes sees the latest value. */
921
+ watch(): AsyncIterable<V>;
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;
924
+ }
925
+ /** The robot's task declarations, `instance` → declaration (operators write them). The node owns this handle: it has no `close` or `destroy`. */
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>>;
929
+ get(instance: string): TaskDeclaration | undefined;
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>;
937
+ set(instance: string, declaration: TaskDeclaration): Promise<void>;
938
+ set(declarations: Record<string, TaskDeclaration>): Promise<void>;
939
+ /** The whole value after each change; with `instance`, that declaration each time it changes (`undefined` after a delete). */
940
+ watch(instance?: string): AsyncIterable<unknown>;
941
+ /** Resolves once the value is current (the first time). */
942
+ ready(): Promise<void>;
943
+ }
944
+ /**
945
+ * `node.tasks`: the robot's task declarations, what runs (`status`), what the runtime can run
946
+ * (`catalog`), and the verbs. Running and stopping need the operator role. A run's reply is the
947
+ * runtime's answer; what runs afterwards is `status`.
948
+ */
949
+ export interface Tasks {
950
+ readonly declarations: TaskDeclarations;
951
+ readonly status: ReadonlyStoreHandle<TasksStatus>;
952
+ /** By address. */
953
+ readonly catalog: ReadonlyStoreHandle<Readonly<Record<string, TaskCatalogEntry>>>;
954
+ /**
955
+ * Starts the robot's tasks (the enabled declarations). Every declaration is checked first and nothing starts unless all
956
+ * pass: a failed check rejects `preflight_failed`, a process that could not start `launch_failed` (what had started is
957
+ * stopped); both carry the report ({@link isTasksRefusal}). `already_running` while they run. Robot tasks keep running
958
+ * across project switches.
959
+ */
960
+ runRobotTasks(): Promise<TasksRun>;
961
+ /** Stops every robot task (also when none ran). Project tasks are not touched. */
962
+ stopRobotTasks(): Promise<void>;
963
+ /** Starts the active project's tasks (the enabled `tasks` of its scene's entities), as {@link runRobotTasks}; `project_inactive` with no active project. A project switch stops them. */
964
+ runProjectTasks(): Promise<TasksRun>;
965
+ /** Stops every project task (also when none ran). Robot tasks are not touched. */
966
+ stopProjectTasks(): Promise<void>;
967
+ }
656
968
  export interface Catalog {
657
969
  readonly topics: Readonly<Record<string, {
658
970
  desc?: TrackDesc;