@synapse-sdk/client 0.1.0 → 0.3.3

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.
@@ -0,0 +1,299 @@
1
+ import * as Moq from "@moq/net";
2
+ import { SynapseError, type ErrorBody, type ProjectSnapshot } from "@synapse-sdk/core";
3
+ import type { Node } from "./node.ts";
4
+ import { StreamWriter, type Session } from "./session.ts";
5
+ import type { Warn } from "./topics.ts";
6
+ import type { LiveBatch, LiveJoints, LivePose, LiveRemoved, LiveState, Quat, SceneDiffOp, SceneEntity, SceneEntityLive, SceneHandle, SceneOp, ScenePlace, SceneSpawnOptions, SceneState, SceneStatus, SceneTree, SceneTxn, SceneView, Vec3 } from "./types.ts";
7
+ import { AsyncQueue, type Deferred } from "./util.ts";
8
+ /** How many batches a feed may fall behind before it is ended (the store's `STORE_FEED_CAP`). */
9
+ export declare const SCENE_FEED_CAP = 256;
10
+ /** Keys a `patch` may not set (the runtime refuses them); a txn never folds such a patch into a spawn. */
11
+ export declare const PATCH_PROTECTED: readonly string[];
12
+ type Entities = Map<string, SceneEntity>;
13
+ /**
14
+ * A fresh entity id: a ULID (26 Crockford base-32 characters) made from a UUIDv7 (48-bit ms, version,
15
+ * 74 random bits, variant), so ids sort by the time they were minted; within one millisecond the
16
+ * random part counts up, so ids minted here keep their order.
17
+ */
18
+ export declare function ulid(): string;
19
+ /** An `order` strictly before `order`. */
20
+ export declare function orderBefore(order: string): string;
21
+ /** An `order` strictly after `order`. */
22
+ export declare function orderAfter(order: string): string;
23
+ /** An `order` strictly between `left` and `right` (either may be open: `null`); `invalid_input` when `left >= right`. */
24
+ export declare function orderBetween(left: string | null | undefined, right: string | null | undefined): string;
25
+ /**
26
+ * The hierarchy of `entities`, as its roots: each node's children by `(order, id)` (an entity without
27
+ * an `order` after the ordered ones); an entity whose parent is not in the scene is a root (the
28
+ * prototype's rule). A cycle cannot come from the runtime; were one there, it is cut where it closes.
29
+ */
30
+ export declare function treeOf(entities: Readonly<Record<string, SceneEntity>>): SceneTree;
31
+ /**
32
+ * One authoring batch being built (`txn`): the ops it sends as one atomic `apply`. Ops on an entity
33
+ * spawned in the same batch fold into its spawn: a patch (of keys a patch may set) or a rename merges
34
+ * into it, a despawn cancels it (with every entity spawned under it in the batch). Nothing else is
35
+ * checked here: the runtime decides.
36
+ */
37
+ export declare class Txn implements SceneTxn {
38
+ #private;
39
+ get ops(): readonly SceneOp[];
40
+ /** Spawns an entity of `kind` under `opts.parent` (default the world root) with the components given (`layer` `authored` when not given); its id is minted here (or `opts.id`, a known one) and returned. No `order` is sent: the runtime appends it after its siblings. */
41
+ spawn(kind: string, opts: SceneSpawnOptions): string;
42
+ /** Sets components of `id` (one level deep: an object merges into an object). */
43
+ patch(id: string, set: Partial<SceneEntity>): void;
44
+ rename(id: string, name: string): void;
45
+ /** Moves `id` under `parent` (`null`: the world root), before or after a sibling (default: after the last). */
46
+ reparent(id: string, parent: string | null, place?: ScenePlace): void;
47
+ /** Despawns `id` (`cascade`: with its subtree). One spawned in this batch is cancelled instead, unless it has children spawned in the batch and `cascade` is off (then the runtime decides). */
48
+ despawn(id: string, opts?: {
49
+ cascade?: boolean;
50
+ }): void;
51
+ /** Locks or unlocks `id` (a flags-only patch, which a lock lets through). */
52
+ setLocked(id: string, locked: boolean): void;
53
+ }
54
+ /**
55
+ * Applies one diff op of a batch the runtime stamped `t` by `by` to the authored entities (the
56
+ * runtime's result, as the op describes it); returns the ids whose entity changed. A changed entity's
57
+ * `meta` is `{rev, t, by}`, as the runtime stamped it.
58
+ */
59
+ export declare function foldAuthored(authored: Entities, op: SceneDiffOp, t: number, by: string | null): string[];
60
+ /** What changed from `old` to `next`, in the diff ops' shapes (a snapshot replacing the mirror, the mirror emptying): a spawn per new entity, a despawn per gone one, and per changed one a reparent (parent or order), a rename (name) and a patch of the other keys that differ. */
61
+ export declare function netDifference(old: Entities, next: Entities): SceneDiffOp[];
62
+ /** One live row as this node keeps it: the whole live entity of an upsert (absent: an override of an authored entity), the lanes, and the newest lane stamp (the runtime's clock). */
63
+ export interface LiveRow {
64
+ entity?: SceneEntity;
65
+ pose?: {
66
+ t: Vec3;
67
+ q: Quat;
68
+ };
69
+ joints?: Record<string, number>;
70
+ state?: Record<string, unknown>;
71
+ stamp?: number;
72
+ }
73
+ export type LiveTable = Map<string, LiveRow>;
74
+ /** A frame of the merged `live` track: a keyframe (the whole live set) or a tick; the runtime stamps each lane row. */
75
+ type Stamped<T> = T & {
76
+ stamp?: number;
77
+ };
78
+ export interface LiveFrame {
79
+ upsert?: SceneEntity[];
80
+ poses?: Stamped<LivePose>[];
81
+ joints?: Stamped<LiveJoints>[];
82
+ state?: Stamped<LiveState>[];
83
+ remove?: LiveRemoved[];
84
+ keyframe?: boolean;
85
+ }
86
+ /** A keyframe as a fresh live table. */
87
+ export declare function liveTableOf(k: LiveFrame): LiveTable;
88
+ /** One tick folded into `table` with the runtime's rules (an upsert replaces the whole row, a lane replaces its lane, a remove drops the row); returns the ids it touched. */
89
+ export declare function foldLive(table: LiveTable, d: LiveFrame): string[];
90
+ /** The merged read of one entity: the pose lane replaces `xform.t`/`xform.q` (the rest of the xform stays), the joints lane replaces `joints`, the state lane replaces `state`; nothing merges deeper. */
91
+ export declare function overlay(entity: SceneEntity, row: LiveRow): SceneEntity;
92
+ /** A live row as the live plane shows it (`SceneEntityLive`): each key present only when the row has it. */
93
+ export declare function liveValue(row: LiveRow): SceneEntityLive;
94
+ /** `id` as the entities show it: its live row over its authored entity, or a live entity's own row. */
95
+ export declare function mergedOf(authored: Entities, live: LiveTable, id: string): SceneEntity | undefined;
96
+ type FeedKind = "changes" | "removals" | "denied" | "states";
97
+ interface FeedItems {
98
+ changes: SceneDiffOp[];
99
+ removals: LiveRemoved[];
100
+ denied: string[];
101
+ states: SceneState;
102
+ }
103
+ /** One feed of one handle: where its items go, and how it ends. */
104
+ interface Sink<T> {
105
+ handle: number;
106
+ deliver(item: T): void;
107
+ end(): void;
108
+ }
109
+ /** A snapshot frame of the `authored` track. */
110
+ interface Snapshot {
111
+ version: number;
112
+ hash: string;
113
+ entities: Record<string, SceneEntity>;
114
+ }
115
+ /** A diff frame of the `authored` track: one applied batch, with the `t` and `by` it was stamped with. */
116
+ interface Diff {
117
+ version: number;
118
+ hash: string;
119
+ ops: SceneDiffOp[];
120
+ t: number;
121
+ by?: string | null;
122
+ }
123
+ /** What the task's reader writes and every handle reads: the scene as this node has it, and the handles' feeds. */
124
+ export declare class Mirror {
125
+ #private;
126
+ state: SceneState;
127
+ /** Why the handle closed (`client`, `connection_lost`). */
128
+ ended: string | undefined;
129
+ epoch: string | undefined;
130
+ version: number;
131
+ hash: string;
132
+ /** An authored diff skipped a version; cleared by the next snapshot. */
133
+ gap: boolean;
134
+ pending: number;
135
+ authored: Entities;
136
+ live: LiveTable;
137
+ /** authored ⊕ live: the same object while nothing changed; an unchanged entity keeps its object. */
138
+ merged: Readonly<Record<string, SceneEntity>>;
139
+ subscribe(l: () => void): () => void;
140
+ addSink<K extends FeedKind>(kind: K, s: Sink<FeedItems[K]>): void;
141
+ removeSink<K extends FeedKind>(kind: K, s: Sink<FeedItems[K]>): void;
142
+ /** Every feed of handle `id` ends. */
143
+ dropHandle(id: number): void;
144
+ /** The hierarchy now (recomputed only after a change of the ids, a parent or an order; the same object while equal). */
145
+ tree(): SceneTree;
146
+ /** A snapshot of incarnation `epoch` replaces the authored copy; the change feeds get the net difference. Not yet `attached`: the reader catches up first (`attach`). */
147
+ applySnapshot(epoch: string, s: Snapshot): void;
148
+ /** The reader has applied everything this node held when it bound the incarnation. */
149
+ attach(): void;
150
+ /** One applied batch: its ops folded into the authored copy; the feeds get them as they came. */
151
+ applyDiff(d: Diff, warn: Warn): void;
152
+ /** A keyframe replaces the live table. */
153
+ applyKeyframe(k: LiveFrame): void;
154
+ /** One tick folded into the live table; its removals go to the feeds. */
155
+ applyLiveTick(d: LiveFrame): void;
156
+ /** Ids of this node's live rows the runtime dropped. */
157
+ denied(ids: string[]): void;
158
+ setPending(n: number): void;
159
+ /** No incarnation: the copy empties (the change feeds see the despawns), `detached`. */
160
+ detach(): void;
161
+ /** The end: emptied, `closed` (`reason` recorded), every feed ended. */
162
+ close(reason: string): void;
163
+ }
164
+ type Result<T> = {
165
+ ok: T;
166
+ } | {
167
+ err: SynapseError;
168
+ };
169
+ type Event = {
170
+ kind: "session";
171
+ session: Session;
172
+ } | {
173
+ kind: "sessionEnded";
174
+ } | {
175
+ kind: "nodeEnded";
176
+ }
177
+ /** The reader applied an authored frame of incarnation `epoch`: the mirror is at `version`. */
178
+ | {
179
+ kind: "applied";
180
+ epoch: string;
181
+ version: number;
182
+ }
183
+ /** The reader of attempt `attempt` ended: the incarnation it bound and its final frame's reason (none: the tracks or the session went). */
184
+ | {
185
+ kind: "followEnded";
186
+ attempt: number;
187
+ epoch: string | undefined;
188
+ reason: string | undefined;
189
+ } | {
190
+ kind: "join";
191
+ reply: Deferred<void>;
192
+ } | {
193
+ kind: "apply";
194
+ ops: SceneOp[];
195
+ base: number | undefined;
196
+ reply: Deferred<void>;
197
+ }
198
+ /** The runtime's answer to a batch sent on incarnation `epoch`. */
199
+ | {
200
+ kind: "applyDone";
201
+ epoch: string;
202
+ result: Result<unknown>;
203
+ reply: Deferred<void>;
204
+ } | {
205
+ kind: "project";
206
+ snapshot: ProjectSnapshot | undefined;
207
+ } | {
208
+ kind: "notice";
209
+ body: ErrorBody;
210
+ } | {
211
+ kind: "paced";
212
+ attempt: number;
213
+ } | {
214
+ kind: "close";
215
+ };
216
+ /** This node's live writer: the session's `<prefix>/scene/live` broadcast and its track `live`. */
217
+ interface LiveWriter {
218
+ session: Session;
219
+ bc: Moq.Broadcast.Producer;
220
+ stream: StreamWriter;
221
+ seq: number;
222
+ }
223
+ /** The node's scene: what every handle shares with the task. */
224
+ export declare class Scene {
225
+ readonly node: Node;
226
+ readonly warn: Warn;
227
+ readonly mirror: Mirror;
228
+ readonly mailbox: AsyncQueue<Event>;
229
+ readonly exited: Deferred<void>;
230
+ /** Handles neither closed nor dropped; the one that brings it to 0 tells the task `close`. */
231
+ handles: number;
232
+ nextHandle: number;
233
+ /** The live writer of the current session, made on the first `live()`. */
234
+ writer: LiveWriter | undefined;
235
+ detachedDrops: number;
236
+ constructor(node: Node, warn: Warn);
237
+ post(e: Event): boolean;
238
+ request<T>(make: (reply: Deferred<T>) => Event): Promise<T>;
239
+ }
240
+ /** The node's one scene task, if any (set with the task, cleared by the task when it exits). */
241
+ export interface SceneRegistry {
242
+ current: Scene | undefined;
243
+ }
244
+ export declare function startScene(scene: Scene, registry: SceneRegistry): void;
245
+ /** The application's handle on the node's scene: the last one closed ends the task. */
246
+ export declare class SceneHandleImpl implements SceneHandle {
247
+ #private;
248
+ private readonly scene;
249
+ constructor(scene: Scene);
250
+ get entities(): Readonly<Record<string, SceneEntity>>;
251
+ entity(id: string, opts?: {
252
+ plane?: "authored";
253
+ }): SceneView<SceneEntity | undefined>;
254
+ entity(id: string, opts: {
255
+ plane: "live";
256
+ }): SceneView<SceneEntityLive | undefined>;
257
+ entity(id: string, opts?: {
258
+ plane?: "authored" | "live";
259
+ }): SceneView<SceneEntity | SceneEntityLive | undefined>;
260
+ tree(): SceneView<SceneTree>;
261
+ query(where: (e: SceneEntity) => boolean): SceneView<SceneEntity[]>;
262
+ get status(): SceneStatus;
263
+ watchChanges(cb: (batch: SceneDiffOp[]) => void): () => void;
264
+ watchChanges(): AsyncIterableIterator<SceneDiffOp[]>;
265
+ watchLiveRemovals(cb: (batch: LiveRemoved[]) => void): () => void;
266
+ watchLiveRemovals(): AsyncIterableIterator<LiveRemoved[]>;
267
+ watchLiveDenied(cb: (ids: string[]) => void): () => void;
268
+ watchLiveDenied(): AsyncIterableIterator<string[]>;
269
+ watchState(cb: (state: SceneState) => void): () => void;
270
+ watchState(): AsyncIterableIterator<SceneState>;
271
+ apply(ops: SceneOp[], base?: number): Promise<void>;
272
+ txn(build: (t: SceneTxn) => void): Promise<void>;
273
+ spawn(kind: string, opts: SceneSpawnOptions): Promise<string>;
274
+ patch(id: string, set: Partial<SceneEntity>): Promise<void>;
275
+ rename(id: string, name: string): Promise<void>;
276
+ reparent(id: string, parent: string | null, place?: ScenePlace): Promise<void>;
277
+ despawn(id: string, opts?: {
278
+ cascade?: boolean;
279
+ }): Promise<void>;
280
+ setLocked(id: string, locked: boolean): Promise<void>;
281
+ /**
282
+ * Writes one live batch on this node's live track (made on the first call of each session); returns
283
+ * at once, like `publish`. The runtime stamps the rows and owns the outcome: an id another writer owns
284
+ * comes back on `watchLiveDenied`, a malformed batch as a node notice. Not attached: dropped and counted.
285
+ */
286
+ live(batch: LiveBatch): void;
287
+ /** The framework bridge (React's hooks): called on every change of the entities or the status. */
288
+ subscribe(listener: () => void): () => void;
289
+ /** Closes the handle: its verbs fail, its views and feeds end, its status says `closed`; the node's other scene handles go on. The last one ends the task, which ends this node's live writer (the runtime retracts its lifetimed rows and live entities: `writer_gone`). Idempotent. */
290
+ close(): void;
291
+ [Symbol.asyncDispose](): Promise<void>;
292
+ }
293
+ /**
294
+ * The active project's scene (one per project; one task per node, shared by every handle). Resolves
295
+ * once the handle is attached, or at once `detached` when the runtime says no project is active; the
296
+ * handle then follows the incarnations as they come and go. `not_connected` without a session.
297
+ */
298
+ export declare function openScene(node: Node, registry: SceneRegistry, warn: Warn): Promise<SceneHandleImpl>;
299
+ export {};