@statelyai/sdk 0.12.2 → 0.13.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.
@@ -1,4 +1,4 @@
1
- import { PREFIX } from "./protocol.mjs";
1
+ import { INSPECTION_PROTOCOL_VERSION, PREFIX } from "./protocol.mjs";
2
2
 
3
3
  //#region src/clientUtils.ts
4
4
  const jsonResultFormats = new Set([
@@ -113,6 +113,7 @@ function toInitMessage(options) {
113
113
  mode: options.mode,
114
114
  theme: options.theme,
115
115
  readOnly: options.readOnly,
116
+ readOnlyReason: options.readOnlyReason,
116
117
  depth: options.depth,
117
118
  capabilities: options.capabilities,
118
119
  leftPanels: options.panels?.leftPanels,
@@ -198,6 +199,7 @@ function createWebSocketTransport(options) {
198
199
  reconnectAttempts = 0;
199
200
  ws.send(JSON.stringify({
200
201
  type: "@statelyai.register",
202
+ protocolVersion: INSPECTION_PROTOCOL_VERSION,
201
203
  role,
202
204
  sessionId,
203
205
  ...metadata && { metadata }
@@ -265,6 +267,181 @@ function createWebSocketTransport(options) {
265
267
  }
266
268
  };
267
269
  }
270
+ function createMultiplexedWebSocketConnection(options) {
271
+ const { url, role, sessionId: primarySessionId, metadata: primaryMetadata, reconnect = true, maxReconnectAttempts = 10 } = options;
272
+ let ws = null;
273
+ let destroyed = false;
274
+ let connectionReady = false;
275
+ let reconnectAttempts = 0;
276
+ let reconnectTimer = null;
277
+ const pendingOutbound = [];
278
+ const channels = /* @__PURE__ */ new Map();
279
+ function ensureChannel(opts) {
280
+ const existing = channels.get(opts.sessionId);
281
+ if (existing) return existing;
282
+ const state = {
283
+ sessionId: opts.sessionId,
284
+ metadata: opts.metadata,
285
+ ready: false,
286
+ readyHandlers: /* @__PURE__ */ new Set(),
287
+ messageHandlers: /* @__PURE__ */ new Set(),
288
+ registered: false,
289
+ transport: null
290
+ };
291
+ state.transport = {
292
+ send(msg) {
293
+ if (destroyed) return;
294
+ const stamped = {
295
+ ...msg,
296
+ sessionId: state.sessionId
297
+ };
298
+ if (ws && ws.readyState === WebSocket.OPEN && state.registered) {
299
+ ws.send(JSON.stringify(stamped));
300
+ return;
301
+ }
302
+ pendingOutbound.push(stamped);
303
+ },
304
+ onMessage(handler) {
305
+ state.messageHandlers.add(handler);
306
+ return () => {
307
+ state.messageHandlers.delete(handler);
308
+ };
309
+ },
310
+ destroy() {
311
+ state.messageHandlers.clear();
312
+ state.readyHandlers.clear();
313
+ channels.delete(state.sessionId);
314
+ },
315
+ get ready() {
316
+ return state.ready;
317
+ },
318
+ onReady(handler) {
319
+ state.readyHandlers.add(handler);
320
+ if (state.ready) handler();
321
+ return () => {
322
+ state.readyHandlers.delete(handler);
323
+ };
324
+ }
325
+ };
326
+ channels.set(state.sessionId, state);
327
+ if (connectionReady && ws && ws.readyState === WebSocket.OPEN) registerChannel(state);
328
+ return state;
329
+ }
330
+ function registerChannel(state) {
331
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
332
+ if (state.registered) return;
333
+ if (state.sessionId === primarySessionId) return;
334
+ ws.send(JSON.stringify({
335
+ type: "@statelyai.addSession",
336
+ sessionId: state.sessionId,
337
+ ...state.metadata && { metadata: state.metadata }
338
+ }));
339
+ }
340
+ function markChannelReady(sessionId) {
341
+ const state = channels.get(sessionId);
342
+ if (!state || state.ready) return;
343
+ state.ready = true;
344
+ state.registered = true;
345
+ state.readyHandlers.forEach((fn) => fn());
346
+ flushPending();
347
+ }
348
+ function flushPending() {
349
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
350
+ const remaining = [];
351
+ for (const msg of pendingOutbound) {
352
+ const sessionId = msg.sessionId ?? primarySessionId;
353
+ const state = channels.get(sessionId);
354
+ if (state && state.registered) ws.send(JSON.stringify(msg));
355
+ else remaining.push(msg);
356
+ }
357
+ pendingOutbound.length = 0;
358
+ pendingOutbound.push(...remaining);
359
+ }
360
+ function connect() {
361
+ if (destroyed) return;
362
+ ws = new WebSocket(url);
363
+ ws.onopen = () => {
364
+ reconnectAttempts = 0;
365
+ ws.send(JSON.stringify({
366
+ type: "@statelyai.register",
367
+ protocolVersion: INSPECTION_PROTOCOL_VERSION,
368
+ role,
369
+ sessionId: primarySessionId,
370
+ ...primaryMetadata && { metadata: primaryMetadata }
371
+ }));
372
+ };
373
+ ws.onmessage = (event) => {
374
+ if (destroyed) return;
375
+ let raw;
376
+ try {
377
+ raw = JSON.parse(event.data);
378
+ } catch {
379
+ return;
380
+ }
381
+ if (!raw?.type || typeof raw.type !== "string") return;
382
+ if (!raw.type.startsWith(PREFIX)) return;
383
+ const data = raw;
384
+ if (data.type === "@statelyai.registered") {
385
+ connectionReady = true;
386
+ markChannelReady(primarySessionId);
387
+ for (const state of channels.values()) if (!state.registered && state.sessionId !== primarySessionId) registerChannel(state);
388
+ flushPending();
389
+ return;
390
+ }
391
+ if (data.type === "@statelyai.sessionAdded") {
392
+ const added = data.sessionId;
393
+ if (added) markChannelReady(added);
394
+ return;
395
+ }
396
+ const sessionId = raw.sessionId ?? primarySessionId;
397
+ const state = channels.get(sessionId);
398
+ if (state) state.messageHandlers.forEach((fn) => fn(data));
399
+ };
400
+ ws.onclose = () => {
401
+ if (destroyed) return;
402
+ connectionReady = false;
403
+ for (const state of channels.values()) {
404
+ state.ready = false;
405
+ state.registered = false;
406
+ }
407
+ if (reconnect && reconnectAttempts < maxReconnectAttempts) {
408
+ const delay = Math.min(1e3 * 2 ** reconnectAttempts, 3e4);
409
+ reconnectAttempts++;
410
+ reconnectTimer = setTimeout(connect, delay);
411
+ }
412
+ };
413
+ ws.onerror = () => {};
414
+ }
415
+ ensureChannel({
416
+ sessionId: primarySessionId,
417
+ metadata: primaryMetadata
418
+ });
419
+ connect();
420
+ return {
421
+ openChannel(opts) {
422
+ return ensureChannel(opts).transport;
423
+ },
424
+ destroy() {
425
+ if (destroyed) return;
426
+ destroyed = true;
427
+ if (reconnectTimer) clearTimeout(reconnectTimer);
428
+ ws?.close();
429
+ ws = null;
430
+ for (const state of channels.values()) {
431
+ state.messageHandlers.clear();
432
+ state.readyHandlers.clear();
433
+ }
434
+ channels.clear();
435
+ pendingOutbound.length = 0;
436
+ },
437
+ get ready() {
438
+ return connectionReady;
439
+ },
440
+ get primarySessionId() {
441
+ return primarySessionId;
442
+ }
443
+ };
444
+ }
268
445
 
269
446
  //#endregion
270
- export { toInitMessage as a, createPendingExportManager as i, createWebSocketTransport as n, createEventRegistry as r, createPostMessageTransport as t };
447
+ export { createPendingExportManager as a, createEventRegistry as i, createPostMessageTransport as n, toInitMessage as o, createWebSocketTransport as r, createMultiplexedWebSocketConnection as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist",
@@ -58,6 +58,10 @@
58
58
  "types": "./dist/protocol.d.mts",
59
59
  "import": "./dist/protocol.mjs"
60
60
  },
61
+ "./relay": {
62
+ "types": "./dist/relay.d.mts",
63
+ "import": "./dist/relay.mjs"
64
+ },
61
65
  "./statelyai.schema.json": "./schemas/statelyai.schema.json",
62
66
  "./schemas/statelyai.schema.json": "./schemas/statelyai.schema.json",
63
67
  "./xstate.schema.json": "./schemas/xstate.schema.json",
@@ -76,7 +80,8 @@
76
80
  "jsdom": "^27.4.0",
77
81
  "tsdown": "0.21.0-beta.2",
78
82
  "vitest": "^3.2.4",
79
- "@statelyai/graph-tools": "0.0.3"
83
+ "ws": "^8.19.0",
84
+ "@statelyai/graph-tools": "0.0.4"
80
85
  },
81
86
  "scripts": {
82
87
  "build": "tsdown",
File without changes