@synapse-sdk/client 0.1.0
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/README.md +113 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +15 -0
- package/dist/node-CSSIXG4k.js +1501 -0
- package/dist/node-DbD7G0Nx.d.ts +466 -0
- package/dist/react.d.ts +63 -0
- package/dist/react.js +141 -0
- package/package.json +57 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/** The minimal slice of the WHATWG WebSocket we depend on — so any conforming
|
|
3
|
+
* implementation (browser, Bun/Node global, a test double) can be injected. */
|
|
4
|
+
interface WebSocketLike {
|
|
5
|
+
readonly readyState: number;
|
|
6
|
+
send(data: string | ArrayBufferLike): void;
|
|
7
|
+
close(code?: number, reason?: string): void;
|
|
8
|
+
onopen: ((ev: unknown) => void) | null;
|
|
9
|
+
onmessage: ((ev: {
|
|
10
|
+
data: unknown;
|
|
11
|
+
}) => void) | null;
|
|
12
|
+
onclose: ((ev: {
|
|
13
|
+
code?: number;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}) => void) | null;
|
|
16
|
+
onerror: ((ev: unknown) => void) | null;
|
|
17
|
+
}
|
|
18
|
+
type WebSocketCtor = new (url: string, protocols?: string | string[]) => WebSocketLike;
|
|
19
|
+
/** Observable connection status. `connected` means open AND verified (handshake acked). */
|
|
20
|
+
type ConnectionStatus = "idle" | "connecting" | "connected" | "reconnecting" | "closing" | "error";
|
|
21
|
+
interface ConnectionState {
|
|
22
|
+
readonly status: ConnectionStatus;
|
|
23
|
+
readonly endpoint: string | null;
|
|
24
|
+
readonly error: Error | null;
|
|
25
|
+
readonly since: number;
|
|
26
|
+
}
|
|
27
|
+
type Params = Record<string, unknown>;
|
|
28
|
+
type ParamsInput = Params | (() => Params | Promise<Params>);
|
|
29
|
+
type TokenInput = () => string | Promise<string>;
|
|
30
|
+
/** Runs after the socket opens; must resolve for status to become `connected`. */
|
|
31
|
+
type VerifyFn = (handle: VerifyHandle) => Promise<void>;
|
|
32
|
+
interface VerifyHandle {
|
|
33
|
+
channel(topic: string, params?: Params): Channel;
|
|
34
|
+
}
|
|
35
|
+
/** What to connect to. Identity = `url` + plain `params`; `token`/`verify` are *how* to
|
|
36
|
+
* connect, never part of identity. */
|
|
37
|
+
interface Target {
|
|
38
|
+
url: string;
|
|
39
|
+
params?: ParamsInput;
|
|
40
|
+
token?: TokenInput;
|
|
41
|
+
verify?: VerifyFn;
|
|
42
|
+
}
|
|
43
|
+
interface Reply {
|
|
44
|
+
status: "ok" | "error" | "timeout";
|
|
45
|
+
response: unknown;
|
|
46
|
+
}
|
|
47
|
+
type Logger = (level: "debug" | "info" | "warn" | "error", msg: string, data?: unknown) => void;
|
|
48
|
+
interface ConnectionOptions {
|
|
49
|
+
/** Defaults to `globalThis.WebSocket` (browser / Bun / Node 22+ / Deno / edge). */
|
|
50
|
+
WebSocket?: WebSocketCtor;
|
|
51
|
+
serializer?: Serializer;
|
|
52
|
+
/** Heartbeat cadence; a missed reply within one interval declares the socket dead.
|
|
53
|
+
* HMIs want this low (e.g. 2000). Default 15000. */
|
|
54
|
+
heartbeatIntervalMs?: number;
|
|
55
|
+
requestTimeoutMs?: number;
|
|
56
|
+
/** Max wait for a healthy socket's `close` during teardown before force-fence. Default 2000. */
|
|
57
|
+
closeTimeoutMs?: number;
|
|
58
|
+
reconnectAfterMs?: (tries: number) => number;
|
|
59
|
+
vsn?: string;
|
|
60
|
+
logger?: Logger;
|
|
61
|
+
}
|
|
62
|
+
/** One decoded Phoenix v2 frame: `[join_ref, ref, topic, event, payload]`. */
|
|
63
|
+
interface Message {
|
|
64
|
+
join_ref: string | null;
|
|
65
|
+
ref: string | null;
|
|
66
|
+
topic: string;
|
|
67
|
+
event: string;
|
|
68
|
+
payload: unknown;
|
|
69
|
+
}
|
|
70
|
+
interface Serializer {
|
|
71
|
+
encode(msg: Message): string | ArrayBufferLike;
|
|
72
|
+
decode(raw: string | ArrayBufferLike): Message;
|
|
73
|
+
}
|
|
74
|
+
/** The surface a Channel needs from its Connection (so Channel depends on a contract,
|
|
75
|
+
* not the concrete class). */
|
|
76
|
+
interface ChannelHost {
|
|
77
|
+
_makeRef(): string;
|
|
78
|
+
_send(msg: Message): void;
|
|
79
|
+
_request(msg: Message, timeoutMs?: number): Promise<Reply>;
|
|
80
|
+
_isOpen(): boolean;
|
|
81
|
+
_removeChannel(topic: string): void;
|
|
82
|
+
_log: Logger;
|
|
83
|
+
}
|
|
84
|
+
interface SynapseOptions extends ConnectionOptions {
|
|
85
|
+
/** Stable node identity (defaults to a random uuid). */
|
|
86
|
+
userId?: string;
|
|
87
|
+
/** Human-friendly display name. */
|
|
88
|
+
name?: string;
|
|
89
|
+
/** Role the bus authorizes against: "agent" (default) or "viewer". */
|
|
90
|
+
role?: string;
|
|
91
|
+
/** Join `synapse:system` on connect and gate readiness on a server-assigned role.
|
|
92
|
+
* Default `true`. Set `false` for headless nodes that don't need a session/role —
|
|
93
|
+
* then `ready` simply means transport-connected. */
|
|
94
|
+
system?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/** What to connect to. Identity params (user_id/type/name) are merged in automatically. */
|
|
97
|
+
interface ConnectTarget {
|
|
98
|
+
url: string;
|
|
99
|
+
params?: Target["params"];
|
|
100
|
+
token?: Target["token"];
|
|
101
|
+
verify?: Target["verify"];
|
|
102
|
+
/** Project to activate on the system channel (request controller of it). */
|
|
103
|
+
project?: string;
|
|
104
|
+
/** Request the controller role (vs viewer). Needs a `project`; `type: "hmi"` also auto-controllers. */
|
|
105
|
+
controller?: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Server-assigned session role from the `synapse:system` handshake. **Open-ended** — the
|
|
109
|
+
* server may assign roles beyond these (the Python node already has `agent`), so the type is
|
|
110
|
+
* any string; the constants are just for ergonomic, typo-safe comparison
|
|
111
|
+
* (`role === SystemRole.Controller`). Add entries here as the server grows.
|
|
112
|
+
*/
|
|
113
|
+
declare const SystemRole: {
|
|
114
|
+
readonly Viewer: "viewer";
|
|
115
|
+
readonly Controller: "controller";
|
|
116
|
+
readonly Agent: "agent";
|
|
117
|
+
};
|
|
118
|
+
type SystemRole = (typeof SystemRole)[keyof typeof SystemRole] | (string & {});
|
|
119
|
+
/**
|
|
120
|
+
* The node's readiness, three layers deep:
|
|
121
|
+
* - `status` — transport: reaching the socket (`connecting`) → reached it (`connected`).
|
|
122
|
+
* - `role` — system: who the server says you are once negotiated (null until then).
|
|
123
|
+
* - `ready` — negotiated and usable: `connected` AND (role assigned, or system disabled).
|
|
124
|
+
*/
|
|
125
|
+
interface SystemState {
|
|
126
|
+
readonly status: ConnectionStatus;
|
|
127
|
+
readonly role: SystemRole | null;
|
|
128
|
+
readonly projectId: string | null;
|
|
129
|
+
readonly ready: boolean;
|
|
130
|
+
readonly error: Error | null;
|
|
131
|
+
}
|
|
132
|
+
/** A `state_diff` from a state mirror — absolute + version-ordered (keys changed/removed). */
|
|
133
|
+
interface StateDiff {
|
|
134
|
+
changed?: Record<string, unknown>;
|
|
135
|
+
removed?: string[];
|
|
136
|
+
version?: number;
|
|
137
|
+
}
|
|
138
|
+
/** A service's capability card (from the catalog). Open-ended; always has a `name`. */
|
|
139
|
+
interface ServiceCard {
|
|
140
|
+
name: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
[key: string]: unknown;
|
|
143
|
+
}
|
|
144
|
+
/** A state resource entry in the catalog. */
|
|
145
|
+
interface ResourceInfo {
|
|
146
|
+
name: string;
|
|
147
|
+
kind: string;
|
|
148
|
+
}
|
|
149
|
+
/** A live catalog change as providers / state resources are added or removed. */
|
|
150
|
+
interface CatalogChange {
|
|
151
|
+
change: "added" | "removed";
|
|
152
|
+
name: string;
|
|
153
|
+
kind: "service" | "resource";
|
|
154
|
+
}
|
|
155
|
+
type Frame = Record<string, unknown>;
|
|
156
|
+
interface CallOptions {
|
|
157
|
+
onUpdate?: (frame: Frame) => void;
|
|
158
|
+
/** Cancel the call the web-standard way; the server is told to stop the provider. */
|
|
159
|
+
signal?: AbortSignal;
|
|
160
|
+
/** Bounds the initial accept (ack). */
|
|
161
|
+
timeout?: number;
|
|
162
|
+
/** Idle deadline (ms): fail the call if no `update`/`result`/`error` arrives within this
|
|
163
|
+
* window after the ack — reset on each `update`. Catches a wedged provider that an overall
|
|
164
|
+
* `AbortSignal.timeout` can't tell apart from legitimately slow work. Off by default. */
|
|
165
|
+
idleTimeout?: number;
|
|
166
|
+
}
|
|
167
|
+
interface SubscribeOptions {
|
|
168
|
+
/** Channel join params — e.g. ROS topics need `{ type: "std_msgs/String" }`. */
|
|
169
|
+
opts?: Record<string, unknown>;
|
|
170
|
+
}
|
|
171
|
+
interface StreamEvent {
|
|
172
|
+
type: "update" | "result";
|
|
173
|
+
data: Frame;
|
|
174
|
+
}
|
|
175
|
+
interface Subscription {
|
|
176
|
+
unsubscribe(): void;
|
|
177
|
+
}
|
|
178
|
+
interface CallContext {
|
|
179
|
+
update(data: Frame): void;
|
|
180
|
+
/** Aborted when the caller cancels — poll it for cooperative cancellation. */
|
|
181
|
+
readonly signal: AbortSignal;
|
|
182
|
+
/** The id of the call being served — handy for logging/correlation. */
|
|
183
|
+
readonly taskId: string;
|
|
184
|
+
}
|
|
185
|
+
type ServiceHandler = (input: Frame, ctx: CallContext) => unknown | Promise<unknown>;
|
|
186
|
+
interface ServiceRegistration {
|
|
187
|
+
unregister(): void;
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/channel.d.ts
|
|
191
|
+
type ChannelState = "closed" | "joining" | "joined" | "leaving" | "errored";
|
|
192
|
+
type ChannelHandler = (payload: unknown, ref: string | null) => void;
|
|
193
|
+
/**
|
|
194
|
+
* One topic on the connection. Owned and reference-counted by `Connection` (one Channel
|
|
195
|
+
* per topic). Membership is independent of any UI component lifecycle: listeners attach
|
|
196
|
+
* and detach freely via `on`/`off` without re-joining, and the channel auto-rejoins when
|
|
197
|
+
* the socket reconnects.
|
|
198
|
+
*/
|
|
199
|
+
declare class Channel {
|
|
200
|
+
readonly topic: string;
|
|
201
|
+
state: ChannelState;
|
|
202
|
+
private host;
|
|
203
|
+
private params;
|
|
204
|
+
private joinRef;
|
|
205
|
+
private joinPromise;
|
|
206
|
+
private joinResponse;
|
|
207
|
+
private bindings;
|
|
208
|
+
constructor(host: ChannelHost, topic: string, params: Params);
|
|
209
|
+
/** Idempotent: a join in flight or already joined returns the existing result. */
|
|
210
|
+
join(timeoutMs?: number): Promise<unknown>;
|
|
211
|
+
private doJoin;
|
|
212
|
+
leave(timeoutMs?: number): Promise<void>;
|
|
213
|
+
/** Fire-and-forget; for handlers that reply with `{:noreply, ...}` (e.g. pub/sub publish). */
|
|
214
|
+
push(event: string, payload?: unknown): void;
|
|
215
|
+
/** Send and await the reply; for handlers that reply with `{:reply, ...}`. */
|
|
216
|
+
request(event: string, payload?: unknown, timeoutMs?: number): Promise<Reply>;
|
|
217
|
+
on(event: string, cb: ChannelHandler): ChannelHandler;
|
|
218
|
+
off(event: string, cb: ChannelHandler): void;
|
|
219
|
+
_trigger(event: string, payload: unknown, ref: string | null): void;
|
|
220
|
+
/** Called when a (new) socket is open. Re-establishes membership if needed. */
|
|
221
|
+
_rejoin(): void;
|
|
222
|
+
/** Called when the socket goes down: the join is no longer valid; we'll rejoin on reconnect. */
|
|
223
|
+
_onSocketDown(): void;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/connection.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* Single-socket connection supervisor.
|
|
229
|
+
*
|
|
230
|
+
* Invariant (the whole point): **at most one socket is ever acted upon.** A new socket is
|
|
231
|
+
* never created until the previous one's `close` is confirmed (healthy switch) or it has
|
|
232
|
+
* been fenced and abandoned (dead socket). Three mechanics enforce it:
|
|
233
|
+
*
|
|
234
|
+
* 1. **Serialized reconcile** — every transition runs through one queue; the latest
|
|
235
|
+
* desired target always wins (rapid `connect`/`disconnect` coalesce).
|
|
236
|
+
* 2. **Generation + live-identity guards** — every socket event checks it belongs to the
|
|
237
|
+
* current desired-epoch *and* the current socket; late events from a superseded or
|
|
238
|
+
* replaced socket are inert.
|
|
239
|
+
* 3. **Verified teardown** — switching/closing awaits the real `close` event (with a
|
|
240
|
+
* force-fence timeout) before any new socket opens.
|
|
241
|
+
*
|
|
242
|
+
* No global/`window` coupling, so discarding a socket is clean — nothing to leak or
|
|
243
|
+
* resurrect on tab visibility.
|
|
244
|
+
*/
|
|
245
|
+
declare class Connection implements ChannelHost {
|
|
246
|
+
private readonly WS;
|
|
247
|
+
private readonly serializer;
|
|
248
|
+
private readonly heartbeatIntervalMs;
|
|
249
|
+
private readonly requestTimeoutMs;
|
|
250
|
+
private readonly closeTimeoutMs;
|
|
251
|
+
private readonly reconnectAfterMs;
|
|
252
|
+
private readonly vsn;
|
|
253
|
+
readonly _log: Logger;
|
|
254
|
+
private desired;
|
|
255
|
+
private live;
|
|
256
|
+
private generation;
|
|
257
|
+
private state;
|
|
258
|
+
private listeners;
|
|
259
|
+
private channels;
|
|
260
|
+
private pending;
|
|
261
|
+
private refCounter;
|
|
262
|
+
private reconciling;
|
|
263
|
+
private dirty;
|
|
264
|
+
private reconnectAttempt;
|
|
265
|
+
private reconnectTimer;
|
|
266
|
+
private closed;
|
|
267
|
+
constructor(options?: ConnectionOptions);
|
|
268
|
+
getState(): ConnectionState;
|
|
269
|
+
/** Observe connection state. Returns an unsubscribe fn. `getState`/state are stable
|
|
270
|
+
* references between changes, so this is `useSyncExternalStore`-friendly. */
|
|
271
|
+
subscribe(listener: (s: ConnectionState) => void): () => void;
|
|
272
|
+
/** Declarative + idempotent. Sets the desired target and converges to it. Resolves when
|
|
273
|
+
* this target reaches `connected`; rejects if a different target supersedes it. */
|
|
274
|
+
connect(target: Target): Promise<void>;
|
|
275
|
+
/** Tear down to `idle`. Resolves once fully closed. */
|
|
276
|
+
disconnect(): Promise<void>;
|
|
277
|
+
/** Permanent dispose — closes the socket and stops everything. */
|
|
278
|
+
close(): void;
|
|
279
|
+
/** Get-or-create the (single) channel for a topic. Joined now if the socket is open,
|
|
280
|
+
* otherwise it joins automatically once a socket comes up. */
|
|
281
|
+
channel(topic: string, params?: Params): Channel;
|
|
282
|
+
_makeRef(): string;
|
|
283
|
+
_isOpen(): boolean;
|
|
284
|
+
_send(msg: Message): void;
|
|
285
|
+
_request(msg: Message, timeoutMs?: number): Promise<Reply>;
|
|
286
|
+
_removeChannel(topic: string): void;
|
|
287
|
+
private reconcile;
|
|
288
|
+
private runReconcile;
|
|
289
|
+
private step;
|
|
290
|
+
private bringUp;
|
|
291
|
+
private onOpen;
|
|
292
|
+
private verifyHandle;
|
|
293
|
+
private setConnected;
|
|
294
|
+
private teardown;
|
|
295
|
+
private replaceDead;
|
|
296
|
+
private onUnexpectedClose;
|
|
297
|
+
/** Detach handlers and best-effort close, without awaiting (the socket may never close). */
|
|
298
|
+
private fence;
|
|
299
|
+
private afterSocketGone;
|
|
300
|
+
private scheduleReconnect;
|
|
301
|
+
private cancelReconnect;
|
|
302
|
+
private startHeartbeat;
|
|
303
|
+
private sendHeartbeat;
|
|
304
|
+
private clearHeartbeat;
|
|
305
|
+
private onMessage;
|
|
306
|
+
private sendRaw;
|
|
307
|
+
private flushPending;
|
|
308
|
+
private setState;
|
|
309
|
+
private failTarget;
|
|
310
|
+
private desiredKey;
|
|
311
|
+
private waitForTarget;
|
|
312
|
+
private bumpGeneration;
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/node.d.ts
|
|
316
|
+
type MessageHandler = (m: Frame) => void;
|
|
317
|
+
/**
|
|
318
|
+
* A Synapse bus node. Wraps the single-socket {@link Connection} and adds the bus verbs —
|
|
319
|
+
* `call`, `callStream`, `subscribe`, `publish`, `service` — mirroring the Python `Node`.
|
|
320
|
+
* Any node can both consume and provide (a browser can publish joystick state *and* expose
|
|
321
|
+
* a service the robot calls into).
|
|
322
|
+
*/
|
|
323
|
+
declare class Synapse {
|
|
324
|
+
readonly connection: Connection;
|
|
325
|
+
private identity;
|
|
326
|
+
private tasks;
|
|
327
|
+
private consumerChannels;
|
|
328
|
+
private pubsubChannels;
|
|
329
|
+
private pubsubSubs;
|
|
330
|
+
private stateHandles;
|
|
331
|
+
private catalogChannelP;
|
|
332
|
+
private catalogWatchers;
|
|
333
|
+
private readonly systemEnabled;
|
|
334
|
+
private systemChannel;
|
|
335
|
+
private role;
|
|
336
|
+
private projectId;
|
|
337
|
+
private systemError;
|
|
338
|
+
private systemRequest;
|
|
339
|
+
private systemState;
|
|
340
|
+
private systemListeners;
|
|
341
|
+
private systemGen;
|
|
342
|
+
constructor(options?: SynapseOptions);
|
|
343
|
+
/**
|
|
344
|
+
* Connect (and negotiate the system session). Identity params are merged in. Resolves when
|
|
345
|
+
* **ready** — transport connected *and* a server role assigned (unless `system: false`,
|
|
346
|
+
* where ready just means connected). Declarative + idempotent. To request control, pass
|
|
347
|
+
* `{ project, controller: true }`.
|
|
348
|
+
*/
|
|
349
|
+
connect(target: ConnectTarget): Promise<void>;
|
|
350
|
+
disconnect(): Promise<void>;
|
|
351
|
+
close(): void;
|
|
352
|
+
getState(): ConnectionState;
|
|
353
|
+
/** Observe connection state — `useSyncExternalStore`-friendly. */
|
|
354
|
+
subscribeState(listener: (s: ConnectionState) => void): () => void;
|
|
355
|
+
/** The full readiness — `{ status, role, projectId, ready, error }`. */
|
|
356
|
+
getSystem(): SystemState;
|
|
357
|
+
/** Observe readiness — `useSyncExternalStore`-friendly. The connection indicator binds
|
|
358
|
+
* to `{ status, role }` (transport status + server role) and `ready` is the gate. */
|
|
359
|
+
subscribeSystem(listener: (s: SystemState) => void): () => void;
|
|
360
|
+
/** Resolves once **ready** (negotiated + role assigned), or rejects after `timeoutMs`. */
|
|
361
|
+
whenReady(timeoutMs?: number): Promise<void>;
|
|
362
|
+
/** Activate a project (request control of it). Role updates arrive live via the session. */
|
|
363
|
+
activateProject(projectId: string): Promise<void>;
|
|
364
|
+
/** Step back to viewer for the active project. */
|
|
365
|
+
deactivateProject(): Promise<void>;
|
|
366
|
+
/** Resolves once the transport is connected — used internally before channel ops (channels
|
|
367
|
+
* need the socket, not a role). */
|
|
368
|
+
private awaitConnected;
|
|
369
|
+
private onTransport;
|
|
370
|
+
private enterSystem;
|
|
371
|
+
private onSessionState;
|
|
372
|
+
private setRole;
|
|
373
|
+
private requireSystem;
|
|
374
|
+
private systemPayload;
|
|
375
|
+
private computeSystem;
|
|
376
|
+
private emitSystem;
|
|
377
|
+
call<T = unknown>(service: string, input?: Frame, opts?: CallOptions): Promise<T>;
|
|
378
|
+
/** Call as a loop: yields each `update` then the terminal `result`. Breaking cancels. */
|
|
379
|
+
callStream(service: string, input?: Frame, opts?: {
|
|
380
|
+
timeout?: number;
|
|
381
|
+
idleTimeout?: number;
|
|
382
|
+
}): AsyncGenerator<StreamEvent>;
|
|
383
|
+
publish(resource: string, data?: Frame, opts?: SubscribeOptions): Promise<void>;
|
|
384
|
+
subscribe(resource: string, callback: MessageHandler, opts?: SubscribeOptions): Subscription;
|
|
385
|
+
subscribe(resource: string, opts?: SubscribeOptions): AsyncIterable<Frame>;
|
|
386
|
+
/** A render-loop-friendly latest-value holder for high-rate topics (three.js / `useFrame`). */
|
|
387
|
+
latest<T extends Frame = Frame>(resource: string, opts?: SubscribeOptions): LatestValue<T>;
|
|
388
|
+
/**
|
|
389
|
+
* Open a live, always-current local mirror of a `synapse:state:<name>` resource — a
|
|
390
|
+
* shared key→value map that converges across nodes. `handle.value` is kept up to date
|
|
391
|
+
* from the initial sync and each diff; write with `handle.put`/`handle.delete`. Cached
|
|
392
|
+
* per resource (repeated calls share one mirror).
|
|
393
|
+
*/
|
|
394
|
+
openState(resource: string): Promise<StateHandle>;
|
|
395
|
+
private doOpenState;
|
|
396
|
+
/** Capability cards of every registered service. */
|
|
397
|
+
listServices(): Promise<ServiceCard[]>;
|
|
398
|
+
/** One service's capability card (throws `BusError` if unknown). */
|
|
399
|
+
describe(service: string): Promise<ServiceCard>;
|
|
400
|
+
/** State resources currently registered on the bus. */
|
|
401
|
+
listResources(): Promise<ResourceInfo[]>;
|
|
402
|
+
/** Watch providers / resources coming and going. With a callback → push (returns a
|
|
403
|
+
* Subscription); without → an async iterator of `{ change, name, kind }`. */
|
|
404
|
+
watchCatalog(callback: (c: CatalogChange) => void): Subscription;
|
|
405
|
+
watchCatalog(): AsyncIterable<CatalogChange>;
|
|
406
|
+
private watchCatalogIterator;
|
|
407
|
+
private catalog;
|
|
408
|
+
private openCatalog;
|
|
409
|
+
service(name: string, handler: ServiceHandler, card?: Frame): ServiceRegistration;
|
|
410
|
+
private runProvider;
|
|
411
|
+
private withIdentity;
|
|
412
|
+
private consumerChannel;
|
|
413
|
+
private routeTask;
|
|
414
|
+
private subsFor;
|
|
415
|
+
private ensurePubsub;
|
|
416
|
+
private subscribePush;
|
|
417
|
+
private subscribePull;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Latest-value holder for high-frequency topics. Read `.get()` inside a three.js / r3f
|
|
421
|
+
* `useFrame` loop (non-reactive) and mutate the scene directly — React never re-renders.
|
|
422
|
+
*/
|
|
423
|
+
declare class LatestValue<T> {
|
|
424
|
+
private value;
|
|
425
|
+
private listeners;
|
|
426
|
+
private sub;
|
|
427
|
+
constructor(subscribe: (cb: (m: T) => void) => Subscription);
|
|
428
|
+
get(): T | undefined;
|
|
429
|
+
onChange(cb: (v: T) => void): () => void;
|
|
430
|
+
close(): void;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* A live local mirror of a `synapse:state:<name>` resource. `value` is kept current from the
|
|
434
|
+
* initial `state_sync` and each `state_diff` (absolute, version-ordered); a version gap
|
|
435
|
+
* triggers an automatic `resync`. Writes (`put`/`delete`) are server-confirmed and come back
|
|
436
|
+
* as a diff (self-included). `value` is replaced (new reference) on each change, so it binds
|
|
437
|
+
* cleanly to `useSyncExternalStore` / reads cleanly in a `useFrame` loop.
|
|
438
|
+
*/
|
|
439
|
+
declare class StateHandle {
|
|
440
|
+
readonly resource: string;
|
|
441
|
+
private ch;
|
|
442
|
+
private _value;
|
|
443
|
+
private _version;
|
|
444
|
+
private listeners;
|
|
445
|
+
private resyncing;
|
|
446
|
+
constructor(resource: string, ch: Channel);
|
|
447
|
+
/** The current converged map (new reference on every change). */
|
|
448
|
+
get value(): Record<string, unknown>;
|
|
449
|
+
/** The version of the last applied op. */
|
|
450
|
+
get version(): number;
|
|
451
|
+
put(key: string, value: unknown): Promise<void>;
|
|
452
|
+
delete(key: string): Promise<void>;
|
|
453
|
+
/** Observe each applied change: `(value, diff) => …`. Returns unsubscribe. */
|
|
454
|
+
subscribe(listener: (value: Record<string, unknown>, diff: StateDiff) => void): () => void;
|
|
455
|
+
/** Force a full re-sync from the server. */
|
|
456
|
+
resync(): Promise<void>;
|
|
457
|
+
close(): void;
|
|
458
|
+
_applySync(sync: {
|
|
459
|
+
version?: number;
|
|
460
|
+
state?: Record<string, unknown>;
|
|
461
|
+
}): void;
|
|
462
|
+
_onDiff(diff: StateDiff): void;
|
|
463
|
+
private emit;
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
export { SynapseOptions as A, ServiceCard as C, StreamEvent as D, StateDiff as E, VerifyFn as F, VerifyHandle as I, WebSocketCtor as L, SystemState as M, Target as N, SubscribeOptions as O, TokenInput as P, WebSocketLike as R, Serializer as S, ServiceRegistration as T, Message as _, Channel as a, Reply as b, CallOptions as c, ConnectTarget as d, ConnectionOptions as f, Logger as g, Frame as h, Connection as i, SystemRole as j, Subscription as k, CatalogChange as l, ConnectionStatus as m, StateHandle as n, ChannelHandler as o, ConnectionState as p, Synapse as r, CallContext as s, LatestValue as t, ChannelHost as u, Params as v, ServiceHandler as w, ResourceInfo as x, ParamsInput as y };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { C as ServiceCard, M as SystemState, O as SubscribeOptions, c as CallOptions, h as Frame, j as SystemRole, m as ConnectionStatus, n as StateHandle, p as ConnectionState, r as Synapse, w as ServiceHandler, x as ResourceInfo } from "./node-DbD7G0Nx.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react.d.ts
|
|
5
|
+
/** Pass as the resource to `useSubscribe` to disable it conditionally (à la Convex). */
|
|
6
|
+
declare const skip: "skip";
|
|
7
|
+
declare function SynapseProvider({
|
|
8
|
+
client,
|
|
9
|
+
children
|
|
10
|
+
}: {
|
|
11
|
+
client: Synapse;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}): ReactNode;
|
|
14
|
+
/** The `Synapse` client from the nearest provider. */
|
|
15
|
+
declare function useSynapse(): Synapse;
|
|
16
|
+
/** The full connection state; re-renders on any status/endpoint/error change. */
|
|
17
|
+
declare function useConnectionState(): ConnectionState;
|
|
18
|
+
/** Just the status — `idle | connecting | connected | reconnecting | closing | error`. */
|
|
19
|
+
declare function useConnectionStatus(): ConnectionStatus;
|
|
20
|
+
/** Full readiness — `{ status, role, projectId, ready, error }`. Bind your connection
|
|
21
|
+
* indicator to `{ status, role }`. */
|
|
22
|
+
declare function useSystem(): SystemState;
|
|
23
|
+
/** The gate: transport-connected AND a server role assigned (or `system: false`). */
|
|
24
|
+
declare function useReady(): boolean;
|
|
25
|
+
/** The server-assigned session role (`viewer` / `controller` / …), or null until negotiated. */
|
|
26
|
+
declare function useRole(): SystemRole | null;
|
|
27
|
+
/**
|
|
28
|
+
* Live value of a pub/sub resource — the latest message (latest-wins, no queue growth), or
|
|
29
|
+
* `undefined` until the first arrives. Pass `skip` to disable. For 60 Hz data feeding
|
|
30
|
+
* three.js, prefer the imperative `synapse.latest(resource).get()` inside `useFrame` instead
|
|
31
|
+
* (this hook re-renders on every message).
|
|
32
|
+
*/
|
|
33
|
+
declare function useSubscribe<T extends Frame = Frame>(resource: string, opts?: SubscribeOptions): T | undefined;
|
|
34
|
+
/** A stable bound `call` — like TanStack/Convex `useMutation` returning a callable. */
|
|
35
|
+
declare function useCall(): <R = unknown>(service: string, input?: Frame, opts?: CallOptions) => Promise<R>;
|
|
36
|
+
/**
|
|
37
|
+
* Register this node as a provider for the lifetime of the component (e.g. a browser
|
|
38
|
+
* exposing a joystick service the robot calls into). The handler is read from a ref, so it
|
|
39
|
+
* can close over fresh props without re-registering.
|
|
40
|
+
*/
|
|
41
|
+
declare function useService(name: string, handler: ServiceHandler, card?: Frame): void;
|
|
42
|
+
/**
|
|
43
|
+
* Live mirror of a `synapse:state:<name>` resource. Returns `[value, handle]`: `value` is the
|
|
44
|
+
* converged map (re-renders on change), `handle` exposes `put`/`delete` (null until opened).
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const [scene, handle] = useStateMirror("world/collision")
|
|
48
|
+
* // render scene.box_1 …; handle?.put("box_2", { xyz: [1, 0, 0] })
|
|
49
|
+
*/
|
|
50
|
+
declare function useStateMirror(resource: string): readonly [Record<string, unknown>, StateHandle | null];
|
|
51
|
+
/**
|
|
52
|
+
* Live discovery — the bus's services and state resources, refreshed when any provider or
|
|
53
|
+
* resource is added/removed.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const { services, resources } = useCatalog()
|
|
57
|
+
*/
|
|
58
|
+
declare function useCatalog(): {
|
|
59
|
+
services: ServiceCard[];
|
|
60
|
+
resources: ResourceInfo[];
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { SynapseProvider, skip, useCall, useCatalog, useConnectionState, useConnectionStatus, useReady, useRole, useService, useStateMirror, useSubscribe, useSynapse, useSystem };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/react.tsx
|
|
4
|
+
/** Pass as the resource to `useSubscribe` to disable it conditionally (à la Convex). */
|
|
5
|
+
const skip = "skip";
|
|
6
|
+
const SynapseContext = createContext(null);
|
|
7
|
+
function SynapseProvider({ client, children }) {
|
|
8
|
+
return /* @__PURE__ */ jsx(SynapseContext.Provider, {
|
|
9
|
+
value: client,
|
|
10
|
+
children
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** The `Synapse` client from the nearest provider. */
|
|
14
|
+
function useSynapse() {
|
|
15
|
+
const client = useContext(SynapseContext);
|
|
16
|
+
if (!client) throw new Error("useSynapse must be used within <SynapseProvider>");
|
|
17
|
+
return client;
|
|
18
|
+
}
|
|
19
|
+
/** The full connection state; re-renders on any status/endpoint/error change. */
|
|
20
|
+
function useConnectionState() {
|
|
21
|
+
const client = useSynapse();
|
|
22
|
+
const subscribe = useCallback((cb) => client.subscribeState(() => cb()), [client]);
|
|
23
|
+
const getSnapshot = useCallback(() => client.getState(), [client]);
|
|
24
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
25
|
+
}
|
|
26
|
+
/** Just the status — `idle | connecting | connected | reconnecting | closing | error`. */
|
|
27
|
+
function useConnectionStatus() {
|
|
28
|
+
return useConnectionState().status;
|
|
29
|
+
}
|
|
30
|
+
/** Full readiness — `{ status, role, projectId, ready, error }`. Bind your connection
|
|
31
|
+
* indicator to `{ status, role }`. */
|
|
32
|
+
function useSystem() {
|
|
33
|
+
const client = useSynapse();
|
|
34
|
+
const subscribe = useCallback((cb) => client.subscribeSystem(() => cb()), [client]);
|
|
35
|
+
const getSnapshot = useCallback(() => client.getSystem(), [client]);
|
|
36
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
37
|
+
}
|
|
38
|
+
/** The gate: transport-connected AND a server role assigned (or `system: false`). */
|
|
39
|
+
function useReady() {
|
|
40
|
+
return useSystem().ready;
|
|
41
|
+
}
|
|
42
|
+
/** The server-assigned session role (`viewer` / `controller` / …), or null until negotiated. */
|
|
43
|
+
function useRole() {
|
|
44
|
+
return useSystem().role;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Live value of a pub/sub resource — the latest message (latest-wins, no queue growth), or
|
|
48
|
+
* `undefined` until the first arrives. Pass `skip` to disable. For 60 Hz data feeding
|
|
49
|
+
* three.js, prefer the imperative `synapse.latest(resource).get()` inside `useFrame` instead
|
|
50
|
+
* (this hook re-renders on every message).
|
|
51
|
+
*/
|
|
52
|
+
function useSubscribe(resource, opts) {
|
|
53
|
+
const client = useSynapse();
|
|
54
|
+
const disabled = resource === skip;
|
|
55
|
+
const holder = useMemo(() => disabled ? null : client.latest(resource, opts), [
|
|
56
|
+
client,
|
|
57
|
+
resource,
|
|
58
|
+
disabled,
|
|
59
|
+
opts ? JSON.stringify(opts) : ""
|
|
60
|
+
]);
|
|
61
|
+
useEffect(() => () => holder?.close(), [holder]);
|
|
62
|
+
return useSyncExternalStore(useCallback((cb) => holder ? holder.onChange(() => cb()) : () => {}, [holder]), useCallback(() => holder?.get(), [holder]), () => void 0);
|
|
63
|
+
}
|
|
64
|
+
/** A stable bound `call` — like TanStack/Convex `useMutation` returning a callable. */
|
|
65
|
+
function useCall() {
|
|
66
|
+
const client = useSynapse();
|
|
67
|
+
return useMemo(() => client.call.bind(client), [client]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Register this node as a provider for the lifetime of the component (e.g. a browser
|
|
71
|
+
* exposing a joystick service the robot calls into). The handler is read from a ref, so it
|
|
72
|
+
* can close over fresh props without re-registering.
|
|
73
|
+
*/
|
|
74
|
+
function useService(name, handler, card) {
|
|
75
|
+
const client = useSynapse();
|
|
76
|
+
const handlerRef = useRef(handler);
|
|
77
|
+
handlerRef.current = handler;
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const reg = client.service(name, (input, ctx) => handlerRef.current(input, ctx), card);
|
|
80
|
+
return () => reg.unregister();
|
|
81
|
+
}, [client, name]);
|
|
82
|
+
}
|
|
83
|
+
const EMPTY_STATE = Object.freeze({});
|
|
84
|
+
/**
|
|
85
|
+
* Live mirror of a `synapse:state:<name>` resource. Returns `[value, handle]`: `value` is the
|
|
86
|
+
* converged map (re-renders on change), `handle` exposes `put`/`delete` (null until opened).
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* const [scene, handle] = useStateMirror("world/collision")
|
|
90
|
+
* // render scene.box_1 …; handle?.put("box_2", { xyz: [1, 0, 0] })
|
|
91
|
+
*/
|
|
92
|
+
function useStateMirror(resource) {
|
|
93
|
+
const client = useSynapse();
|
|
94
|
+
const [handle, setHandle] = useState(null);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let active = true;
|
|
97
|
+
setHandle(null);
|
|
98
|
+
client.openState(resource).then((h) => {
|
|
99
|
+
if (active) setHandle(h);
|
|
100
|
+
}).catch(() => {});
|
|
101
|
+
return () => {
|
|
102
|
+
active = false;
|
|
103
|
+
};
|
|
104
|
+
}, [client, resource]);
|
|
105
|
+
return [useSyncExternalStore(useCallback((cb) => handle ? handle.subscribe(() => cb()) : () => {}, [handle]), useCallback(() => handle?.value ?? EMPTY_STATE, [handle]), () => EMPTY_STATE), handle];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Live discovery — the bus's services and state resources, refreshed when any provider or
|
|
109
|
+
* resource is added/removed.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* const { services, resources } = useCatalog()
|
|
113
|
+
*/
|
|
114
|
+
function useCatalog() {
|
|
115
|
+
const client = useSynapse();
|
|
116
|
+
const [catalog, setCatalog] = useState({
|
|
117
|
+
services: [],
|
|
118
|
+
resources: []
|
|
119
|
+
});
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
let active = true;
|
|
122
|
+
const refresh = async () => {
|
|
123
|
+
try {
|
|
124
|
+
const [services, resources] = await Promise.all([client.listServices(), client.listResources()]);
|
|
125
|
+
if (active) setCatalog({
|
|
126
|
+
services,
|
|
127
|
+
resources
|
|
128
|
+
});
|
|
129
|
+
} catch {}
|
|
130
|
+
};
|
|
131
|
+
refresh();
|
|
132
|
+
const sub = client.watchCatalog(() => void refresh());
|
|
133
|
+
return () => {
|
|
134
|
+
active = false;
|
|
135
|
+
sub.unsubscribe();
|
|
136
|
+
};
|
|
137
|
+
}, [client]);
|
|
138
|
+
return catalog;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { SynapseProvider, skip, useCall, useCatalog, useConnectionState, useConnectionStatus, useReady, useRole, useService, useStateMirror, useSubscribe, useSynapse, useSystem };
|