@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/react.cjs CHANGED
@@ -22,114 +22,15 @@ var react_exports = {};
22
22
  __export(react_exports, {
23
23
  InspectorProvider: () => InspectorProvider,
24
24
  useInspectedActorRef: () => useInspectedActorRef,
25
- useInspectedMachine: () => useInspectedMachine
25
+ useInspectedMachine: () => useInspectedMachine,
26
+ useRestorableInspectedMachine: () => useRestorableInspectedMachine
26
27
  });
27
28
  module.exports = __toCommonJS(react_exports);
28
- var import_react = require("@xstate/react");
29
- var import_react2 = require("react");
30
-
31
- // src/logging.ts
32
- function hasProcessEnv() {
33
- return typeof process !== "undefined" && typeof process.env !== "undefined";
34
- }
35
- function isLoggingEnabled() {
36
- if (globalThis.__XSTATE_DEVTOOLS_LOGGING__ === true) return true;
37
- if (!hasProcessEnv()) return false;
38
- const value = process.env.XSTATE_DEVTOOLS_LOGGING;
39
- return value === "1" || value === "true";
40
- }
41
- function log(level, scope, message, details) {
42
- if (!isLoggingEnabled()) return;
43
- if (details === void 0) {
44
- console[level](`[xstate-devtools:${scope}] ${message}`);
45
- return;
46
- }
47
- console[level](`[xstate-devtools:${scope}] ${message}`, details);
48
- }
49
- function debugLog(scope, message, details) {
50
- log("debug", scope, message, details);
51
- }
52
- function infoLog(scope, message, details) {
53
- log("info", scope, message, details);
54
- }
55
- function warnLog(scope, message, details) {
56
- log("warn", scope, message, details);
57
- }
58
-
59
- // src/sanitize.ts
60
- var MAX_DEPTH = 10;
61
- var MAX_STRING_LENGTH = 500;
62
- var MAX_ARRAY_LENGTH = 100;
63
- function sanitizeValue(value, depth, seen) {
64
- if (depth > MAX_DEPTH) return "[MaxDepth]";
65
- if (value === null || value === void 0) return value;
66
- if (typeof value === "boolean" || typeof value === "number") return value;
67
- if (typeof value === "string") {
68
- return value.length > MAX_STRING_LENGTH ? `${value.slice(0, MAX_STRING_LENGTH)}\u2026` : value;
69
- }
70
- if (typeof value === "function") return `[Function: ${value.name || "(anonymous)"}]`;
71
- if (typeof value === "symbol") return `[Symbol: ${value.description ?? ""}]`;
72
- if (typeof value === "bigint") return `[BigInt: ${value}]`;
73
- if (value instanceof Error) return { __type: "Error", name: value.name, message: value.message };
74
- if (value instanceof Date) return { __type: "Date", iso: value.toISOString() };
75
- if (value instanceof RegExp) return { __type: "RegExp", source: value.source, flags: value.flags };
76
- if (typeof value === "object") {
77
- if (seen.has(value)) return "[Circular]";
78
- seen.add(value);
79
- }
80
- if (value instanceof Map) {
81
- const entries = [];
82
- for (const [k, v] of value) {
83
- if (entries.length >= MAX_ARRAY_LENGTH) break;
84
- entries.push([sanitizeValue(k, depth + 1, seen), sanitizeValue(v, depth + 1, seen)]);
85
- }
86
- return { __type: "Map", entries };
87
- }
88
- if (value instanceof Set) {
89
- const values = [];
90
- for (const v of value) {
91
- if (values.length >= MAX_ARRAY_LENGTH) break;
92
- values.push(sanitizeValue(v, depth + 1, seen));
93
- }
94
- return { __type: "Set", values };
95
- }
96
- if (value instanceof Promise) return "[Promise]";
97
- if (value instanceof WeakMap || value instanceof WeakSet) return "[WeakCollection]";
98
- if (ArrayBuffer.isView(value)) return `[TypedArray: ${value.constructor.name}]`;
99
- if (typeof Node !== "undefined" && value instanceof Node) {
100
- return `[DOMNode: ${value.tagName ?? value.nodeName}]`;
101
- }
102
- if (Array.isArray(value)) {
103
- const sliced = value.slice(0, MAX_ARRAY_LENGTH);
104
- const result = sliced.map((v) => sanitizeValue(v, depth + 1, seen));
105
- if (value.length > MAX_ARRAY_LENGTH) result.push(`[\u2026${value.length - MAX_ARRAY_LENGTH} more]`);
106
- return result;
107
- }
108
- if (typeof value === "object") {
109
- const result = {};
110
- let count = 0;
111
- for (const [k, v] of Object.entries(value)) {
112
- if (count++ >= MAX_ARRAY_LENGTH) {
113
- result["\u2026"] = "[truncated]";
114
- break;
115
- }
116
- result[k] = sanitizeValue(v, depth + 1, seen);
117
- }
118
- return result;
119
- }
120
- return String(value);
121
- }
122
- function sanitize(value, depth = 0) {
123
- return sanitizeValue(value, depth, /* @__PURE__ */ new WeakSet());
124
- }
29
+ var import_react = require("react");
30
+ var import_react2 = require("@xstate/react");
31
+ var import_xstate = require("xstate");
125
32
 
126
33
  // src/serialize.ts
127
- var MAX_SERIALIZED_NODES = 500;
128
- var MAX_TRANSITIONS_PER_NODE = 100;
129
- var MAX_CHILD_STATES = 100;
130
- var MAX_ACTIONS_PER_TRANSITION = 20;
131
- var MAX_ENTRY_EXIT_ACTIONS = 20;
132
- var MAX_INVOKES_PER_NODE = 20;
133
34
  function serializeGuard(guard) {
134
35
  if (!guard) return void 0;
135
36
  if (typeof guard === "string") return guard;
@@ -150,214 +51,133 @@ function serializeAction(action) {
150
51
  return String(action);
151
52
  }
152
53
  function serializeTransitionList(transitions) {
153
- return transitions.slice(0, MAX_TRANSITIONS_PER_NODE).map((t) => ({
54
+ return transitions.map((t) => ({
154
55
  eventType: t.eventType ?? "",
155
56
  targets: (t.target ?? []).map((n) => n?.id ?? String(n)).filter(Boolean),
156
57
  guard: serializeGuard(t.guard),
157
- actions: (t.actions ?? []).slice(0, MAX_ACTIONS_PER_TRANSITION).map(serializeAction).filter(Boolean)
58
+ actions: (t.actions ?? []).map(serializeAction).filter(Boolean)
158
59
  }));
159
60
  }
160
61
  function serializeInvokes(node) {
161
- return node.invoke.slice(0, MAX_INVOKES_PER_NODE).map((inv) => ({
62
+ return node.invoke.map((inv) => ({
162
63
  id: inv.id ?? "(unknown)",
163
64
  src: typeof inv.src === "string" ? inv.src : inv.src?.id ?? inv.src?.name ?? "(inline)"
164
65
  }));
165
66
  }
166
- function serializeNode(node, state) {
167
- if (!node || typeof node !== "object") {
168
- return {
169
- id: "(unknown)",
170
- key: "(unknown)",
171
- type: "atomic",
172
- states: {},
173
- on: [],
174
- always: [],
175
- entry: [],
176
- exit: [],
177
- invoke: []
178
- };
179
- }
180
- if (state.count >= MAX_SERIALIZED_NODES) {
181
- return {
182
- id: node.id ?? "(truncated)",
183
- key: node.key ?? "(truncated)",
184
- type: node.type ?? "atomic",
185
- states: {},
186
- on: [],
187
- always: [],
188
- entry: [],
189
- exit: [],
190
- invoke: []
191
- };
192
- }
193
- if (state.seen.has(node)) {
194
- return {
195
- id: node.id ?? "(circular)",
196
- key: node.key ?? "(circular)",
197
- type: node.type ?? "atomic",
198
- states: {},
199
- on: [],
200
- always: [],
201
- entry: [],
202
- exit: [],
203
- invoke: []
204
- };
205
- }
206
- state.seen.add(node);
207
- state.count += 1;
67
+ function serializeNode(node) {
208
68
  const allTransitions = [];
209
69
  if (node.transitions instanceof Map) {
210
70
  for (const [, tList] of node.transitions) {
211
- if (allTransitions.length >= MAX_TRANSITIONS_PER_NODE) break;
212
71
  allTransitions.push(...serializeTransitionList(tList));
213
- if (allTransitions.length >= MAX_TRANSITIONS_PER_NODE) {
214
- allTransitions.length = MAX_TRANSITIONS_PER_NODE;
215
- break;
216
- }
217
72
  }
218
73
  }
219
74
  const always = Array.isArray(node.always) ? serializeTransitionList(node.always) : [];
220
- const childEntries = Object.entries(node.states ?? {}).slice(0, MAX_CHILD_STATES);
221
75
  return {
222
76
  id: node.id,
223
77
  key: node.key,
224
78
  type: node.type,
225
79
  initial: node.initial?.target?.[0]?.key,
226
- states: Object.fromEntries(childEntries.map(([k, v]) => [k, serializeNode(v, state)])),
80
+ states: Object.fromEntries(
81
+ Object.entries(node.states ?? {}).map(([k, v]) => [k, serializeNode(v)])
82
+ ),
227
83
  on: allTransitions,
228
84
  always,
229
- entry: (node.entry ?? []).slice(0, MAX_ENTRY_EXIT_ACTIONS).map(serializeAction).filter(Boolean),
230
- exit: (node.exit ?? []).slice(0, MAX_ENTRY_EXIT_ACTIONS).map(serializeAction).filter(Boolean),
231
- invoke: serializeInvokes(node),
232
- sourceLocation: node.config?.__xstateDevtoolsSource ?? void 0,
233
- description: node.config?.description ?? void 0
85
+ entry: (node.entry ?? []).map(serializeAction).filter(Boolean),
86
+ exit: (node.exit ?? []).map(serializeAction).filter(Boolean),
87
+ invoke: serializeInvokes(node)
234
88
  };
235
89
  }
236
90
  function serializeMachine(machine, sourceLocation) {
237
91
  return {
238
92
  id: machine.id,
239
- root: serializeNode(machine.root, { seen: /* @__PURE__ */ new WeakSet(), count: 0 }),
93
+ root: serializeNode(machine.root),
240
94
  sourceLocation
241
95
  };
242
96
  }
243
97
 
244
- // src/core.ts
245
- function summarizeMessage(message) {
246
- const summary = { type: message.type };
247
- if ("sessionId" in message) summary.sessionId = message.sessionId;
248
- if ("stateNodeId" in message) summary.stateNodeId = message.stateNodeId;
249
- if ("parentSessionId" in message && message.parentSessionId) {
250
- summary.parentSessionId = message.parentSessionId;
98
+ // src/sanitize.ts
99
+ var MAX_DEPTH = 10;
100
+ var MAX_STRING_LENGTH = 500;
101
+ var MAX_ARRAY_LENGTH = 100;
102
+ var MAX_NODES = 1e4;
103
+ function sanitizeInner(value, ctx) {
104
+ if (ctx.depth > MAX_DEPTH) return "[MaxDepth]";
105
+ if (++ctx.budget.n > MAX_NODES) return "[Truncated]";
106
+ if (value === null || value === void 0) return value;
107
+ if (typeof value === "boolean" || typeof value === "number") return value;
108
+ if (typeof value === "string") {
109
+ return value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) + "\u2026" : value;
251
110
  }
252
- if ("globalSeq" in message) summary.globalSeq = message.globalSeq;
253
- if ("timestamp" in message) summary.timestamp = message.timestamp;
254
- if ("event" in message && message.event && typeof message.event === "object" && "type" in message.event) {
255
- summary.eventType = message.event.type;
111
+ if (typeof value === "function") return `[Function: ${value.name || "(anonymous)"}]`;
112
+ if (typeof value === "symbol") return `[Symbol: ${value.description ?? ""}]`;
113
+ if (typeof value === "bigint") return `[BigInt: ${value}]`;
114
+ if (value instanceof Error) return { __type: "Error", name: value.name, message: value.message };
115
+ if (value instanceof Date) return { __type: "Date", iso: value.toISOString() };
116
+ if (value instanceof RegExp) return { __type: "RegExp", source: value.source, flags: value.flags };
117
+ if (value instanceof Promise) return "[Promise]";
118
+ if (value instanceof WeakMap || value instanceof WeakSet) return "[WeakCollection]";
119
+ if (ArrayBuffer.isView(value)) return `[TypedArray: ${value.constructor.name}]`;
120
+ if (typeof Node !== "undefined" && value instanceof Node) {
121
+ return `[DOMNode: ${value.tagName ?? value.nodeName}]`;
256
122
  }
257
- return summary;
258
- }
259
- function summarizeInspectionEvent(event) {
260
- return {
261
- type: event?.type,
262
- sessionId: event?.actorRef?.sessionId,
263
- eventType: event?.type === "@xstate.event" && event?.event && typeof event.event === "object" ? event.event.type : void 0
264
- };
265
- }
266
- function debugLog2(source, message, details) {
267
- debugLog(`${source}:adapter`, message, details);
268
- }
269
- function infoLog2(source, message, details) {
270
- infoLog(`${source}:adapter`, message, details);
271
- }
272
- function warnLog2(source, message, details) {
273
- warnLog(`${source}:adapter`, message, details);
274
- }
275
- function isLibraryStackFrame(line) {
276
- const normalized = line.replace(/\\/g, "/").toLowerCase();
277
- return normalized.includes("/node_modules/xstate/") || normalized.includes("/node_modules/@xstate/") || normalized.includes("/@xstate-devtools/adapter/") || normalized.includes("/packages/adapter/");
278
- }
279
- function normalizeStackFrame(line) {
280
- return line.trim().replace(/^at\s+/, "");
281
- }
282
- function extractStackFrameLocation(frame) {
283
- return frame.match(/\((.*)\)$/)?.[1] ?? frame;
284
- }
285
- function isAnonymousOrEvalLocation(location) {
286
- const normalized = location.trim().toLowerCase().replace(/^[./\\]+/, "");
287
- return normalized === "<anonymous>" || normalized === "anonymous" || normalized === "(anonymous)" || normalized === "eval" || normalized === "<eval>" || normalized === "[native code]";
288
- }
289
- function hasFilesystemBackedPath(location) {
290
- const trimmed = location.trim();
291
- if (!trimmed) return false;
292
- const match = trimmed.match(/^(.*?)(?::\d+)?(?::\d+)?$/);
293
- const rawPath = (match?.[1] ?? trimmed).trim();
294
- if (!rawPath || isAnonymousOrEvalLocation(rawPath)) return false;
295
- if (/^[a-zA-Z]:[\\/]/.test(rawPath)) return true;
296
- if (rawPath.startsWith("/") || rawPath.startsWith("./") || rawPath.startsWith("../")) return true;
297
- if (/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(rawPath)) {
298
- try {
299
- const url = new URL(rawPath);
300
- if (url.protocol === "file:") return true;
301
- return url.pathname.startsWith("/@fs/");
302
- } catch {
303
- return false;
123
+ if (ctx.seen.has(value)) return "[Circular]";
124
+ ctx.seen.add(value);
125
+ const child = { ...ctx, depth: ctx.depth + 1 };
126
+ if (value instanceof Map) {
127
+ const entries = [];
128
+ for (const [k, v] of value) {
129
+ if (entries.length >= MAX_ARRAY_LENGTH) break;
130
+ entries.push([sanitizeInner(k, child), sanitizeInner(v, child)]);
304
131
  }
132
+ return { __type: "Map", entries };
305
133
  }
306
- return false;
307
- }
308
- function remapWebUrlLocationToFsPath(location, source, options) {
309
- if (source !== "web" || !options?.webSourceRoot) return location;
310
- const match = location.trim().match(/^(.*?)(?::(\d+))?(?::(\d+))?$/);
311
- if (!match) return location;
312
- const [, rawPath, line, column] = match;
313
- if (!rawPath || !/^https?:\/\//.test(rawPath)) return location;
314
- try {
315
- const url = new URL(rawPath);
316
- const pathname = decodeURIComponent(url.pathname);
317
- if (!pathname.startsWith("/app/")) return location;
318
- const root = options.webSourceRoot.replace(/\/+$/, "");
319
- const filePath = `${root}${pathname}`;
320
- const suffix = line ? `:${line}${column ? `:${column}` : ""}` : "";
321
- return `${filePath}${suffix}`;
322
- } catch {
323
- return location;
134
+ if (value instanceof Set) {
135
+ const values = [];
136
+ for (const v of value) {
137
+ if (values.length >= MAX_ARRAY_LENGTH) break;
138
+ values.push(sanitizeInner(v, child));
139
+ }
140
+ return { __type: "Set", values };
324
141
  }
325
- }
326
- function getSourceLocationFromStack(stack, source = "web", options) {
327
- const lines = stack?.split("\n") ?? [];
328
- for (let i = 0; i < lines.length; i += 1) {
329
- if (i <= 2) continue;
330
- const line = lines[i];
331
- if (isLibraryStackFrame(line)) continue;
332
- const normalizedFrame = normalizeStackFrame(line);
333
- const location = remapWebUrlLocationToFsPath(
334
- extractStackFrameLocation(normalizedFrame),
335
- source,
336
- options
337
- );
338
- if (!hasFilesystemBackedPath(location)) continue;
339
- const wrapped = normalizedFrame.match(/\((.*)\)$/);
340
- if (wrapped) {
341
- return normalizedFrame.replace(wrapped[1], location);
142
+ if (Array.isArray(value)) {
143
+ const sliced = value.slice(0, MAX_ARRAY_LENGTH);
144
+ const result = sliced.map((v) => sanitizeInner(v, child));
145
+ if (value.length > MAX_ARRAY_LENGTH) result.push(`[\u2026${value.length - MAX_ARRAY_LENGTH} more]`);
146
+ return result;
147
+ }
148
+ if (typeof value === "object") {
149
+ const result = {};
150
+ let count = 0;
151
+ for (const [k, v] of Object.entries(value)) {
152
+ if (count++ >= MAX_ARRAY_LENGTH) {
153
+ result["\u2026"] = "[truncated]";
154
+ break;
155
+ }
156
+ result[k] = sanitizeInner(v, child);
342
157
  }
343
- return location;
158
+ return result;
344
159
  }
345
- return void 0;
160
+ return String(value);
161
+ }
162
+ function sanitize(value) {
163
+ return sanitizeInner(value, { depth: 0, budget: { n: 0 }, seen: /* @__PURE__ */ new WeakSet() });
346
164
  }
347
- function getSourceLocation(source, options) {
165
+
166
+ // src/core.ts
167
+ function getSourceLocation() {
348
168
  try {
349
- const oldLimit = Error.stackTraceLimit;
350
- Error.stackTraceLimit = 50;
351
- const stack = new Error().stack;
352
- Error.stackTraceLimit = oldLimit;
353
- return getSourceLocationFromStack(stack, source, options);
169
+ const lines = new Error().stack?.split("\n") ?? [];
170
+ const callerLine = lines.find(
171
+ (l, i) => i > 3 && !l.includes("xstate") && !l.includes("adapter")
172
+ );
173
+ return callerLine?.trim().replace(/^at\s+/, "");
354
174
  } catch {
355
175
  return void 0;
356
176
  }
357
177
  }
358
178
  function serializeSnapshot(snapshot) {
359
179
  return {
360
- value: snapshot?.value ?? null,
180
+ value: sanitize(snapshot?.value ?? null),
361
181
  context: sanitize(snapshot?.context),
362
182
  status: snapshot?.status ?? "active",
363
183
  error: snapshot?.error ? sanitize(snapshot.error) : void 0
@@ -370,125 +190,18 @@ function safeSerializeSnapshot(actorRef) {
370
190
  return { value: null, context: void 0, status: "active" };
371
191
  }
372
192
  }
373
- function getActorDisplayName(actorRef) {
374
- const actor = actorRef;
375
- const src = actor.src ?? actor.logic?.src;
376
- if (typeof src === "string" && src.length > 0) return src;
377
- if (typeof actor.logic?.id === "string" && actor.logic.id.length > 0) return actor.logic.id;
378
- if (typeof actor.logic?.name === "string" && actor.logic.name.length > 0) return actor.logic.name;
379
- if (src && typeof src === "object") {
380
- const namedSrc = src;
381
- if (typeof namedSrc.id === "string" && namedSrc.id.length > 0) return namedSrc.id;
382
- if (typeof namedSrc.name === "string" && namedSrc.name.length > 0) return namedSrc.name;
383
- }
384
- return void 0;
385
- }
386
- function getNodeInitialChild(node) {
387
- if (!node.states) return null;
388
- if (typeof node.initial === "string") {
389
- return node.states[node.initial] ?? null;
193
+ function safePersistedSnapshot(actorRef) {
194
+ const getPersisted = actorRef.getPersistedSnapshot;
195
+ if (typeof getPersisted !== "function") {
196
+ return { error: "Actor does not support getPersistedSnapshot." };
390
197
  }
391
- const target = Array.isArray(node.initial?.target) ? node.initial.target[0] : null;
392
- return target ?? null;
393
- }
394
- function encodeChildValue(child, childValue) {
395
- if (child.type === "atomic" || child.type === "final" || child.type === "history") {
396
- return child.key;
397
- }
398
- return { [child.key]: childValue };
399
- }
400
- function getDefaultStateValue(node) {
401
- if (node.type === "parallel") {
402
- const value = {};
403
- for (const child of Object.values(node.states ?? {})) {
404
- value[child.key] = getDefaultSelectionValue(child);
405
- }
406
- return value;
407
- }
408
- const initialChild = getNodeInitialChild(node);
409
- if (!initialChild) return {};
410
- return encodeChildValue(initialChild, getDefaultStateValue(initialChild));
411
- }
412
- function getDefaultSelectionValue(node) {
413
- if (node.type === "atomic" || node.type === "final" || node.type === "history") {
414
- return node.key;
415
- }
416
- return getDefaultStateValue(node);
417
- }
418
- function getExistingChildValue(value, childKey) {
419
- if (!value || typeof value !== "object") return void 0;
420
- return value[childKey];
421
- }
422
- function getPathToRoot(target, root) {
423
- const path = [];
424
- let current = target;
425
- while (current) {
426
- path.unshift(current);
427
- if (current.id === root.id) return path;
428
- current = current.parent;
429
- }
430
- throw new Error(`State node '${target.id}' is not part of machine '${root.id}'`);
431
- }
432
- function buildTargetStateValue(node, path, currentValue) {
433
- const [, ...restPath] = path;
434
- if (restPath.length === 0) {
435
- if (node.type === "parallel") {
436
- const next = {};
437
- for (const child2 of Object.values(node.states ?? {})) {
438
- next[child2.key] = getExistingChildValue(currentValue, child2.key) ?? getDefaultSelectionValue(child2);
439
- }
440
- return next;
441
- }
442
- if (node.type === "compound") {
443
- return getDefaultStateValue(node);
444
- }
445
- return node.key;
446
- }
447
- const child = restPath[0];
448
- if (node.type === "parallel") {
449
- const next = {};
450
- for (const sibling of Object.values(node.states ?? {})) {
451
- if (sibling.key === child.key) {
452
- next[sibling.key] = buildTargetStateValue(
453
- sibling,
454
- restPath,
455
- getExistingChildValue(currentValue, sibling.key)
456
- );
457
- } else {
458
- next[sibling.key] = getExistingChildValue(currentValue, sibling.key) ?? getDefaultSelectionValue(sibling);
459
- }
460
- }
461
- return next;
462
- }
463
- const childValue = buildTargetStateValue(
464
- child,
465
- restPath,
466
- getExistingChildValue(currentValue, child.key)
467
- );
468
- return encodeChildValue(child, childValue);
469
- }
470
- function setActiveState(actorRef, stateNodeId) {
471
- const mutableActorRef = actorRef;
472
- const machine = mutableActorRef.logic;
473
- if (!machine?.getStateNodeById || !machine.resolveState || typeof mutableActorRef.update !== "function") {
474
- throw new Error("Actor does not expose machine state mutation internals");
198
+ try {
199
+ const raw = getPersisted.call(actorRef);
200
+ if (raw === void 0) return { error: "No persisted snapshot available." };
201
+ return { persisted: JSON.parse(JSON.stringify(raw)) };
202
+ } catch (e) {
203
+ return { error: `Snapshot is not JSON-serializable: ${e.message}` };
475
204
  }
476
- const currentSnapshot = actorRef.getSnapshot();
477
- const targetNode = machine.getStateNodeById(stateNodeId);
478
- const path = getPathToRoot(targetNode, machine.root);
479
- const targetValue = buildTargetStateValue(machine.root, path, currentSnapshot?.value);
480
- const nextSnapshot = machine.resolveState({
481
- value: targetValue,
482
- context: currentSnapshot?.context,
483
- status: currentSnapshot?.status,
484
- output: currentSnapshot?.output,
485
- error: currentSnapshot?.error,
486
- historyValue: currentSnapshot?.historyValue
487
- });
488
- mutableActorRef.update(nextSnapshot, {
489
- type: "xstate.devtools.set-active-state",
490
- stateNodeId
491
- });
492
205
  }
493
206
  var SEQ_KEY = "__xstate_devtools_global_seq__";
494
207
  function nextSeq() {
@@ -498,10 +211,10 @@ function nextSeq() {
498
211
  g[SEQ_KEY] = next;
499
212
  return next;
500
213
  }
501
- function createInspector(transport, source, options = {}) {
214
+ function createInspector(transport, source) {
502
215
  const actorRefs = /* @__PURE__ */ new Map();
503
- const actorMachines = /* @__PURE__ */ new Map();
504
- const prefix = `${source}:`;
216
+ const restoreHandlers = /* @__PURE__ */ new Map();
217
+ const prefix = source + ":";
505
218
  const tag = (sessionId) => prefix + sessionId;
506
219
  const tagOptional = (id) => id ? prefix + id : void 0;
507
220
  const stripIfMine = (id) => id.startsWith(prefix) ? id.slice(prefix.length) : null;
@@ -513,278 +226,200 @@ function createInspector(transport, source, options = {}) {
513
226
  return;
514
227
  }
515
228
  if (snap?.status !== "active") {
516
- const message = {
517
- type: "XSTATE_ACTOR_STOPPED",
518
- sessionId: tag(actorRef.sessionId)
519
- };
520
- debugLog2(source, "actor stopped; notifying transport", summarizeMessage(message));
521
- transport.send(message);
229
+ transport.send({ type: "XSTATE_ACTOR_STOPPED", sessionId: tag(actorRef.sessionId) });
522
230
  actorRefs.delete(actorRef.sessionId);
523
- actorMachines.delete(actorRef.sessionId);
524
231
  }
525
232
  }
526
233
  const unsubscribe = transport.subscribe((message) => {
527
- debugLog2(source, "received message from transport", summarizeMessage(message));
528
- if (message.type === "XSTATE_PANEL_CONNECTED") {
529
- infoLog2(source, "panel connected; resyncing actors", { actorCount: actorRefs.size });
530
- actorRefs.forEach((actorRef, sessionId) => {
531
- const machine = actorMachines.get(sessionId) ?? null;
532
- const resyncMessage = {
533
- type: "XSTATE_ACTOR_REGISTERED",
534
- sessionId: tag(sessionId),
535
- parentSessionId: tagOptional(actorRef._parent?.sessionId),
536
- displayName: getActorDisplayName(actorRef),
537
- machine,
538
- snapshot: safeSerializeSnapshot(actorRef),
539
- globalSeq: nextSeq(),
540
- timestamp: Date.now()
541
- };
542
- debugLog2(source, "resyncing actor", summarizeMessage(resyncMessage));
543
- transport.send(resyncMessage);
544
- });
545
- return;
546
- }
547
234
  if (message.type === "XSTATE_DISPATCH") {
548
235
  const local = stripIfMine(message.sessionId);
549
- if (local === null) {
550
- debugLog2(source, "ignoring dispatch for different source", summarizeMessage(message));
551
- return;
552
- }
236
+ if (local === null) return;
553
237
  const ref = actorRefs.get(local);
554
238
  if (ref) {
555
239
  try {
556
- debugLog2(source, "dispatching event to actor", {
557
- sessionId: local,
558
- eventType: message.event && typeof message.event === "object" && "type" in message.event ? message.event.type : void 0
559
- });
560
240
  ref.send(message.event);
561
241
  } catch (e) {
562
- warnLog2(source, "dispatch error", { error: e, sessionId: local });
242
+ console.warn("[xstate-devtools] dispatch error:", e);
563
243
  }
564
- } else {
565
- warnLog2(source, "received dispatch for unknown actor", {
566
- sessionId: local,
567
- knownActors: actorRefs.size
568
- });
569
244
  }
570
- return;
571
- }
572
- if (message.type === "XSTATE_SET_ACTIVE_STATE") {
245
+ } else if (message.type === "XSTATE_REQUEST_PERSISTED") {
573
246
  const local = stripIfMine(message.sessionId);
574
- if (local === null) {
575
- debugLog2(
576
- source,
577
- "ignoring state activation for different source",
578
- summarizeMessage(message)
579
- );
580
- return;
581
- }
247
+ if (local === null) return;
582
248
  const ref = actorRefs.get(local);
583
- if (!ref) {
584
- warnLog2(source, "received state activation for unknown actor", {
585
- sessionId: local,
586
- knownActors: actorRefs.size
587
- });
588
- return;
249
+ if (!ref) return;
250
+ const { persisted, error } = safePersistedSnapshot(ref);
251
+ transport.send({
252
+ type: "XSTATE_PERSISTED_SNAPSHOT",
253
+ sessionId: tag(local),
254
+ persisted,
255
+ error,
256
+ timestamp: Date.now()
257
+ });
258
+ } else if (message.type === "XSTATE_RESTORE") {
259
+ const local = stripIfMine(message.sessionId);
260
+ if (local === null) return;
261
+ const handler = restoreHandlers.get(local);
262
+ if (handler) {
263
+ try {
264
+ handler(message.persisted);
265
+ } catch (e) {
266
+ console.warn("[xstate-devtools] restore error:", e);
267
+ }
589
268
  }
590
- try {
591
- debugLog2(source, "setting active state on actor", summarizeMessage(message));
592
- setActiveState(ref, message.stateNodeId);
593
- const snapshotMessage = {
269
+ }
270
+ });
271
+ const inspect = (inspectionEvent) => {
272
+ try {
273
+ if (inspectionEvent.type === "@xstate.actor") {
274
+ const actorRef = inspectionEvent.actorRef;
275
+ const sessionId = actorRef.sessionId;
276
+ actorRefs.set(sessionId, actorRef);
277
+ const logic = actorRef.logic;
278
+ const machine = logic?.root ? serializeMachine(logic, getSourceLocation()) : null;
279
+ transport.send({
280
+ type: "XSTATE_ACTOR_REGISTERED",
281
+ sessionId: tag(sessionId),
282
+ parentSessionId: tagOptional(actorRef._parent?.sessionId),
283
+ // actorRef.id is the invoke `id` for invoked actors — lets the debugger
284
+ // nest non-machine actors (promise/callback) under their state.
285
+ actorId: actorRef.id,
286
+ machine,
287
+ snapshot: safeSerializeSnapshot(actorRef),
288
+ globalSeq: nextSeq(),
289
+ timestamp: Date.now()
290
+ });
291
+ } else if (inspectionEvent.type === "@xstate.snapshot") {
292
+ transport.send({
594
293
  type: "XSTATE_SNAPSHOT",
595
- sessionId: tag(local),
596
- snapshot: safeSerializeSnapshot(ref),
294
+ sessionId: tag(inspectionEvent.actorRef.sessionId),
295
+ snapshot: serializeSnapshot(inspectionEvent.snapshot),
597
296
  timestamp: Date.now(),
598
297
  globalSeq: nextSeq()
599
- };
600
- debugLog2(
601
- source,
602
- "sending snapshot after state activation",
603
- summarizeMessage(snapshotMessage)
604
- );
605
- transport.send(snapshotMessage);
606
- } catch (error) {
607
- warnLog2(source, "failed to set active state", {
608
- error,
609
- sessionId: local,
610
- stateNodeId: message.stateNodeId
611
298
  });
299
+ checkAndNotifyStop(inspectionEvent.actorRef);
300
+ } else if (inspectionEvent.type === "@xstate.event") {
301
+ transport.send({
302
+ type: "XSTATE_EVENT",
303
+ sessionId: tag(inspectionEvent.actorRef.sessionId),
304
+ event: inspectionEvent.event,
305
+ snapshotAfter: safeSerializeSnapshot(inspectionEvent.actorRef),
306
+ timestamp: Date.now(),
307
+ globalSeq: nextSeq()
308
+ });
309
+ checkAndNotifyStop(inspectionEvent.actorRef);
612
310
  }
613
- }
614
- });
615
- transport.send({ type: "XSTATE_ADAPTER_READY" });
616
- infoLog2(source, "inspector created");
617
- const inspect = (inspectionEvent) => {
618
- debugLog2(source, "inspect callback invoked", summarizeInspectionEvent(inspectionEvent));
619
- if (inspectionEvent.type === "@xstate.actor") {
620
- const actorRef = inspectionEvent.actorRef;
621
- const sessionId = actorRef.sessionId;
622
- const actorLogic = actorRef.logic;
623
- const machine = actorLogic?.root ? serializeMachine(
624
- actorLogic,
625
- actorLogic.config?.__xstateDevtoolsSource ?? getSourceLocation(source, options)
626
- ) : null;
627
- actorRefs.set(sessionId, actorRef);
628
- actorMachines.set(sessionId, machine);
629
- const notifyStop = () => {
630
- if (actorRefs.has(sessionId)) {
631
- actorRefs.delete(sessionId);
632
- actorMachines.delete(sessionId);
633
- const stopMsg = {
634
- type: "XSTATE_ACTOR_STOPPED",
635
- sessionId: tag(sessionId)
636
- };
637
- debugLog2(source, "actor stopped; notifying transport", summarizeMessage(stopMsg));
638
- transport.send(stopMsg);
639
- }
640
- };
641
- try {
642
- actorRef.subscribe({ complete: notifyStop, error: notifyStop });
643
- } catch {
644
- }
645
- const message = {
646
- type: "XSTATE_ACTOR_REGISTERED",
647
- sessionId: tag(sessionId),
648
- parentSessionId: tagOptional(actorRef._parent?.sessionId),
649
- displayName: getActorDisplayName(actorRef),
650
- machine,
651
- snapshot: safeSerializeSnapshot(actorRef),
652
- globalSeq: nextSeq(),
653
- timestamp: Date.now()
654
- };
655
- infoLog2(source, "registering actor with transport", {
656
- message: summarizeMessage(message),
657
- actorCount: actorRefs.size,
658
- hasMachine: machine !== null
659
- });
660
- transport.send(message);
661
- } else if (inspectionEvent.type === "@xstate.snapshot") {
662
- const message = {
663
- type: "XSTATE_SNAPSHOT",
664
- sessionId: tag(inspectionEvent.actorRef.sessionId),
665
- snapshot: serializeSnapshot(inspectionEvent.snapshot),
666
- timestamp: Date.now(),
667
- globalSeq: nextSeq()
668
- };
669
- debugLog2(source, "sending snapshot to transport", summarizeMessage(message));
670
- transport.send(message);
671
- checkAndNotifyStop(inspectionEvent.actorRef);
672
- } else if (inspectionEvent.type === "@xstate.event") {
673
- const message = {
674
- type: "XSTATE_EVENT",
675
- sessionId: tag(inspectionEvent.actorRef.sessionId),
676
- // sanitize() returns unknown; the inspected event keeps its { type, ... }
677
- // shape, so it is a SerializedEvent after deep-sanitizing.
678
- event: sanitize(inspectionEvent.event),
679
- snapshotAfter: safeSerializeSnapshot(inspectionEvent.actorRef),
680
- timestamp: Date.now(),
681
- globalSeq: nextSeq()
682
- };
683
- debugLog2(source, "sending event to transport", summarizeMessage(message));
684
- transport.send(message);
685
- checkAndNotifyStop(inspectionEvent.actorRef);
686
- } else {
687
- debugLog2(
688
- source,
689
- "ignoring unsupported inspection event",
690
- summarizeInspectionEvent(inspectionEvent)
691
- );
311
+ } catch (e) {
312
+ console.warn("[xstate-devtools] inspection failed, dropping event:", e.message);
692
313
  }
693
314
  };
315
+ function registerRestore(sessionId, handler) {
316
+ restoreHandlers.set(sessionId, handler);
317
+ return () => {
318
+ if (restoreHandlers.get(sessionId) === handler) restoreHandlers.delete(sessionId);
319
+ };
320
+ }
694
321
  function dispose() {
695
- infoLog2(source, "disposing inspector", { actorCount: actorRefs.size });
696
322
  unsubscribe();
697
323
  actorRefs.clear();
698
- actorMachines.clear();
324
+ restoreHandlers.clear();
699
325
  }
700
- return { inspect, dispose };
326
+ return { inspect, dispose, registerRestore };
701
327
  }
702
328
 
703
329
  // src/index.ts
704
- function createAdapter(options = {}) {
330
+ function createAdapter() {
705
331
  if (typeof window === "undefined") {
706
- infoLog("web:adapter", "createAdapter called without window; returning no-op adapter");
707
332
  return { inspect: () => {
708
333
  }, dispose: () => {
334
+ }, registerRestore: () => () => {
709
335
  } };
710
336
  }
711
- infoLog("web:adapter", "creating browser adapter", {
712
- hookInstalled: Boolean(window.__XSTATE_DEVTOOLS__)
713
- });
714
- let warnedMissingHook = false;
715
337
  const transport = {
716
338
  send(message) {
717
- const payload = { ...message, __xstateDevtools: true };
718
- debugLog("web:adapter", "sending message via page hook", {
719
- type: message.type,
720
- sessionId: "sessionId" in message ? message.sessionId : void 0
721
- });
722
- if (window.__XSTATE_DEVTOOLS__) {
723
- window.__XSTATE_DEVTOOLS__.send(payload);
724
- return;
725
- }
726
- if (!warnedMissingHook) {
727
- warnedMissingHook = true;
728
- warnLog("web:adapter", "page hook missing; using direct window.postMessage fallback");
729
- }
730
- window.postMessage(payload, "*");
339
+ window.__XSTATE_DEVTOOLS__?.send({ ...message, __xstateDevtools: true });
731
340
  },
732
341
  subscribe(handler) {
733
- infoLog("web:adapter", "subscribing to window messages");
734
342
  const onMessage = (evt) => {
735
343
  if (evt.source !== window) return;
736
344
  const data = evt.data;
737
345
  if (!data?.__xstateDevtools) return;
738
- debugLog("web:adapter", "received message from window bridge", {
739
- type: data.type,
740
- sessionId: "sessionId" in data ? data.sessionId : void 0
741
- });
742
346
  handler(data);
743
347
  };
744
348
  window.addEventListener("message", onMessage);
745
- return () => {
746
- infoLog("web:adapter", "unsubscribing from window messages");
747
- window.removeEventListener("message", onMessage);
748
- };
349
+ return () => window.removeEventListener("message", onMessage);
749
350
  }
750
351
  };
751
- return createInspector(transport, "web", options);
352
+ return createInspector(transport, "web");
752
353
  }
753
354
 
754
355
  // src/react.tsx
755
356
  var import_jsx_runtime = require("react/jsx-runtime");
756
- var InspectorContext = (0, import_react2.createContext)(null);
757
- function InspectorProvider({ children }) {
758
- const adapterRef = (0, import_react2.useRef)(null);
759
- if (!adapterRef.current && typeof window !== "undefined") {
760
- adapterRef.current = createAdapter();
761
- }
762
- (0, import_react2.useEffect)(() => {
357
+ var InspectorContext = (0, import_react.createContext)(null);
358
+ function InspectorProvider({
359
+ children,
360
+ adapter
361
+ }) {
362
+ const ownRef = (0, import_react.useRef)(null);
363
+ const useOwn = adapter == null;
364
+ if (useOwn && !ownRef.current && typeof window !== "undefined") {
365
+ ownRef.current = createAdapter();
366
+ }
367
+ const resolved = adapter ?? ownRef.current;
368
+ (0, import_react.useEffect)(() => {
369
+ if (!useOwn) return;
763
370
  return () => {
764
- adapterRef.current?.dispose();
765
- adapterRef.current = null;
371
+ ownRef.current?.dispose();
372
+ ownRef.current = null;
766
373
  };
767
- }, []);
768
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InspectorContext.Provider, { value: adapterRef.current, children });
374
+ }, [useOwn]);
375
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InspectorContext.Provider, { value: resolved, children });
769
376
  }
770
377
  function useInspectedMachine(machine, options) {
771
- const adapter = (0, import_react2.useContext)(InspectorContext);
772
- return (0, import_react.useMachine)(machine, {
378
+ const adapter = (0, import_react.useContext)(InspectorContext);
379
+ return (0, import_react2.useMachine)(machine, {
773
380
  ...options,
774
381
  inspect: adapter?.inspect
775
382
  });
776
383
  }
777
384
  function useInspectedActorRef(machine, options) {
778
- const adapter = (0, import_react2.useContext)(InspectorContext);
779
- return (0, import_react.useActorRef)(machine, {
385
+ const adapter = (0, import_react.useContext)(InspectorContext);
386
+ return (0, import_react2.useActorRef)(machine, {
780
387
  ...options,
781
388
  inspect: adapter?.inspect
782
389
  });
783
390
  }
391
+ function useRestorableInspectedMachine(machine, options) {
392
+ const adapter = (0, import_react.useContext)(InspectorContext);
393
+ const restoreSnapshotRef = (0, import_react.useRef)(options?.snapshot);
394
+ const [generation, setGeneration] = (0, import_react.useState)(0);
395
+ const actorRef = (0, import_react.useMemo)(() => {
396
+ return (0, import_xstate.createActor)(machine, {
397
+ ...options,
398
+ snapshot: restoreSnapshotRef.current,
399
+ inspect: adapter?.inspect
400
+ });
401
+ }, [generation, adapter]);
402
+ (0, import_react.useEffect)(() => {
403
+ actorRef.start();
404
+ return () => {
405
+ actorRef.stop();
406
+ };
407
+ }, [actorRef]);
408
+ (0, import_react.useEffect)(() => {
409
+ if (!adapter?.registerRestore) return;
410
+ return adapter.registerRestore(actorRef.sessionId, (persisted) => {
411
+ restoreSnapshotRef.current = persisted;
412
+ setGeneration((g) => g + 1);
413
+ });
414
+ }, [adapter, actorRef]);
415
+ const snapshot = (0, import_react2.useSelector)(actorRef, (s) => s);
416
+ return [snapshot, actorRef.send, actorRef];
417
+ }
784
418
  // Annotate the CommonJS export names for ESM import in node:
785
419
  0 && (module.exports = {
786
420
  InspectorProvider,
787
421
  useInspectedActorRef,
788
- useInspectedMachine
422
+ useInspectedMachine,
423
+ useRestorableInspectedMachine
789
424
  });
790
425
  //# sourceMappingURL=react.cjs.map