@statelyai/sdk 0.15.0 → 0.16.1

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/relay.mjs CHANGED
@@ -1,6 +1,29 @@
1
1
  import { INSPECTION_PROTOCOL_VERSION, PREFIX } from "./protocol.mjs";
2
2
 
3
3
  //#region src/relay.ts
4
+ const REPLAYABLE_MESSAGE_TYPES = new Set([
5
+ "@statelyai.init",
6
+ "@statelyai.inspectSnapshot",
7
+ "@statelyai.system.init",
8
+ "@statelyai.system.actorRegistered",
9
+ "@statelyai.system.actorEvent",
10
+ "@statelyai.system.actorSnapshot",
11
+ "@statelyai.system.actorStopped"
12
+ ]);
13
+ const FORWARD_ONLY_MESSAGE_TYPES = new Set([
14
+ "@statelyai.system.selectActor",
15
+ "@statelyai.retrieve",
16
+ "@statelyai.retrieved",
17
+ "@statelyai.error",
18
+ "@statelyai.ready",
19
+ "@statelyai.loaded",
20
+ "@statelyai.capabilityDenied",
21
+ "@statelyai.register",
22
+ "@statelyai.registered"
23
+ ]);
24
+ function isSupportedMessageType(type) {
25
+ return REPLAYABLE_MESSAGE_TYPES.has(type) || FORWARD_ONLY_MESSAGE_TYPES.has(type);
26
+ }
4
27
  function parseMessage(raw) {
5
28
  if (raw instanceof ArrayBuffer) raw = new TextDecoder().decode(raw);
6
29
  if (typeof raw === "string") try {
@@ -12,6 +35,22 @@ function parseMessage(raw) {
12
35
  const message = raw;
13
36
  return typeof message.type === "string" && message.type.startsWith(PREFIX) ? message : null;
14
37
  }
38
+ function validationError(message) {
39
+ if (!isSupportedMessageType(message.type)) return `Unsupported inspection message type ${String(message.type)}`;
40
+ const hasString = (key) => typeof message[key] === "string" && message[key] !== "";
41
+ switch (message.type) {
42
+ case "@statelyai.system.init": return message.protocolVersion === INSPECTION_PROTOCOL_VERSION && Array.isArray(message.actors) ? null : "system.init requires the current protocolVersion and actors array";
43
+ case "@statelyai.system.selectActor":
44
+ case "@statelyai.system.actorStopped": return hasString("sessionId") ? null : `${message.type} requires sessionId`;
45
+ case "@statelyai.system.actorRegistered": return hasString("sessionId") && hasString("actorId") ? null : "actorRegistered requires sessionId and actorId";
46
+ case "@statelyai.system.actorEvent": return hasString("sessionId") && "event" in message ? null : "actorEvent requires sessionId and event";
47
+ case "@statelyai.system.actorSnapshot": return hasString("sessionId") && "snapshot" in message ? null : "actorSnapshot requires sessionId and snapshot";
48
+ case "@statelyai.retrieve":
49
+ case "@statelyai.retrieved": return hasString("requestId") ? null : `${message.type} requires requestId`;
50
+ case "@statelyai.error": return hasString("code") && hasString("message") ? null : "error requires code and message";
51
+ default: return null;
52
+ }
53
+ }
15
54
  function errorMessage(code, message) {
16
55
  return {
17
56
  type: "@statelyai.error",
@@ -30,10 +69,10 @@ function compactReplay(replay, message, max) {
30
69
  const index = replay.findIndex((entry) => entry.type === "@statelyai.init");
31
70
  if (index >= 0) replay.splice(index, 1);
32
71
  } else if (type === "@statelyai.system.actorSnapshot" || type === "@statelyai.inspectSnapshot") {
33
- const actorId = record.actorId;
72
+ const sessionId = record.sessionId;
34
73
  const index = replay.findIndex((entry) => {
35
74
  const candidate = entry;
36
- return candidate.type === type && candidate.actorId === actorId;
75
+ return candidate.type === type && candidate.sessionId === sessionId;
37
76
  });
38
77
  if (index >= 0) replay.splice(index, 1);
39
78
  }
@@ -41,110 +80,92 @@ function compactReplay(replay, message, max) {
41
80
  if (replay.length > max) replay.splice(0, replay.length - max);
42
81
  }
43
82
  /**
44
- * Runtime-neutral WebSocket inspection room.
83
+ * Runtime-neutral implementation of one inspection room.
45
84
  *
46
- * It owns protocol registration, routing, multiplexed session membership, and
47
- * bounded late-join replay. Node and Durable Object adapters only translate
48
- * `send` effects to their native WebSocket APIs and persist `snapshot()`.
85
+ * The hosting adapter selects the room from the WebSocket URL. This module
86
+ * owns protocol registration, multi-producer/viewer routing, and bounded
87
+ * per-producer replay.
49
88
  */
50
89
  function createInspectionRelay(options = {}) {
51
90
  const maxReplayMessages = options.maxReplayMessages ?? 200;
52
- const maxReplaySessions = Math.max(1, options.maxReplaySessions ?? 100);
91
+ const maxReplayProducers = Math.max(1, options.maxReplayProducers ?? 100);
53
92
  const peers = /* @__PURE__ */ new Map();
54
- const sessions = new Map(Object.entries(options.snapshot?.sessions ?? {}).slice(-maxReplaySessions).map(([id, replay]) => [id, [...replay]]));
93
+ const producers = new Map(Object.entries(options.snapshot?.producers ?? {}).slice(-maxReplayProducers).map(([id, replay]) => [id, [...replay]]));
55
94
  const send = (peerId, message) => ({
56
95
  type: "send",
57
96
  peerId,
58
97
  message
59
98
  });
60
- function replayTo(peerId, sessionId) {
61
- return (sessions.get(sessionId) ?? []).map((message) => send(peerId, message));
62
- }
63
- function oppositePeers(sender, sessionId) {
64
- return Array.from(peers.values()).filter((peer) => peer.id !== sender.id && peer.role !== sender.role && peer.sessionIds.includes(sessionId));
65
- }
66
- function retainReplay(sessionId, replay) {
67
- sessions.delete(sessionId);
68
- sessions.set(sessionId, replay);
69
- while (sessions.size > maxReplaySessions) {
70
- const oldestSessionId = sessions.keys().next().value;
71
- if (oldestSessionId === void 0) break;
72
- sessions.delete(oldestSessionId);
99
+ function retainReplay(producerId, replay) {
100
+ producers.delete(producerId);
101
+ producers.set(producerId, replay);
102
+ while (producers.size > maxReplayProducers) {
103
+ const oldestProducerId = producers.keys().next().value;
104
+ if (oldestProducerId === void 0) break;
105
+ producers.delete(oldestProducerId);
73
106
  }
74
107
  }
75
108
  return {
76
109
  connect(peer) {
77
- peers.set(peer.id, {
78
- ...peer,
79
- sessionIds: Array.from(new Set([peer.primarySessionId, ...peer.sessionIds]))
80
- });
110
+ peers.set(peer.id, { ...peer });
81
111
  },
82
112
  disconnect(peerId) {
83
113
  peers.delete(peerId);
84
114
  },
85
115
  clear() {
86
116
  peers.clear();
87
- sessions.clear();
117
+ producers.clear();
88
118
  },
89
119
  peer(peerId) {
90
120
  const peer = peers.get(peerId);
91
- return peer ? {
92
- ...peer,
93
- sessionIds: [...peer.sessionIds]
94
- } : void 0;
121
+ return peer ? { ...peer } : void 0;
95
122
  },
96
123
  receive(peerId, raw) {
97
124
  const message = parseMessage(raw);
98
- if (!message) return [];
125
+ if (!message) return [send(peerId, errorMessage("invalid_message", "Expected a JSON protocol message"))];
126
+ const invalid = validationError(message);
127
+ if (invalid) return [send(peerId, errorMessage("invalid_message", invalid))];
99
128
  if (message.type === "@statelyai.register") {
100
129
  const role = message.role;
101
- const sessionId = message.sessionId;
102
130
  const protocolVersion = message.protocolVersion;
103
- if (role !== "client" && role !== "viz" || typeof sessionId !== "string" || sessionId.length === 0) return [send(peerId, errorMessage("invalid_registration", "Invalid relay registration"))];
104
- if (typeof protocolVersion === "number" && protocolVersion > INSPECTION_PROTOCOL_VERSION) return [send(peerId, errorMessage("unsupported_protocol", `Protocol ${protocolVersion} is not supported`))];
131
+ const producerId = message.producerId;
132
+ if (role !== "producer" && role !== "viewer" || role === "producer" && (typeof producerId !== "string" || producerId.length === 0)) return [send(peerId, errorMessage("invalid_registration", "Invalid relay registration"))];
133
+ if (protocolVersion !== INSPECTION_PROTOCOL_VERSION) return [send(peerId, errorMessage("unsupported_protocol", `Protocol ${String(protocolVersion)} is not supported; expected ${INSPECTION_PROTOCOL_VERSION}`))];
105
134
  const peer = {
106
135
  id: peerId,
107
136
  role,
108
- primarySessionId: sessionId,
109
- sessionIds: [sessionId]
137
+ ...role === "producer" && { producerId }
110
138
  };
111
139
  peers.set(peerId, peer);
112
140
  const effects = [send(peerId, {
113
141
  type: "@statelyai.registered",
114
142
  protocolVersion: INSPECTION_PROTOCOL_VERSION,
115
- sessionId
143
+ ...peer.producerId && { producerId: peer.producerId }
116
144
  })];
117
- if (role === "viz") effects.push(...replayTo(peerId, sessionId));
145
+ if (role === "viewer") for (const replay of producers.values()) effects.push(...replay.map((entry) => send(peerId, entry)));
118
146
  return effects;
119
147
  }
120
148
  const sender = peers.get(peerId);
121
149
  if (!sender) return [send(peerId, errorMessage("not_registered", "Register before sending messages"))];
122
- if (message.type === "@statelyai.addSession") {
123
- const sessionId = message.sessionId;
124
- if (typeof sessionId !== "string" || sessionId.length === 0) return [send(peerId, errorMessage("invalid_session", "Invalid session id"))];
125
- if (!sender.sessionIds.includes(sessionId)) sender.sessionIds.push(sessionId);
126
- const effects = [send(peerId, {
127
- type: "@statelyai.sessionAdded",
128
- sessionId
129
- })];
130
- if (sender.role === "viz") effects.push(...replayTo(peerId, sessionId));
131
- return effects;
132
- }
133
- const sessionId = (typeof message.sessionId === "string" ? message.sessionId : void 0) ?? sender.primarySessionId;
134
- if (!sender.sessionIds.includes(sessionId)) return [send(peerId, errorMessage("unknown_session", "Peer is not registered for session"))];
135
- const stamped = {
136
- ...message,
137
- sessionId
138
- };
139
- if (sender.role === "client") {
140
- const replay = sessions.get(sessionId) ?? [];
141
- compactReplay(replay, stamped, maxReplayMessages);
142
- retainReplay(sessionId, replay);
150
+ if (sender.role === "producer") {
151
+ const producerId = sender.producerId;
152
+ const stamped = {
153
+ ...message,
154
+ producerId
155
+ };
156
+ if (REPLAYABLE_MESSAGE_TYPES.has(message.type)) {
157
+ const replay = producers.get(producerId) ?? [];
158
+ compactReplay(replay, stamped, maxReplayMessages);
159
+ retainReplay(producerId, replay);
160
+ }
161
+ return Array.from(peers.values()).filter((peer) => peer.role === "viewer").map((peer) => send(peer.id, stamped));
143
162
  }
144
- return oppositePeers(sender, sessionId).map((peer) => send(peer.id, stamped));
163
+ const targetProducerId = typeof message.producerId === "string" ? message.producerId : void 0;
164
+ return Array.from(peers.values()).filter((peer) => peer.role === "producer" && (!targetProducerId || peer.producerId === targetProducerId)).map((peer) => send(peer.id, message));
145
165
  },
146
- snapshot() {
147
- return { sessions: Object.fromEntries(Array.from(sessions, ([sessionId, replay]) => [sessionId, [...replay]])) };
166
+ snapshot(producerId) {
167
+ const entries = producerId ? producers.has(producerId) ? [[producerId, producers.get(producerId)]] : [] : Array.from(producers.entries());
168
+ return { producers: Object.fromEntries(entries.map(([producerId, replay]) => [producerId, [...replay]])) };
148
169
  }
149
170
  };
150
171
  }
package/dist/studio.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as Machine, C as StudioMachineRecord, D as VerifyApiKeyResponse, E as UpdateProjectInput, F as ProjectVisibility, I as VerifyResult, L as XStateVersion, M as Project, N as ProjectMachineSummary, O as createStatelyClient, P as ProjectSummary, R as ResourceErrorCode, S as StudioClientOptions, T as UpdateMachineInput, _ as ProjectMachine, a as CreateMachineTemplate, b as StudioApiErrorOptions, c as EnsureProjectInput, d as ExtractedMachine, f as GetMachineOptions, g as ProjectData, h as LegacyUpdateMachineInput, i as CreateMachineInput, j as MachineVersion, k as ExtractedMachines, l as ExtractMachinesInput, m as LegacyProjectVisibility, n as CreateMachineFromDefinitionInput, o as CreateProjectInput, p as LegacyExtractMachinesOptions, r as CreateMachineFromTemplateInput, s as DEFAULT_STATELY_BASE_URL, t as ConnectedRepo, u as ExtractMachinesResponse, v as RepoType, w as StudioRequestOptions, x as StudioClient, y as StudioApiError } from "./studio-Da5pyxv6.mjs";
1
+ import { A as Machine, C as StudioMachineRecord, D as VerifyApiKeyResponse, E as UpdateProjectInput, F as ProjectVisibility, I as VerifyResult, L as XStateVersion, M as Project, N as ProjectMachineSummary, O as createStatelyClient, P as ProjectSummary, R as ResourceErrorCode, S as StudioClientOptions, T as UpdateMachineInput, _ as ProjectMachine, a as CreateMachineTemplate, b as StudioApiErrorOptions, c as EnsureProjectInput, d as ExtractedMachine, f as GetMachineOptions, g as ProjectData, h as LegacyUpdateMachineInput, i as CreateMachineInput, j as MachineVersion, k as ExtractedMachines, l as ExtractMachinesInput, m as LegacyProjectVisibility, n as CreateMachineFromDefinitionInput, o as CreateProjectInput, p as LegacyExtractMachinesOptions, r as CreateMachineFromTemplateInput, s as DEFAULT_STATELY_BASE_URL, t as ConnectedRepo, u as ExtractMachinesResponse, v as RepoType, w as StudioRequestOptions, x as StudioClient, y as StudioApiError } from "./studio-Bk_u41gu.mjs";
2
2
  export { ConnectedRepo, CreateMachineFromDefinitionInput, CreateMachineFromTemplateInput, CreateMachineInput, CreateMachineTemplate, CreateProjectInput, DEFAULT_STATELY_BASE_URL, EnsureProjectInput, ExtractMachinesInput, ExtractMachinesResponse, ExtractedMachine, ExtractedMachines, GetMachineOptions, LegacyExtractMachinesOptions, LegacyProjectVisibility, LegacyUpdateMachineInput, Machine, MachineVersion, Project, ProjectData, ProjectMachine, ProjectMachineSummary, ProjectSummary, ProjectVisibility, RepoType, ResourceErrorCode, StudioApiError, StudioApiErrorOptions, StudioClient, StudioClientOptions, StudioMachineRecord, StudioRequestOptions, UpdateMachineInput, UpdateProjectInput, VerifyApiKeyResponse, VerifyResult, XStateVersion, createStatelyClient };
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { b as DigraphConfig, i as StatelyGraph } from "./graph-DuOeLGvh.mjs";
2
- import { C as StudioMachineRecord, L as XStateVersion, g as ProjectData, o as CreateProjectInput, x as StudioClient } from "./studio-Da5pyxv6.mjs";
2
+ import { C as StudioMachineRecord, L as XStateVersion, g as ProjectData, o as CreateProjectInput, x as StudioClient } from "./studio-Bk_u41gu.mjs";
3
3
  import { GraphDiff } from "@statelyai/graph";
4
4
 
5
5
  //#region src/sync.d.ts
@@ -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.15.0",
3
+ "version": "0.16.1",
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,16 +80,20 @@
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",
84
- "@statelyai/graph-tools": "0.0.7",
85
- "@statelyai/api-contract": "0.0.1"
92
+ "@statelyai/api-contract": "0.0.1",
93
+ "@statelyai/graph-tools": "0.0.7"
94
+ },
95
+ "engines": {
96
+ "node": ">=22.4"
86
97
  },
87
98
  "scripts": {
88
99
  "build": "tsdown",