@statelyai/sdk 0.14.2 → 0.16.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.
@@ -157,9 +157,7 @@ function createPostMessageTransport(options) {
157
157
  },
158
158
  onMessage(handler) {
159
159
  messageHandlers.add(handler);
160
- return () => {
161
- messageHandlers.delete(handler);
162
- };
160
+ return () => messageHandlers.delete(handler);
163
161
  },
164
162
  destroy() {
165
163
  if (destroyed) return;
@@ -177,31 +175,52 @@ function createPostMessageTransport(options) {
177
175
  return () => {};
178
176
  }
179
177
  readyHandlers.add(handler);
180
- return () => {
181
- readyHandlers.delete(handler);
182
- };
178
+ return () => readyHandlers.delete(handler);
183
179
  }
184
180
  };
185
181
  }
186
182
  function createWebSocketTransport(options) {
187
- const { url, role, sessionId, metadata, reconnect = true, maxReconnectAttempts = 10 } = options;
183
+ const { url, role, producerId, metadata, reconnect = true, maxReconnectAttempts = 10 } = options;
184
+ if (role === "producer" && !producerId) throw new Error("A producerId is required for producer transports.");
188
185
  let ws = null;
189
186
  let destroyed = false;
187
+ let terminalFailure = false;
190
188
  let isReady = false;
191
189
  let reconnectAttempts = 0;
192
190
  let reconnectTimer = null;
193
191
  const readyHandlers = /* @__PURE__ */ new Set();
194
192
  const messageHandlers = /* @__PURE__ */ new Set();
193
+ const errorHandlers = /* @__PURE__ */ new Set();
194
+ function fail(failure) {
195
+ errorHandlers.forEach((handler) => handler(failure));
196
+ }
197
+ function scheduleReconnect() {
198
+ if (!reconnect || reconnectAttempts >= maxReconnectAttempts) {
199
+ terminalFailure = true;
200
+ fail({
201
+ code: "connection_failed",
202
+ message: `Unable to connect to inspection relay ${url}`
203
+ });
204
+ return;
205
+ }
206
+ const delay = Math.min(1e3 * 2 ** reconnectAttempts, 3e4);
207
+ reconnectAttempts++;
208
+ reconnectTimer = setTimeout(connect, delay);
209
+ }
195
210
  function connect() {
196
- if (destroyed) return;
197
- ws = new WebSocket(url);
211
+ if (destroyed || terminalFailure) return;
212
+ try {
213
+ ws = new WebSocket(url);
214
+ } catch {
215
+ scheduleReconnect();
216
+ return;
217
+ }
198
218
  ws.onopen = () => {
199
- reconnectAttempts = 0;
200
219
  ws.send(JSON.stringify({
201
220
  type: "@statelyai.register",
202
221
  protocolVersion: INSPECTION_PROTOCOL_VERSION,
203
222
  role,
204
- sessionId,
223
+ ...producerId && { producerId },
205
224
  ...metadata && { metadata }
206
225
  }));
207
226
  };
@@ -217,20 +236,33 @@ function createWebSocketTransport(options) {
217
236
  if (!raw.type.startsWith(PREFIX)) return;
218
237
  const data = raw;
219
238
  if (data.type === "@statelyai.registered") {
239
+ if (data.protocolVersion !== INSPECTION_PROTOCOL_VERSION) {
240
+ terminalFailure = true;
241
+ fail({
242
+ code: "unsupported_protocol",
243
+ message: `Relay selected protocol ${data.protocolVersion}; expected ${INSPECTION_PROTOCOL_VERSION}`
244
+ });
245
+ ws?.close(1002, "Unsupported inspection protocol");
246
+ return;
247
+ }
248
+ reconnectAttempts = 0;
220
249
  const wasReady = isReady;
221
250
  isReady = true;
222
251
  if (!wasReady) readyHandlers.forEach((fn) => fn());
252
+ } else if (data.type === "@statelyai.error" && !isReady) {
253
+ terminalFailure = true;
254
+ fail({
255
+ code: data.code,
256
+ message: data.message
257
+ });
258
+ ws?.close(1002, data.message.slice(0, 123));
223
259
  }
224
260
  messageHandlers.forEach((fn) => fn(data));
225
261
  };
226
262
  ws.onclose = () => {
227
- if (destroyed) return;
263
+ if (destroyed || terminalFailure) return;
228
264
  isReady = false;
229
- if (reconnect && reconnectAttempts < maxReconnectAttempts) {
230
- const delay = Math.min(1e3 * 2 ** reconnectAttempts, 3e4);
231
- reconnectAttempts++;
232
- reconnectTimer = setTimeout(connect, delay);
233
- }
265
+ scheduleReconnect();
234
266
  };
235
267
  ws.onerror = () => {};
236
268
  }
@@ -242,9 +274,7 @@ function createWebSocketTransport(options) {
242
274
  },
243
275
  onMessage(handler) {
244
276
  messageHandlers.add(handler);
245
- return () => {
246
- messageHandlers.delete(handler);
247
- };
277
+ return () => messageHandlers.delete(handler);
248
278
  },
249
279
  destroy() {
250
280
  if (destroyed) return;
@@ -254,6 +284,7 @@ function createWebSocketTransport(options) {
254
284
  ws = null;
255
285
  messageHandlers.clear();
256
286
  readyHandlers.clear();
287
+ errorHandlers.clear();
257
288
  },
258
289
  get ready() {
259
290
  return isReady && !!ws && ws.readyState === WebSocket.OPEN;
@@ -261,187 +292,14 @@ function createWebSocketTransport(options) {
261
292
  onReady(handler) {
262
293
  readyHandlers.add(handler);
263
294
  if (isReady) handler();
264
- return () => {
265
- readyHandlers.delete(handler);
266
- };
267
- }
268
- };
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;
295
+ return () => readyHandlers.delete(handler);
439
296
  },
440
- get primarySessionId() {
441
- return primarySessionId;
297
+ onError(handler) {
298
+ errorHandlers.add(handler);
299
+ return () => errorHandlers.delete(handler);
442
300
  }
443
301
  };
444
302
  }
445
303
 
446
304
  //#endregion
447
- export { createPendingExportManager as a, createEventRegistry as i, createPostMessageTransport as n, toInitMessage as o, createWebSocketTransport as r, createMultiplexedWebSocketConnection as t };
305
+ export { toInitMessage as a, createPendingExportManager as i, createWebSocketTransport as n, createEventRegistry as r, createPostMessageTransport as t };
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "license": "MIT",
5
+ "bin": {
6
+ "stately-sdk": "./dist/bin.mjs"
7
+ },
5
8
  "files": [
6
9
  "dist",
7
10
  "schemas"
@@ -62,6 +65,10 @@
62
65
  "types": "./dist/relay.d.mts",
63
66
  "import": "./dist/relay.mjs"
64
67
  },
68
+ "./node-relay": {
69
+ "types": "./dist/node-relay.d.mts",
70
+ "import": "./dist/node-relay.mjs"
71
+ },
65
72
  "./statelyai.schema.json": "./schemas/statelyai.schema.json",
66
73
  "./schemas/statelyai.schema.json": "./schemas/statelyai.schema.json",
67
74
  "./xstate.schema.json": "./schemas/xstate.schema.json",
@@ -73,20 +80,25 @@
73
80
  "@statelyai/graph": "^1.0.0",
74
81
  "esbuild": "^0.27.0",
75
82
  "typescript": "^5.9.3",
83
+ "ws": "^8.19.0",
76
84
  "xstate": "^5.0.0"
77
85
  },
78
86
  "devDependencies": {
79
87
  "@types/json-schema": "^7.0.15",
88
+ "@types/ws": "^8.18.1",
80
89
  "jsdom": "^27.4.0",
81
90
  "tsdown": "0.21.0-beta.2",
82
91
  "vitest": "^3.2.4",
83
- "ws": "^8.19.0",
92
+ "@statelyai/api-contract": "0.0.1",
84
93
  "@statelyai/graph-tools": "0.0.7"
85
94
  },
95
+ "engines": {
96
+ "node": ">=22.4"
97
+ },
86
98
  "scripts": {
87
99
  "build": "tsdown",
88
100
  "test": "vitest run",
89
- "test:live": "vitest run src/studio.live.integration.test.ts",
101
+ "test:live": "vitest run --config vitest.live.config.ts",
90
102
  "test:watch": "vitest"
91
103
  }
92
104
  }