@xstate-devtools/adapter 0.1.4 → 0.1.5

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/server.cjs CHANGED
@@ -33,110 +33,8 @@ __export(server_exports, {
33
33
  createServerAdapter: () => createServerAdapter
34
34
  });
35
35
  module.exports = __toCommonJS(server_exports);
36
- var import_node_net = require("net");
37
-
38
- // src/logging.ts
39
- function hasProcessEnv() {
40
- return typeof process !== "undefined" && typeof process.env !== "undefined";
41
- }
42
- function isLoggingEnabled() {
43
- if (globalThis.__XSTATE_DEVTOOLS_LOGGING__ === true) return true;
44
- if (!hasProcessEnv()) return false;
45
- const value = process.env.XSTATE_DEVTOOLS_LOGGING;
46
- return value === "1" || value === "true";
47
- }
48
- function log(level, scope, message, details) {
49
- if (!isLoggingEnabled()) return;
50
- if (details === void 0) {
51
- console[level](`[xstate-devtools:${scope}] ${message}`);
52
- return;
53
- }
54
- console[level](`[xstate-devtools:${scope}] ${message}`, details);
55
- }
56
- function debugLog(scope, message, details) {
57
- log("debug", scope, message, details);
58
- }
59
- function infoLog(scope, message, details) {
60
- log("info", scope, message, details);
61
- }
62
- function warnLog(scope, message, details) {
63
- log("warn", scope, message, details);
64
- }
65
-
66
- // src/sanitize.ts
67
- var MAX_DEPTH = 10;
68
- var MAX_STRING_LENGTH = 500;
69
- var MAX_ARRAY_LENGTH = 100;
70
- function sanitizeValue(value, depth, seen) {
71
- if (depth > MAX_DEPTH) return "[MaxDepth]";
72
- if (value === null || value === void 0) return value;
73
- if (typeof value === "boolean" || typeof value === "number") return value;
74
- if (typeof value === "string") {
75
- return value.length > MAX_STRING_LENGTH ? `${value.slice(0, MAX_STRING_LENGTH)}\u2026` : value;
76
- }
77
- if (typeof value === "function") return `[Function: ${value.name || "(anonymous)"}]`;
78
- if (typeof value === "symbol") return `[Symbol: ${value.description ?? ""}]`;
79
- if (typeof value === "bigint") return `[BigInt: ${value}]`;
80
- if (value instanceof Error) return { __type: "Error", name: value.name, message: value.message };
81
- if (value instanceof Date) return { __type: "Date", iso: value.toISOString() };
82
- if (value instanceof RegExp) return { __type: "RegExp", source: value.source, flags: value.flags };
83
- if (typeof value === "object") {
84
- if (seen.has(value)) return "[Circular]";
85
- seen.add(value);
86
- }
87
- if (value instanceof Map) {
88
- const entries = [];
89
- for (const [k, v] of value) {
90
- if (entries.length >= MAX_ARRAY_LENGTH) break;
91
- entries.push([sanitizeValue(k, depth + 1, seen), sanitizeValue(v, depth + 1, seen)]);
92
- }
93
- return { __type: "Map", entries };
94
- }
95
- if (value instanceof Set) {
96
- const values = [];
97
- for (const v of value) {
98
- if (values.length >= MAX_ARRAY_LENGTH) break;
99
- values.push(sanitizeValue(v, depth + 1, seen));
100
- }
101
- return { __type: "Set", values };
102
- }
103
- if (value instanceof Promise) return "[Promise]";
104
- if (value instanceof WeakMap || value instanceof WeakSet) return "[WeakCollection]";
105
- if (ArrayBuffer.isView(value)) return `[TypedArray: ${value.constructor.name}]`;
106
- if (typeof Node !== "undefined" && value instanceof Node) {
107
- return `[DOMNode: ${value.tagName ?? value.nodeName}]`;
108
- }
109
- if (Array.isArray(value)) {
110
- const sliced = value.slice(0, MAX_ARRAY_LENGTH);
111
- const result = sliced.map((v) => sanitizeValue(v, depth + 1, seen));
112
- if (value.length > MAX_ARRAY_LENGTH) result.push(`[\u2026${value.length - MAX_ARRAY_LENGTH} more]`);
113
- return result;
114
- }
115
- if (typeof value === "object") {
116
- const result = {};
117
- let count = 0;
118
- for (const [k, v] of Object.entries(value)) {
119
- if (count++ >= MAX_ARRAY_LENGTH) {
120
- result["\u2026"] = "[truncated]";
121
- break;
122
- }
123
- result[k] = sanitizeValue(v, depth + 1, seen);
124
- }
125
- return result;
126
- }
127
- return String(value);
128
- }
129
- function sanitize(value, depth = 0) {
130
- return sanitizeValue(value, depth, /* @__PURE__ */ new WeakSet());
131
- }
132
36
 
133
37
  // src/serialize.ts
134
- var MAX_SERIALIZED_NODES = 500;
135
- var MAX_TRANSITIONS_PER_NODE = 100;
136
- var MAX_CHILD_STATES = 100;
137
- var MAX_ACTIONS_PER_TRANSITION = 20;
138
- var MAX_ENTRY_EXIT_ACTIONS = 20;
139
- var MAX_INVOKES_PER_NODE = 20;
140
38
  function serializeGuard(guard) {
141
39
  if (!guard) return void 0;
142
40
  if (typeof guard === "string") return guard;
@@ -157,214 +55,133 @@ function serializeAction(action) {
157
55
  return String(action);
158
56
  }
159
57
  function serializeTransitionList(transitions) {
160
- return transitions.slice(0, MAX_TRANSITIONS_PER_NODE).map((t) => ({
58
+ return transitions.map((t) => ({
161
59
  eventType: t.eventType ?? "",
162
60
  targets: (t.target ?? []).map((n) => n?.id ?? String(n)).filter(Boolean),
163
61
  guard: serializeGuard(t.guard),
164
- actions: (t.actions ?? []).slice(0, MAX_ACTIONS_PER_TRANSITION).map(serializeAction).filter(Boolean)
62
+ actions: (t.actions ?? []).map(serializeAction).filter(Boolean)
165
63
  }));
166
64
  }
167
65
  function serializeInvokes(node) {
168
- return node.invoke.slice(0, MAX_INVOKES_PER_NODE).map((inv) => ({
66
+ return node.invoke.map((inv) => ({
169
67
  id: inv.id ?? "(unknown)",
170
68
  src: typeof inv.src === "string" ? inv.src : inv.src?.id ?? inv.src?.name ?? "(inline)"
171
69
  }));
172
70
  }
173
- function serializeNode(node, state) {
174
- if (!node || typeof node !== "object") {
175
- return {
176
- id: "(unknown)",
177
- key: "(unknown)",
178
- type: "atomic",
179
- states: {},
180
- on: [],
181
- always: [],
182
- entry: [],
183
- exit: [],
184
- invoke: []
185
- };
186
- }
187
- if (state.count >= MAX_SERIALIZED_NODES) {
188
- return {
189
- id: node.id ?? "(truncated)",
190
- key: node.key ?? "(truncated)",
191
- type: node.type ?? "atomic",
192
- states: {},
193
- on: [],
194
- always: [],
195
- entry: [],
196
- exit: [],
197
- invoke: []
198
- };
199
- }
200
- if (state.seen.has(node)) {
201
- return {
202
- id: node.id ?? "(circular)",
203
- key: node.key ?? "(circular)",
204
- type: node.type ?? "atomic",
205
- states: {},
206
- on: [],
207
- always: [],
208
- entry: [],
209
- exit: [],
210
- invoke: []
211
- };
212
- }
213
- state.seen.add(node);
214
- state.count += 1;
71
+ function serializeNode(node) {
215
72
  const allTransitions = [];
216
73
  if (node.transitions instanceof Map) {
217
74
  for (const [, tList] of node.transitions) {
218
- if (allTransitions.length >= MAX_TRANSITIONS_PER_NODE) break;
219
75
  allTransitions.push(...serializeTransitionList(tList));
220
- if (allTransitions.length >= MAX_TRANSITIONS_PER_NODE) {
221
- allTransitions.length = MAX_TRANSITIONS_PER_NODE;
222
- break;
223
- }
224
76
  }
225
77
  }
226
78
  const always = Array.isArray(node.always) ? serializeTransitionList(node.always) : [];
227
- const childEntries = Object.entries(node.states ?? {}).slice(0, MAX_CHILD_STATES);
228
79
  return {
229
80
  id: node.id,
230
81
  key: node.key,
231
82
  type: node.type,
232
83
  initial: node.initial?.target?.[0]?.key,
233
- states: Object.fromEntries(childEntries.map(([k, v]) => [k, serializeNode(v, state)])),
84
+ states: Object.fromEntries(
85
+ Object.entries(node.states ?? {}).map(([k, v]) => [k, serializeNode(v)])
86
+ ),
234
87
  on: allTransitions,
235
88
  always,
236
- entry: (node.entry ?? []).slice(0, MAX_ENTRY_EXIT_ACTIONS).map(serializeAction).filter(Boolean),
237
- exit: (node.exit ?? []).slice(0, MAX_ENTRY_EXIT_ACTIONS).map(serializeAction).filter(Boolean),
238
- invoke: serializeInvokes(node),
239
- sourceLocation: node.config?.__xstateDevtoolsSource ?? void 0,
240
- description: node.config?.description ?? void 0
89
+ entry: (node.entry ?? []).map(serializeAction).filter(Boolean),
90
+ exit: (node.exit ?? []).map(serializeAction).filter(Boolean),
91
+ invoke: serializeInvokes(node)
241
92
  };
242
93
  }
243
94
  function serializeMachine(machine, sourceLocation) {
244
95
  return {
245
96
  id: machine.id,
246
- root: serializeNode(machine.root, { seen: /* @__PURE__ */ new WeakSet(), count: 0 }),
97
+ root: serializeNode(machine.root),
247
98
  sourceLocation
248
99
  };
249
100
  }
250
101
 
251
- // src/core.ts
252
- function summarizeMessage(message) {
253
- const summary = { type: message.type };
254
- if ("sessionId" in message) summary.sessionId = message.sessionId;
255
- if ("stateNodeId" in message) summary.stateNodeId = message.stateNodeId;
256
- if ("parentSessionId" in message && message.parentSessionId) {
257
- summary.parentSessionId = message.parentSessionId;
102
+ // src/sanitize.ts
103
+ var MAX_DEPTH = 10;
104
+ var MAX_STRING_LENGTH = 500;
105
+ var MAX_ARRAY_LENGTH = 100;
106
+ var MAX_NODES = 1e4;
107
+ function sanitizeInner(value, ctx) {
108
+ if (ctx.depth > MAX_DEPTH) return "[MaxDepth]";
109
+ if (++ctx.budget.n > MAX_NODES) return "[Truncated]";
110
+ if (value === null || value === void 0) return value;
111
+ if (typeof value === "boolean" || typeof value === "number") return value;
112
+ if (typeof value === "string") {
113
+ return value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) + "\u2026" : value;
258
114
  }
259
- if ("globalSeq" in message) summary.globalSeq = message.globalSeq;
260
- if ("timestamp" in message) summary.timestamp = message.timestamp;
261
- if ("event" in message && message.event && typeof message.event === "object" && "type" in message.event) {
262
- summary.eventType = message.event.type;
115
+ if (typeof value === "function") return `[Function: ${value.name || "(anonymous)"}]`;
116
+ if (typeof value === "symbol") return `[Symbol: ${value.description ?? ""}]`;
117
+ if (typeof value === "bigint") return `[BigInt: ${value}]`;
118
+ if (value instanceof Error) return { __type: "Error", name: value.name, message: value.message };
119
+ if (value instanceof Date) return { __type: "Date", iso: value.toISOString() };
120
+ if (value instanceof RegExp) return { __type: "RegExp", source: value.source, flags: value.flags };
121
+ if (value instanceof Promise) return "[Promise]";
122
+ if (value instanceof WeakMap || value instanceof WeakSet) return "[WeakCollection]";
123
+ if (ArrayBuffer.isView(value)) return `[TypedArray: ${value.constructor.name}]`;
124
+ if (typeof Node !== "undefined" && value instanceof Node) {
125
+ return `[DOMNode: ${value.tagName ?? value.nodeName}]`;
263
126
  }
264
- return summary;
265
- }
266
- function summarizeInspectionEvent(event) {
267
- return {
268
- type: event?.type,
269
- sessionId: event?.actorRef?.sessionId,
270
- eventType: event?.type === "@xstate.event" && event?.event && typeof event.event === "object" ? event.event.type : void 0
271
- };
272
- }
273
- function debugLog2(source, message, details) {
274
- debugLog(`${source}:adapter`, message, details);
275
- }
276
- function infoLog2(source, message, details) {
277
- infoLog(`${source}:adapter`, message, details);
278
- }
279
- function warnLog2(source, message, details) {
280
- warnLog(`${source}:adapter`, message, details);
281
- }
282
- function isLibraryStackFrame(line) {
283
- const normalized = line.replace(/\\/g, "/").toLowerCase();
284
- return normalized.includes("/node_modules/xstate/") || normalized.includes("/node_modules/@xstate/") || normalized.includes("/@xstate-devtools/adapter/") || normalized.includes("/packages/adapter/");
285
- }
286
- function normalizeStackFrame(line) {
287
- return line.trim().replace(/^at\s+/, "");
288
- }
289
- function extractStackFrameLocation(frame) {
290
- return frame.match(/\((.*)\)$/)?.[1] ?? frame;
291
- }
292
- function isAnonymousOrEvalLocation(location) {
293
- const normalized = location.trim().toLowerCase().replace(/^[./\\]+/, "");
294
- return normalized === "<anonymous>" || normalized === "anonymous" || normalized === "(anonymous)" || normalized === "eval" || normalized === "<eval>" || normalized === "[native code]";
295
- }
296
- function hasFilesystemBackedPath(location) {
297
- const trimmed = location.trim();
298
- if (!trimmed) return false;
299
- const match = trimmed.match(/^(.*?)(?::\d+)?(?::\d+)?$/);
300
- const rawPath = (match?.[1] ?? trimmed).trim();
301
- if (!rawPath || isAnonymousOrEvalLocation(rawPath)) return false;
302
- if (/^[a-zA-Z]:[\\/]/.test(rawPath)) return true;
303
- if (rawPath.startsWith("/") || rawPath.startsWith("./") || rawPath.startsWith("../")) return true;
304
- if (/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(rawPath)) {
305
- try {
306
- const url = new URL(rawPath);
307
- if (url.protocol === "file:") return true;
308
- return url.pathname.startsWith("/@fs/");
309
- } catch {
310
- return false;
127
+ if (ctx.seen.has(value)) return "[Circular]";
128
+ ctx.seen.add(value);
129
+ const child = { ...ctx, depth: ctx.depth + 1 };
130
+ if (value instanceof Map) {
131
+ const entries = [];
132
+ for (const [k, v] of value) {
133
+ if (entries.length >= MAX_ARRAY_LENGTH) break;
134
+ entries.push([sanitizeInner(k, child), sanitizeInner(v, child)]);
311
135
  }
136
+ return { __type: "Map", entries };
312
137
  }
313
- return false;
314
- }
315
- function remapWebUrlLocationToFsPath(location, source, options) {
316
- if (source !== "web" || !options?.webSourceRoot) return location;
317
- const match = location.trim().match(/^(.*?)(?::(\d+))?(?::(\d+))?$/);
318
- if (!match) return location;
319
- const [, rawPath, line, column] = match;
320
- if (!rawPath || !/^https?:\/\//.test(rawPath)) return location;
321
- try {
322
- const url = new URL(rawPath);
323
- const pathname = decodeURIComponent(url.pathname);
324
- if (!pathname.startsWith("/app/")) return location;
325
- const root = options.webSourceRoot.replace(/\/+$/, "");
326
- const filePath = `${root}${pathname}`;
327
- const suffix = line ? `:${line}${column ? `:${column}` : ""}` : "";
328
- return `${filePath}${suffix}`;
329
- } catch {
330
- return location;
138
+ if (value instanceof Set) {
139
+ const values = [];
140
+ for (const v of value) {
141
+ if (values.length >= MAX_ARRAY_LENGTH) break;
142
+ values.push(sanitizeInner(v, child));
143
+ }
144
+ return { __type: "Set", values };
331
145
  }
332
- }
333
- function getSourceLocationFromStack(stack, source = "web", options) {
334
- const lines = stack?.split("\n") ?? [];
335
- for (let i = 0; i < lines.length; i += 1) {
336
- if (i <= 2) continue;
337
- const line = lines[i];
338
- if (isLibraryStackFrame(line)) continue;
339
- const normalizedFrame = normalizeStackFrame(line);
340
- const location = remapWebUrlLocationToFsPath(
341
- extractStackFrameLocation(normalizedFrame),
342
- source,
343
- options
344
- );
345
- if (!hasFilesystemBackedPath(location)) continue;
346
- const wrapped = normalizedFrame.match(/\((.*)\)$/);
347
- if (wrapped) {
348
- return normalizedFrame.replace(wrapped[1], location);
146
+ if (Array.isArray(value)) {
147
+ const sliced = value.slice(0, MAX_ARRAY_LENGTH);
148
+ const result = sliced.map((v) => sanitizeInner(v, child));
149
+ if (value.length > MAX_ARRAY_LENGTH) result.push(`[\u2026${value.length - MAX_ARRAY_LENGTH} more]`);
150
+ return result;
151
+ }
152
+ if (typeof value === "object") {
153
+ const result = {};
154
+ let count = 0;
155
+ for (const [k, v] of Object.entries(value)) {
156
+ if (count++ >= MAX_ARRAY_LENGTH) {
157
+ result["\u2026"] = "[truncated]";
158
+ break;
159
+ }
160
+ result[k] = sanitizeInner(v, child);
349
161
  }
350
- return location;
162
+ return result;
351
163
  }
352
- return void 0;
164
+ return String(value);
353
165
  }
354
- function getSourceLocation(source, options) {
166
+ function sanitize(value) {
167
+ return sanitizeInner(value, { depth: 0, budget: { n: 0 }, seen: /* @__PURE__ */ new WeakSet() });
168
+ }
169
+
170
+ // src/core.ts
171
+ function getSourceLocation() {
355
172
  try {
356
- const oldLimit = Error.stackTraceLimit;
357
- Error.stackTraceLimit = 50;
358
- const stack = new Error().stack;
359
- Error.stackTraceLimit = oldLimit;
360
- return getSourceLocationFromStack(stack, source, options);
173
+ const lines = new Error().stack?.split("\n") ?? [];
174
+ const callerLine = lines.find(
175
+ (l, i) => i > 3 && !l.includes("xstate") && !l.includes("adapter")
176
+ );
177
+ return callerLine?.trim().replace(/^at\s+/, "");
361
178
  } catch {
362
179
  return void 0;
363
180
  }
364
181
  }
365
182
  function serializeSnapshot(snapshot) {
366
183
  return {
367
- value: snapshot?.value ?? null,
184
+ value: sanitize(snapshot?.value ?? null),
368
185
  context: sanitize(snapshot?.context),
369
186
  status: snapshot?.status ?? "active",
370
187
  error: snapshot?.error ? sanitize(snapshot.error) : void 0
@@ -377,125 +194,18 @@ function safeSerializeSnapshot(actorRef) {
377
194
  return { value: null, context: void 0, status: "active" };
378
195
  }
379
196
  }
380
- function getActorDisplayName(actorRef) {
381
- const actor = actorRef;
382
- const src = actor.src ?? actor.logic?.src;
383
- if (typeof src === "string" && src.length > 0) return src;
384
- if (typeof actor.logic?.id === "string" && actor.logic.id.length > 0) return actor.logic.id;
385
- if (typeof actor.logic?.name === "string" && actor.logic.name.length > 0) return actor.logic.name;
386
- if (src && typeof src === "object") {
387
- const namedSrc = src;
388
- if (typeof namedSrc.id === "string" && namedSrc.id.length > 0) return namedSrc.id;
389
- if (typeof namedSrc.name === "string" && namedSrc.name.length > 0) return namedSrc.name;
390
- }
391
- return void 0;
392
- }
393
- function getNodeInitialChild(node) {
394
- if (!node.states) return null;
395
- if (typeof node.initial === "string") {
396
- return node.states[node.initial] ?? null;
397
- }
398
- const target = Array.isArray(node.initial?.target) ? node.initial.target[0] : null;
399
- return target ?? null;
400
- }
401
- function encodeChildValue(child, childValue) {
402
- if (child.type === "atomic" || child.type === "final" || child.type === "history") {
403
- return child.key;
404
- }
405
- return { [child.key]: childValue };
406
- }
407
- function getDefaultStateValue(node) {
408
- if (node.type === "parallel") {
409
- const value = {};
410
- for (const child of Object.values(node.states ?? {})) {
411
- value[child.key] = getDefaultSelectionValue(child);
412
- }
413
- return value;
414
- }
415
- const initialChild = getNodeInitialChild(node);
416
- if (!initialChild) return {};
417
- return encodeChildValue(initialChild, getDefaultStateValue(initialChild));
418
- }
419
- function getDefaultSelectionValue(node) {
420
- if (node.type === "atomic" || node.type === "final" || node.type === "history") {
421
- return node.key;
422
- }
423
- return getDefaultStateValue(node);
424
- }
425
- function getExistingChildValue(value, childKey) {
426
- if (!value || typeof value !== "object") return void 0;
427
- return value[childKey];
428
- }
429
- function getPathToRoot(target, root) {
430
- const path = [];
431
- let current = target;
432
- while (current) {
433
- path.unshift(current);
434
- if (current.id === root.id) return path;
435
- current = current.parent;
436
- }
437
- throw new Error(`State node '${target.id}' is not part of machine '${root.id}'`);
438
- }
439
- function buildTargetStateValue(node, path, currentValue) {
440
- const [, ...restPath] = path;
441
- if (restPath.length === 0) {
442
- if (node.type === "parallel") {
443
- const next = {};
444
- for (const child2 of Object.values(node.states ?? {})) {
445
- next[child2.key] = getExistingChildValue(currentValue, child2.key) ?? getDefaultSelectionValue(child2);
446
- }
447
- return next;
448
- }
449
- if (node.type === "compound") {
450
- return getDefaultStateValue(node);
451
- }
452
- return node.key;
197
+ function safePersistedSnapshot(actorRef) {
198
+ const getPersisted = actorRef.getPersistedSnapshot;
199
+ if (typeof getPersisted !== "function") {
200
+ return { error: "Actor does not support getPersistedSnapshot." };
453
201
  }
454
- const child = restPath[0];
455
- if (node.type === "parallel") {
456
- const next = {};
457
- for (const sibling of Object.values(node.states ?? {})) {
458
- if (sibling.key === child.key) {
459
- next[sibling.key] = buildTargetStateValue(
460
- sibling,
461
- restPath,
462
- getExistingChildValue(currentValue, sibling.key)
463
- );
464
- } else {
465
- next[sibling.key] = getExistingChildValue(currentValue, sibling.key) ?? getDefaultSelectionValue(sibling);
466
- }
467
- }
468
- return next;
469
- }
470
- const childValue = buildTargetStateValue(
471
- child,
472
- restPath,
473
- getExistingChildValue(currentValue, child.key)
474
- );
475
- return encodeChildValue(child, childValue);
476
- }
477
- function setActiveState(actorRef, stateNodeId) {
478
- const mutableActorRef = actorRef;
479
- const machine = mutableActorRef.logic;
480
- if (!machine?.getStateNodeById || !machine.resolveState || typeof mutableActorRef.update !== "function") {
481
- throw new Error("Actor does not expose machine state mutation internals");
202
+ try {
203
+ const raw = getPersisted.call(actorRef);
204
+ if (raw === void 0) return { error: "No persisted snapshot available." };
205
+ return { persisted: JSON.parse(JSON.stringify(raw)) };
206
+ } catch (e) {
207
+ return { error: `Snapshot is not JSON-serializable: ${e.message}` };
482
208
  }
483
- const currentSnapshot = actorRef.getSnapshot();
484
- const targetNode = machine.getStateNodeById(stateNodeId);
485
- const path = getPathToRoot(targetNode, machine.root);
486
- const targetValue = buildTargetStateValue(machine.root, path, currentSnapshot?.value);
487
- const nextSnapshot = machine.resolveState({
488
- value: targetValue,
489
- context: currentSnapshot?.context,
490
- status: currentSnapshot?.status,
491
- output: currentSnapshot?.output,
492
- error: currentSnapshot?.error,
493
- historyValue: currentSnapshot?.historyValue
494
- });
495
- mutableActorRef.update(nextSnapshot, {
496
- type: "xstate.devtools.set-active-state",
497
- stateNodeId
498
- });
499
209
  }
500
210
  var SEQ_KEY = "__xstate_devtools_global_seq__";
501
211
  function nextSeq() {
@@ -505,10 +215,10 @@ function nextSeq() {
505
215
  g[SEQ_KEY] = next;
506
216
  return next;
507
217
  }
508
- function createInspector(transport, source, options = {}) {
218
+ function createInspector(transport, source) {
509
219
  const actorRefs = /* @__PURE__ */ new Map();
510
- const actorMachines = /* @__PURE__ */ new Map();
511
- const prefix = `${source}:`;
220
+ const restoreHandlers = /* @__PURE__ */ new Map();
221
+ const prefix = source + ":";
512
222
  const tag = (sessionId) => prefix + sessionId;
513
223
  const tagOptional = (id) => id ? prefix + id : void 0;
514
224
  const stripIfMine = (id) => id.startsWith(prefix) ? id.slice(prefix.length) : null;
@@ -520,300 +230,166 @@ function createInspector(transport, source, options = {}) {
520
230
  return;
521
231
  }
522
232
  if (snap?.status !== "active") {
523
- const message = {
524
- type: "XSTATE_ACTOR_STOPPED",
525
- sessionId: tag(actorRef.sessionId)
526
- };
527
- debugLog2(source, "actor stopped; notifying transport", summarizeMessage(message));
528
- transport.send(message);
233
+ transport.send({ type: "XSTATE_ACTOR_STOPPED", sessionId: tag(actorRef.sessionId) });
529
234
  actorRefs.delete(actorRef.sessionId);
530
- actorMachines.delete(actorRef.sessionId);
531
235
  }
532
236
  }
533
237
  const unsubscribe = transport.subscribe((message) => {
534
- debugLog2(source, "received message from transport", summarizeMessage(message));
535
- if (message.type === "XSTATE_PANEL_CONNECTED") {
536
- infoLog2(source, "panel connected; resyncing actors", { actorCount: actorRefs.size });
537
- actorRefs.forEach((actorRef, sessionId) => {
538
- const machine = actorMachines.get(sessionId) ?? null;
539
- const resyncMessage = {
540
- type: "XSTATE_ACTOR_REGISTERED",
541
- sessionId: tag(sessionId),
542
- parentSessionId: tagOptional(actorRef._parent?.sessionId),
543
- displayName: getActorDisplayName(actorRef),
544
- machine,
545
- snapshot: safeSerializeSnapshot(actorRef),
546
- globalSeq: nextSeq(),
547
- timestamp: Date.now()
548
- };
549
- debugLog2(source, "resyncing actor", summarizeMessage(resyncMessage));
550
- transport.send(resyncMessage);
551
- });
552
- return;
553
- }
554
238
  if (message.type === "XSTATE_DISPATCH") {
555
239
  const local = stripIfMine(message.sessionId);
556
- if (local === null) {
557
- debugLog2(source, "ignoring dispatch for different source", summarizeMessage(message));
558
- return;
559
- }
240
+ if (local === null) return;
560
241
  const ref = actorRefs.get(local);
561
242
  if (ref) {
562
243
  try {
563
- debugLog2(source, "dispatching event to actor", {
564
- sessionId: local,
565
- eventType: message.event && typeof message.event === "object" && "type" in message.event ? message.event.type : void 0
566
- });
567
244
  ref.send(message.event);
568
245
  } catch (e) {
569
- warnLog2(source, "dispatch error", { error: e, sessionId: local });
246
+ console.warn("[xstate-devtools] dispatch error:", e);
570
247
  }
571
- } else {
572
- warnLog2(source, "received dispatch for unknown actor", {
573
- sessionId: local,
574
- knownActors: actorRefs.size
575
- });
576
248
  }
577
- return;
578
- }
579
- if (message.type === "XSTATE_SET_ACTIVE_STATE") {
249
+ } else if (message.type === "XSTATE_REQUEST_PERSISTED") {
580
250
  const local = stripIfMine(message.sessionId);
581
- if (local === null) {
582
- debugLog2(
583
- source,
584
- "ignoring state activation for different source",
585
- summarizeMessage(message)
586
- );
587
- return;
588
- }
251
+ if (local === null) return;
589
252
  const ref = actorRefs.get(local);
590
- if (!ref) {
591
- warnLog2(source, "received state activation for unknown actor", {
592
- sessionId: local,
593
- knownActors: actorRefs.size
594
- });
595
- return;
253
+ if (!ref) return;
254
+ const { persisted, error } = safePersistedSnapshot(ref);
255
+ transport.send({
256
+ type: "XSTATE_PERSISTED_SNAPSHOT",
257
+ sessionId: tag(local),
258
+ persisted,
259
+ error,
260
+ timestamp: Date.now()
261
+ });
262
+ } else if (message.type === "XSTATE_RESTORE") {
263
+ const local = stripIfMine(message.sessionId);
264
+ if (local === null) return;
265
+ const handler = restoreHandlers.get(local);
266
+ if (handler) {
267
+ try {
268
+ handler(message.persisted);
269
+ } catch (e) {
270
+ console.warn("[xstate-devtools] restore error:", e);
271
+ }
596
272
  }
597
- try {
598
- debugLog2(source, "setting active state on actor", summarizeMessage(message));
599
- setActiveState(ref, message.stateNodeId);
600
- const snapshotMessage = {
273
+ }
274
+ });
275
+ const inspect = (inspectionEvent) => {
276
+ try {
277
+ if (inspectionEvent.type === "@xstate.actor") {
278
+ const actorRef = inspectionEvent.actorRef;
279
+ const sessionId = actorRef.sessionId;
280
+ actorRefs.set(sessionId, actorRef);
281
+ const logic = actorRef.logic;
282
+ const machine = logic?.root ? serializeMachine(logic, getSourceLocation()) : null;
283
+ transport.send({
284
+ type: "XSTATE_ACTOR_REGISTERED",
285
+ sessionId: tag(sessionId),
286
+ parentSessionId: tagOptional(actorRef._parent?.sessionId),
287
+ // actorRef.id is the invoke `id` for invoked actors — lets the debugger
288
+ // nest non-machine actors (promise/callback) under their state.
289
+ actorId: actorRef.id,
290
+ machine,
291
+ snapshot: safeSerializeSnapshot(actorRef),
292
+ globalSeq: nextSeq(),
293
+ timestamp: Date.now()
294
+ });
295
+ } else if (inspectionEvent.type === "@xstate.snapshot") {
296
+ transport.send({
601
297
  type: "XSTATE_SNAPSHOT",
602
- sessionId: tag(local),
603
- snapshot: safeSerializeSnapshot(ref),
298
+ sessionId: tag(inspectionEvent.actorRef.sessionId),
299
+ snapshot: serializeSnapshot(inspectionEvent.snapshot),
604
300
  timestamp: Date.now(),
605
301
  globalSeq: nextSeq()
606
- };
607
- debugLog2(
608
- source,
609
- "sending snapshot after state activation",
610
- summarizeMessage(snapshotMessage)
611
- );
612
- transport.send(snapshotMessage);
613
- } catch (error) {
614
- warnLog2(source, "failed to set active state", {
615
- error,
616
- sessionId: local,
617
- stateNodeId: message.stateNodeId
618
302
  });
303
+ checkAndNotifyStop(inspectionEvent.actorRef);
304
+ } else if (inspectionEvent.type === "@xstate.event") {
305
+ transport.send({
306
+ type: "XSTATE_EVENT",
307
+ sessionId: tag(inspectionEvent.actorRef.sessionId),
308
+ event: inspectionEvent.event,
309
+ snapshotAfter: safeSerializeSnapshot(inspectionEvent.actorRef),
310
+ timestamp: Date.now(),
311
+ globalSeq: nextSeq()
312
+ });
313
+ checkAndNotifyStop(inspectionEvent.actorRef);
619
314
  }
620
- }
621
- });
622
- transport.send({ type: "XSTATE_ADAPTER_READY" });
623
- infoLog2(source, "inspector created");
624
- const inspect = (inspectionEvent) => {
625
- debugLog2(source, "inspect callback invoked", summarizeInspectionEvent(inspectionEvent));
626
- if (inspectionEvent.type === "@xstate.actor") {
627
- const actorRef = inspectionEvent.actorRef;
628
- const sessionId = actorRef.sessionId;
629
- const actorLogic = actorRef.logic;
630
- const machine = actorLogic?.root ? serializeMachine(
631
- actorLogic,
632
- actorLogic.config?.__xstateDevtoolsSource ?? getSourceLocation(source, options)
633
- ) : null;
634
- actorRefs.set(sessionId, actorRef);
635
- actorMachines.set(sessionId, machine);
636
- const notifyStop = () => {
637
- if (actorRefs.has(sessionId)) {
638
- actorRefs.delete(sessionId);
639
- actorMachines.delete(sessionId);
640
- const stopMsg = {
641
- type: "XSTATE_ACTOR_STOPPED",
642
- sessionId: tag(sessionId)
643
- };
644
- debugLog2(source, "actor stopped; notifying transport", summarizeMessage(stopMsg));
645
- transport.send(stopMsg);
646
- }
647
- };
648
- try {
649
- actorRef.subscribe({ complete: notifyStop, error: notifyStop });
650
- } catch {
651
- }
652
- const message = {
653
- type: "XSTATE_ACTOR_REGISTERED",
654
- sessionId: tag(sessionId),
655
- parentSessionId: tagOptional(actorRef._parent?.sessionId),
656
- displayName: getActorDisplayName(actorRef),
657
- machine,
658
- snapshot: safeSerializeSnapshot(actorRef),
659
- globalSeq: nextSeq(),
660
- timestamp: Date.now()
661
- };
662
- infoLog2(source, "registering actor with transport", {
663
- message: summarizeMessage(message),
664
- actorCount: actorRefs.size,
665
- hasMachine: machine !== null
666
- });
667
- transport.send(message);
668
- } else if (inspectionEvent.type === "@xstate.snapshot") {
669
- const message = {
670
- type: "XSTATE_SNAPSHOT",
671
- sessionId: tag(inspectionEvent.actorRef.sessionId),
672
- snapshot: serializeSnapshot(inspectionEvent.snapshot),
673
- timestamp: Date.now(),
674
- globalSeq: nextSeq()
675
- };
676
- debugLog2(source, "sending snapshot to transport", summarizeMessage(message));
677
- transport.send(message);
678
- checkAndNotifyStop(inspectionEvent.actorRef);
679
- } else if (inspectionEvent.type === "@xstate.event") {
680
- const message = {
681
- type: "XSTATE_EVENT",
682
- sessionId: tag(inspectionEvent.actorRef.sessionId),
683
- // sanitize() returns unknown; the inspected event keeps its { type, ... }
684
- // shape, so it is a SerializedEvent after deep-sanitizing.
685
- event: sanitize(inspectionEvent.event),
686
- snapshotAfter: safeSerializeSnapshot(inspectionEvent.actorRef),
687
- timestamp: Date.now(),
688
- globalSeq: nextSeq()
689
- };
690
- debugLog2(source, "sending event to transport", summarizeMessage(message));
691
- transport.send(message);
692
- checkAndNotifyStop(inspectionEvent.actorRef);
693
- } else {
694
- debugLog2(
695
- source,
696
- "ignoring unsupported inspection event",
697
- summarizeInspectionEvent(inspectionEvent)
698
- );
315
+ } catch (e) {
316
+ console.warn("[xstate-devtools] inspection failed, dropping event:", e.message);
699
317
  }
700
318
  };
319
+ function registerRestore(sessionId, handler) {
320
+ restoreHandlers.set(sessionId, handler);
321
+ return () => {
322
+ if (restoreHandlers.get(sessionId) === handler) restoreHandlers.delete(sessionId);
323
+ };
324
+ }
701
325
  function dispose() {
702
- infoLog2(source, "disposing inspector", { actorCount: actorRefs.size });
703
326
  unsubscribe();
704
327
  actorRefs.clear();
705
- actorMachines.clear();
328
+ restoreHandlers.clear();
706
329
  }
707
- return { inspect, dispose };
330
+ return { inspect, dispose, registerRestore };
708
331
  }
709
332
 
710
333
  // src/server.ts
711
- function getAvailablePort(start) {
712
- return new Promise((resolve, reject) => {
713
- const probe = (0, import_node_net.createServer)();
714
- probe.unref();
715
- probe.on("error", (err) => {
716
- if (err.code === "EADDRINUSE") {
717
- resolve(getAvailablePort(start + 1));
718
- } else {
719
- reject(err);
720
- }
721
- });
722
- probe.listen(start, "127.0.0.1", () => {
723
- const port = probe.address().port;
724
- probe.close(() => resolve(port));
725
- });
726
- });
727
- }
728
334
  var OPEN_STATE = 1;
729
- function summarizeMessage2(message) {
730
- const summary = { type: message.type };
731
- if ("sessionId" in message) summary.sessionId = message.sessionId;
732
- if ("parentSessionId" in message && message.parentSessionId) {
733
- summary.parentSessionId = message.parentSessionId;
734
- }
735
- if ("globalSeq" in message) summary.globalSeq = message.globalSeq;
736
- if ("timestamp" in message) summary.timestamp = message.timestamp;
737
- if ("event" in message && message.event && typeof message.event === "object" && "type" in message.event) {
738
- summary.eventType = message.event.type;
739
- }
740
- return summary;
741
- }
742
- function debugLog3(message, details) {
743
- debugLog("server", message, details);
744
- }
745
- function infoLog3(message, details) {
746
- infoLog("server", message, details);
747
- }
748
- function warnLog3(message, details) {
749
- warnLog("server", message, details);
750
- }
751
- function stringifyOutgoingMessage(message) {
752
- const payload = { ...message, __xstateDevtools: true };
753
- try {
754
- return JSON.stringify(payload);
755
- } catch (error) {
756
- warnLog3("failed to stringify adapter message; retrying with sanitized payload", {
757
- error,
758
- message: summarizeMessage2(message)
759
- });
760
- }
761
- try {
762
- return JSON.stringify(sanitize(payload));
763
- } catch (error) {
764
- warnLog3("dropping adapter message that could not be stringified", {
765
- error,
766
- message: summarizeMessage2(message)
767
- });
768
- return null;
335
+ function trackLive(server, message) {
336
+ switch (message.type) {
337
+ case "XSTATE_ACTOR_REGISTERED":
338
+ server.liveActors.set(message.sessionId, { reg: message, snapshot: message.snapshot });
339
+ break;
340
+ case "XSTATE_SNAPSHOT": {
341
+ const live = server.liveActors.get(message.sessionId);
342
+ if (live) {
343
+ live.snapshot = message.snapshot;
344
+ }
345
+ break;
346
+ }
347
+ case "XSTATE_EVENT": {
348
+ const live = server.liveActors.get(message.sessionId);
349
+ if (live) {
350
+ live.snapshot = message.snapshotAfter;
351
+ }
352
+ break;
353
+ }
354
+ case "XSTATE_ACTOR_STOPPED":
355
+ server.liveActors.delete(message.sessionId);
356
+ break;
769
357
  }
770
358
  }
771
359
  function createServerAdapter(options = {}) {
772
360
  const port = options.port ?? (Number(process.env.XSTATE_DEVTOOLS_PORT) || 9301);
773
361
  const host = options.host ?? "127.0.0.1";
774
362
  const bufferSize = options.bufferSize ?? 200;
775
- infoLog3("createServerAdapter called", { host, port, bufferSize });
776
363
  const key = `__xstate_devtools_server_${port}__`;
777
364
  const cache = globalThis[key];
778
365
  let server;
779
366
  if (cache) {
780
367
  server = cache;
781
- infoLog3("reusing cached WebSocket server", {
782
- host,
783
- port,
784
- clientCount: server.clients.size,
785
- bufferedMessages: server.buffer.length
786
- });
787
368
  if (bufferSize > server.bufferSize) server.bufferSize = bufferSize;
788
369
  } else {
789
370
  const clients = /* @__PURE__ */ new Set();
790
371
  const dispatchHandlers = /* @__PURE__ */ new Set();
791
- const buffer = [];
372
+ const liveActors = /* @__PURE__ */ new Map();
373
+ const recentEvents = [];
792
374
  let wss = null;
793
375
  let closed = false;
794
- let portResolve;
795
- let portReject;
796
- const portPromise = new Promise((res, rej) => {
797
- portResolve = res;
798
- portReject = rej;
799
- });
800
376
  server = {
801
377
  clients,
802
378
  dispatchHandlers,
803
- buffer,
379
+ liveActors,
380
+ recentEvents,
804
381
  bufferSize,
805
382
  activated: false,
806
- port: portPromise,
807
383
  close: () => {
808
384
  closed = true;
809
- infoLog3("closing WebSocket server", { host, port, clientCount: clients.size });
810
385
  try {
811
386
  wss?.close();
812
387
  } catch {
813
388
  }
814
389
  clients.clear();
815
390
  dispatchHandlers.clear();
816
- buffer.length = 0;
391
+ liveActors.clear();
392
+ recentEvents.length = 0;
817
393
  delete globalThis[key];
818
394
  }
819
395
  };
@@ -822,112 +398,93 @@ function createServerAdapter(options = {}) {
822
398
  const mod = await import("ws");
823
399
  const WSServer = mod.WebSocketServer ?? mod.Server;
824
400
  if (closed) return;
825
- const actualPort = await getAvailablePort(port);
826
- if (closed) return;
827
- wss = new WSServer({ port: actualPort, host });
828
- process.env.XSTATE_DEVTOOLS_PORT = String(actualPort);
829
- portResolve(actualPort);
830
- infoLog3("WebSocket server listening", { host, port: actualPort });
401
+ wss = new WSServer({ port, host });
831
402
  wss.on("connection", (ws) => {
832
- infoLog3("panel connected to WebSocket server", {
833
- host,
834
- port,
835
- activated: server.activated,
836
- bufferedMessages: server.buffer.length
837
- });
403
+ for (const { reg, snapshot } of server.liveActors.values()) {
404
+ try {
405
+ ws.send(JSON.stringify({ ...reg, __xstateDevtools: true }));
406
+ } catch {
407
+ }
408
+ if (snapshot !== reg.snapshot) {
409
+ try {
410
+ ws.send(JSON.stringify({
411
+ type: "XSTATE_SNAPSHOT",
412
+ sessionId: reg.sessionId,
413
+ snapshot,
414
+ timestamp: reg.timestamp,
415
+ globalSeq: reg.globalSeq,
416
+ __xstateDevtools: true
417
+ }));
418
+ } catch {
419
+ }
420
+ }
421
+ }
422
+ try {
423
+ ws.send(JSON.stringify({
424
+ type: "XSTATE_REPLAY_DONE",
425
+ sessionIds: [...server.liveActors.keys()],
426
+ __xstateDevtools: true
427
+ }));
428
+ } catch {
429
+ }
838
430
  if (!server.activated) {
839
431
  server.activated = true;
840
- infoLog3("flushing bootstrap buffer to first panel", {
841
- host,
842
- port,
843
- bufferedMessages: server.buffer.length
844
- });
845
- for (const payload of server.buffer) {
432
+ for (const payload of server.recentEvents) {
846
433
  try {
847
434
  ws.send(payload);
848
435
  } catch {
849
436
  }
850
437
  }
851
- server.buffer.length = 0;
438
+ server.recentEvents.length = 0;
852
439
  }
853
440
  server.clients.add(ws);
854
441
  ws.on("message", (raw) => {
855
442
  try {
856
443
  const text = typeof raw === "string" ? raw : raw.toString("utf8");
857
444
  const msg = JSON.parse(text);
858
- debugLog3("received dispatch from panel", summarizeMessage2(msg));
859
445
  for (const cb of server.dispatchHandlers) cb(msg);
860
- } catch (error) {
861
- warnLog3("failed to parse panel message", { error });
446
+ } catch {
862
447
  }
863
448
  });
864
- ws.on("close", () => {
865
- server.clients.delete(ws);
866
- infoLog3("panel disconnected from WebSocket server", {
867
- host,
868
- port,
869
- clientCount: server.clients.size
870
- });
871
- });
872
- ws.on("error", (error) => {
873
- server.clients.delete(ws);
874
- warnLog3("WebSocket client error", { error });
875
- });
449
+ ws.on("close", () => server.clients.delete(ws));
450
+ ws.on("error", () => server.clients.delete(ws));
876
451
  });
877
452
  wss.on("error", (err) => {
878
- warnLog3("WS server error", { host, port, message: err.message });
453
+ console.warn("[xstate-devtools] WS server error:", err.message);
879
454
  });
880
455
  } catch (e) {
881
- portReject(e);
882
- warnLog3("could not start server adapter \u2014 install `ws` to enable", {
883
- host,
884
- port,
885
- message: e.message
886
- });
456
+ console.warn(
457
+ "[xstate-devtools] could not start server adapter \u2014 install `ws` to enable.",
458
+ e.message
459
+ );
887
460
  }
888
461
  })();
889
462
  globalThis[key] = server;
890
463
  }
891
464
  const transport = {
892
465
  send(message) {
893
- const payload = stringifyOutgoingMessage(message);
894
- if (payload === null) return;
895
- if (!server.activated) {
896
- if (server.buffer.length >= server.bufferSize) server.buffer.shift();
897
- server.buffer.push(payload);
898
- debugLog3("buffered outgoing adapter message; no panel connected yet", {
899
- bufferedMessages: server.buffer.length,
900
- message: summarizeMessage2(message)
901
- });
902
- return;
466
+ trackLive(server, message);
467
+ const payload = JSON.stringify({ ...message, __xstateDevtools: true });
468
+ if (!server.activated && (message.type === "XSTATE_EVENT" || message.type === "XSTATE_SNAPSHOT")) {
469
+ server.recentEvents.push(payload);
470
+ if (server.recentEvents.length > server.bufferSize) server.recentEvents.shift();
903
471
  }
904
- let sentCount = 0;
905
472
  for (const ws of server.clients) {
906
473
  if (ws.readyState === OPEN_STATE) {
907
474
  try {
908
475
  ws.send(payload);
909
- sentCount += 1;
910
476
  } catch {
911
477
  }
912
478
  }
913
479
  }
914
- debugLog3("sent adapter message to connected panels", {
915
- sentCount,
916
- clientCount: server.clients.size,
917
- message: summarizeMessage2(message)
918
- });
919
480
  },
920
481
  subscribe(handler) {
921
482
  server.dispatchHandlers.add(handler);
922
- debugLog3("registered dispatch handler", { handlerCount: server.dispatchHandlers.size });
923
- return () => {
924
- server.dispatchHandlers.delete(handler);
925
- debugLog3("removed dispatch handler", { handlerCount: server.dispatchHandlers.size });
926
- };
483
+ return () => server.dispatchHandlers.delete(handler);
927
484
  }
928
485
  };
929
486
  const inspector = createInspector(transport, "srv");
930
- return { ...inspector, close: server.close, port: server.port };
487
+ return { ...inspector, close: server.close };
931
488
  }
932
489
  // Annotate the CommonJS export names for ESM import in node:
933
490
  0 && (module.exports = {