@mantaq/traversal 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,281 +1,463 @@
1
- import { Context } from "@mantaq/core";
1
+ import { Context, isStateRef } from "@mantaq/core";
2
2
  import { Either } from "@mantaq/utils";
3
- //#region src/graph.ts
3
+ // src/history.ts
4
+ function entriesOfType(recordedEntries, options) {
5
+ return recordedEntries.filter((entry) => entry.type === options.type).map((entry) => entry.data);
6
+ }
7
+ /**
8
+ * History records the runtime trace of an instrumented actor. A closure over
9
+ * the entry list keeps the recorder mutable without exposing the raw array.
10
+ */
11
+ function createHistory() {
12
+ const recordedEntries = [];
13
+ function append(entry) {
14
+ recordedEntries.push(entry);
15
+ }
16
+ function entries() {
17
+ return recordedEntries;
18
+ }
19
+ function stateVisits() {
20
+ return entriesOfType(recordedEntries, { type: "state_visit" });
21
+ }
22
+ function transitions() {
23
+ return entriesOfType(recordedEntries, { type: "transition" });
24
+ }
25
+ function effects() {
26
+ return entriesOfType(recordedEntries, { type: "effect" });
27
+ }
28
+ function sends() {
29
+ return entriesOfType(recordedEntries, { type: "send" });
30
+ }
31
+ function visitedStates() {
32
+ const visited = /* @__PURE__ */ new Set();
33
+ for (const visit of stateVisits()) visited.add(visit.stateName);
34
+ return visited;
35
+ }
36
+ function firedTransitions() {
37
+ const fired = /* @__PURE__ */ new Set();
38
+ for (const record of transitions()) fired.add(`${record.from}:${record.event}`);
39
+ return fired;
40
+ }
41
+ function reset() {
42
+ recordedEntries.length = 0;
43
+ }
44
+ return {
45
+ append,
46
+ entries,
47
+ stateVisits,
48
+ transitions,
49
+ effects,
50
+ sends,
51
+ visitedStates,
52
+ firedTransitions,
53
+ reset
54
+ };
55
+ }
56
+ // src/graph-ids.ts
4
57
  const INITIAL_NODE_ID = "__initial__";
5
- function collectActiveStates(snapshot, prefix, activeSet) {
6
- if (!snapshot || !snapshot.path) return;
58
+ /** Source of `onAny` edges: any non-final state of the machine (`region.*` inside a region). */
59
+ const ANY_NODE_NAME = "*";
60
+ function isAnyNode(id) {
61
+ return id === "*" || id.endsWith(`.*`);
62
+ }
63
+ function collectActiveStates(snapshot, options) {
64
+ if (!snapshot.path) return;
7
65
  const currentName = snapshot.path[snapshot.path.length - 1];
8
66
  if (!currentName) return;
9
- const fullId = nodeId(prefix, currentName);
10
- activeSet.add(fullId);
11
- if (snapshot.regions && !snapshot.done && !snapshot.error) for (const [regionName, regionSnap] of Object.entries(snapshot.regions)) collectActiveStates(regionSnap, regionName, activeSet);
67
+ const fullId = nodeId({
68
+ prefix: options.prefix,
69
+ name: currentName
70
+ });
71
+ options.activeSet.add(fullId);
72
+ if (snapshot.regions && !snapshot.done && !snapshot.error) for (const [regionName, regionSnapshot] of Object.entries(snapshot.regions)) collectActiveStates(regionSnapshot, {
73
+ prefix: regionName,
74
+ activeSet: options.activeSet
75
+ });
12
76
  }
13
- function nodeId(prefix, name) {
14
- return prefix ? `${prefix}.${name}` : name;
77
+ function nodeId(identity) {
78
+ return identity.prefix ? `${identity.prefix}.${identity.name}` : identity.name;
15
79
  }
16
- function buildNodesFromStates(states, activeSet, pathPrefix) {
17
- return states.map((stateRef) => {
18
- const nid = nodeId(pathPrefix, stateRef.name);
19
- return {
20
- id: nid,
21
- label: stateRef.name,
22
- isActive: activeSet.has(nid),
23
- isFinal: stateRef.isFinal
24
- };
80
+ function collectActorsFromSnapshot(snapshot) {
81
+ const activeSet = /* @__PURE__ */ new Set();
82
+ collectActiveStates(snapshot, {
83
+ prefix: "",
84
+ activeSet
25
85
  });
86
+ return activeSet;
87
+ }
88
+ // src/is-handler-result.ts
89
+ function isHandlerResult(value) {
90
+ if (typeof value !== "object" || value === null) return false;
91
+ const candidate = value;
92
+ return (candidate.state === void 0 || typeof candidate.state === "object" && candidate.state !== null && typeof candidate.state.name === "string") && (candidate.emit === void 0 || Array.isArray(candidate.emit));
26
93
  }
27
- function invokeHandler(handler, eventId, context, actor) {
94
+ // src/graph-edges.ts
95
+ function invokeHandler(handler, options) {
28
96
  return Either.from(() => {
29
97
  const result = handler({
30
- type: eventId,
98
+ type: options.eventId,
31
99
  payload: {}
32
100
  }, {
33
- context: new Context(() => context, () => {}),
34
- actor
101
+ context: Context({
102
+ get: () => options.context,
103
+ set: () => {}
104
+ }),
105
+ actor: options.actor
35
106
  });
36
- return {
37
- targetName: result?.state?.name,
38
- emitNames: result?.emit?.map((e) => e.type).filter((type) => Boolean(type)) ?? []
107
+ return isHandlerResult(result) ? {
108
+ targetName: result.state?.name,
109
+ emitNames: result.emit?.map((emitted) => emitted.type).filter((eventType) => Boolean(eventType)) ?? []
110
+ } : {
111
+ targetName: void 0,
112
+ emitNames: []
39
113
  };
40
114
  });
41
115
  }
42
- function upsertEdge(sink, traversal, eventId, handled) {
43
- const undetermined = !handled.targetName;
44
- const targetId = handled.targetName ? nodeId(traversal.pathPrefix, handled.targetName) : `${sink.sourceId}-undetermined-${eventId}`;
45
- const edgeId = `${sink.sourceId}-${eventId}-${targetId}`;
46
- const existing = sink.edgeMap.get(edgeId);
47
- if (existing) existing.contexts.push(sink.ctxName);
48
- else sink.edgeMap.set(edgeId, {
49
- id: edgeId,
50
- source: sink.sourceId,
51
- target: targetId,
52
- label: eventId,
53
- isActive: traversal.activeSet.has(sink.sourceId),
54
- isInternal: traversal.internalIds?.has(eventId),
55
- isUndetermined: undetermined,
56
- contexts: [sink.ctxName],
57
- ...handled.emitNames.length > 0 && { payload: { action: `emit(${handled.emitNames.join(", ")})` } }
116
+ function probeAll(registration, pass) {
117
+ const probes = [];
118
+ for (const ctxName of pass.traversal.contextNames) {
119
+ const sampleContext = pass.traversal.contexts[ctxName];
120
+ if (sampleContext === void 0) continue;
121
+ const handled = Either.getRight(invokeHandler(registration.handler, {
122
+ eventId: registration.eventId,
123
+ context: sampleContext,
124
+ actor: pass.traversal.actor
125
+ }));
126
+ if (handled !== void 0) probes.push({
127
+ ctxName,
128
+ handled
129
+ });
130
+ }
131
+ return probes;
132
+ }
133
+ /**
134
+ * One edge per declared target, a `stay` edge when `to` is empty, or one
135
+ * undetermined edge when the actor carries no declaration. Probing only labels
136
+ * edges: which sample contexts reach each target, and what it emits.
137
+ */
138
+ function addRegistration(registration, pass) {
139
+ const probes = probeAll(registration, pass);
140
+ const declared = registration.declared ?? [];
141
+ const targets = declared.length > 0 ? declared : [void 0];
142
+ const undetermined = registration.declared === void 0;
143
+ for (const targetName of targets) {
144
+ const matching = undetermined ? probes : probes.filter((probe) => probe.handled.targetName === targetName);
145
+ const emitNames = [...new Set(matching.flatMap((probe) => probe.handled.emitNames))].sort();
146
+ const targetId = targetName ? nodeId({
147
+ prefix: pass.traversal.pathPrefix,
148
+ name: targetName
149
+ }) : pass.sourceId;
150
+ const suffix = targetName ? targetId : undetermined ? "undetermined" : "stay";
151
+ const edgeId = `${pass.sourceId}-${registration.eventId}-${suffix}`;
152
+ pass.edgeMap.set(edgeId, {
153
+ id: edgeId,
154
+ source: pass.sourceId,
155
+ target: targetId,
156
+ label: registration.eventId,
157
+ isActive: pass.isActive,
158
+ isInternal: pass.traversal.internalIds?.has(registration.eventId),
159
+ isUndetermined: undetermined,
160
+ isStay: !undetermined && targetName === void 0,
161
+ isDeclared: !undetermined,
162
+ contexts: matching.map((probe) => probe.ctxName),
163
+ ...emitNames.length > 0 && { payload: { action: `emit(${emitNames.join(", ")})` } }
164
+ });
165
+ }
166
+ }
167
+ function registrations(handlers, declared) {
168
+ return Object.entries(handlers ?? {}).map(([eventId, handler]) => ({
169
+ eventId,
170
+ handler,
171
+ declared: declared?.[eventId]
172
+ }));
173
+ }
174
+ function stateEdges(edges) {
175
+ const sourceId = nodeId({
176
+ prefix: edges.traversal.pathPrefix,
177
+ name: edges.state.name
58
178
  });
179
+ const pass = {
180
+ edgeMap: /* @__PURE__ */ new Map(),
181
+ sourceId,
182
+ isActive: edges.traversal.activeSet.has(sourceId),
183
+ traversal: edges.traversal
184
+ };
185
+ const own = registrations(edges.transitions[edges.state.name], edges.targets[edges.state.name]);
186
+ for (const registration of own) addRegistration(registration, pass);
187
+ return [...pass.edgeMap.values()];
188
+ }
189
+ /**
190
+ * `onAny` edges leave one `*` source, once per target: "from any non-final
191
+ * state". Expanding them per state would claim source/target pairs the code
192
+ * never states. A state's own handler for the same event runs first; when it
193
+ * stays, `onAny` may still transition, so both edges are kept.
194
+ */
195
+ function anyEdges(edges) {
196
+ const prefix = edges.traversal.pathPrefix;
197
+ const pass = {
198
+ edgeMap: /* @__PURE__ */ new Map(),
199
+ sourceId: nodeId({
200
+ prefix,
201
+ name: "*"
202
+ }),
203
+ isActive: edges.states.some((state) => !state.isFinal && edges.traversal.activeSet.has(nodeId({
204
+ prefix,
205
+ name: state.name
206
+ }))),
207
+ traversal: edges.traversal
208
+ };
209
+ for (const registration of registrations(edges.transitions["Any"], edges.targets["Any"])) addRegistration(registration, pass);
210
+ return [...pass.edgeMap.values()];
211
+ }
212
+ function buildEdgesFromTransitions(edges) {
213
+ const scope = {
214
+ transitions: edges.transitions,
215
+ targets: edges.targets ?? {},
216
+ traversal: edges.traversal
217
+ };
218
+ return [...edges.states.flatMap((state) => stateEdges({
219
+ ...scope,
220
+ state
221
+ })), ...anyEdges({
222
+ ...scope,
223
+ states: edges.states
224
+ })];
59
225
  }
60
- function processStateTransitions(stateTransitions, pass) {
61
- const stateTransitionedEvents = /* @__PURE__ */ new Set();
62
- for (const [eventId, handler] of Object.entries(stateTransitions)) {
63
- if (typeof handler !== "function") continue;
64
- for (const ctxName of pass.traversal.contextNames) {
65
- const sampleContext = pass.traversal.contexts[ctxName];
66
- const handled = Either.getOrElse(invokeHandler(handler, eventId, sampleContext, pass.traversal.actor), () => ({
67
- targetName: void 0,
68
- emitNames: []
69
- }));
70
- if (handled.targetName) stateTransitionedEvents.add(eventId);
71
- upsertEdge({
72
- edgeMap: pass.edgeMap,
73
- sourceId: pass.sourceId,
74
- ctxName
75
- }, pass.traversal, eventId, handled);
226
+ // src/parse-graph.ts
227
+ function parseStates(options) {
228
+ const states = options?.states;
229
+ if (!Array.isArray(states)) return [];
230
+ return states.filter((stateEntry) => typeof stateEntry === "object" && stateEntry !== null && typeof stateEntry.name === "string" && typeof stateEntry.isFinal === "boolean");
231
+ }
232
+ function parseTransitionMap(source) {
233
+ const dispatchMap = {};
234
+ if (typeof source !== "object" || source === null) return dispatchMap;
235
+ for (const [stateName, eventMap] of Object.entries(source)) {
236
+ if (typeof eventMap !== "object" || eventMap === null) continue;
237
+ for (const [eventType, handler] of Object.entries(eventMap)) {
238
+ if (typeof handler !== "function") continue;
239
+ (dispatchMap[stateName] ??= {})[eventType] = handler;
76
240
  }
77
241
  }
78
- return stateTransitionedEvents;
79
- }
80
- function mergeAnyTransitions(anyTransitions, stateTransitionedEvents, pass) {
81
- for (const [eventId, handler] of Object.entries(anyTransitions)) {
82
- if (typeof handler !== "function") continue;
83
- if (stateTransitionedEvents.has(eventId)) continue;
84
- for (const ctxName of pass.traversal.contextNames) {
85
- const sampleContext = pass.traversal.contexts[ctxName];
86
- const handled = Either.getOrElse(invokeHandler(handler, eventId, sampleContext, pass.traversal.actor), () => ({
87
- targetName: void 0,
88
- emitNames: []
89
- }));
90
- upsertEdge({
91
- edgeMap: pass.edgeMap,
92
- sourceId: pass.sourceId,
93
- ctxName
94
- }, pass.traversal, eventId, handled);
242
+ return dispatchMap;
243
+ }
244
+ function parseTargetMap(source) {
245
+ const targetMap = {};
246
+ if (typeof source !== "object" || source === null) return targetMap;
247
+ for (const [stateName, eventMap] of Object.entries(source)) {
248
+ if (typeof eventMap !== "object" || eventMap === null) continue;
249
+ for (const [eventType, names] of Object.entries(eventMap)) {
250
+ if (!Array.isArray(names)) continue;
251
+ const declared = names.filter((name) => typeof name === "string");
252
+ (targetMap[stateName] ??= {})[eventType] = declared;
95
253
  }
96
254
  }
255
+ return targetMap;
97
256
  }
98
- function collectTransitionsForState(stateTransitions, anyTransitions, sourceId, traversal) {
99
- const edgeMap = /* @__PURE__ */ new Map();
100
- const pass = {
101
- edgeMap,
102
- sourceId,
103
- traversal
104
- };
105
- const stateTransitionedEvents = stateTransitions ? processStateTransitions(stateTransitions, pass) : /* @__PURE__ */ new Set();
106
- if (anyTransitions) mergeAnyTransitions(anyTransitions, stateTransitionedEvents, pass);
107
- return edgeMap;
108
- }
109
- function buildEdgesFromTransitions(states, transitions, traversal) {
110
- if (!transitions) return [];
111
- const anyTransitions = transitions["Any"];
112
- const edges = [];
113
- for (const stateRef of states) {
114
- const sourceId = nodeId(traversal.pathPrefix, stateRef.name);
115
- const edgeMap = collectTransitionsForState(transitions[stateRef.name], anyTransitions, sourceId, traversal);
116
- edges.push(...edgeMap.values());
257
+ function parseInitialName(options) {
258
+ const initial = options?.initial;
259
+ if (initial === void 0 || initial === null) return void 0;
260
+ if (isStateRef(initial)) return initial.name;
261
+ const wrapped = initial;
262
+ return isStateRef(wrapped.state) ? wrapped.state.name : void 0;
263
+ }
264
+ function parseContextRecord(context) {
265
+ if (context === void 0 || context === null) return {};
266
+ return { ...context };
267
+ }
268
+ function parseInternalEventIds(options) {
269
+ const internal = options?.internal;
270
+ const eventIds = /* @__PURE__ */ new Set();
271
+ if (!Array.isArray(internal)) return eventIds;
272
+ for (const entry of internal) {
273
+ if (typeof entry !== "object" || entry === null || !("type" in entry)) continue;
274
+ const eventType = entry.type;
275
+ if (typeof eventType === "string") eventIds.add(eventType);
117
276
  }
118
- return edges;
277
+ return eventIds;
119
278
  }
120
- function addNodesForActor(actor, pathPrefix, activeSet) {
121
- return buildNodesFromStates(actor.options?.states ?? [], activeSet, pathPrefix);
279
+ // src/graph.ts
280
+ function buildNodesFromStates(states, options) {
281
+ return states.map((stateEntry) => {
282
+ const graphId = nodeId({
283
+ prefix: options.pathPrefix,
284
+ name: stateEntry.name
285
+ });
286
+ return {
287
+ id: graphId,
288
+ label: stateEntry.name,
289
+ isActive: options.activeSet.has(graphId),
290
+ isFinal: stateEntry.isFinal
291
+ };
292
+ });
122
293
  }
123
- function addEdgesForActor(actor, traversal) {
124
- return buildEdgesFromTransitions(actor.options?.states ?? [], actor.options?.transitions, traversal);
294
+ function addNodesForActor(actor, layout) {
295
+ const nodes = buildNodesFromStates(parseStates(actor.options), {
296
+ activeSet: layout.activeSet,
297
+ pathPrefix: layout.pathPrefix
298
+ });
299
+ const anyTransitions = parseTransitionMap(actor.options?.transitions)["Any"];
300
+ if (anyTransitions !== void 0 && Object.keys(anyTransitions).length > 0) {
301
+ const id = nodeId({
302
+ prefix: layout.pathPrefix,
303
+ name: "*"
304
+ });
305
+ nodes.push({
306
+ id,
307
+ label: "any state",
308
+ isActive: false,
309
+ isFinal: false,
310
+ isAny: true
311
+ });
312
+ }
313
+ return nodes;
125
314
  }
126
- function recurseRegions(actor, traversal) {
315
+ function addEdgesForActor(actor, layout) {
316
+ return buildEdgesFromTransitions({
317
+ states: parseStates(actor.options),
318
+ transitions: parseTransitionMap(actor.options?.transitions),
319
+ targets: parseTargetMap(actor.options?.targets),
320
+ traversal: layout.traversal
321
+ });
322
+ }
323
+ function recurseRegions(actor, layout) {
127
324
  const nodes = [];
128
- const edges = [];
325
+ const childEdges = [];
129
326
  if (actor.regions) for (const [regionName, childActor] of Object.entries(actor.regions)) {
130
- const child = buildForActor(childActor, {
131
- ...traversal,
132
- pathPrefix: nodeId(traversal.pathPrefix, regionName)
133
- });
327
+ const child = buildForActor(childActor, { traversal: {
328
+ ...layout.traversal,
329
+ pathPrefix: nodeId({
330
+ prefix: layout.traversal.pathPrefix,
331
+ name: regionName
332
+ })
333
+ } });
134
334
  nodes.push(...child.nodes);
135
- edges.push(...child.edges);
335
+ childEdges.push(...child.edges);
136
336
  }
137
337
  return {
138
338
  nodes,
139
- edges
339
+ edges: childEdges
140
340
  };
141
341
  }
142
- function addEffectSelfLoops(actor, pathPrefix, activeSet) {
143
- const edges = [];
342
+ function readEffect(entry) {
343
+ if (typeof entry !== "object" || entry === null) return [];
344
+ const name = Reflect.get(entry, "name");
345
+ if (typeof name !== "string") return [];
346
+ const sends = Reflect.get(entry, "sends");
347
+ return [{
348
+ name,
349
+ sends: Array.isArray(sends) ? sends.filter((type) => typeof type === "string") : null
350
+ }];
351
+ }
352
+ function addEffectSelfLoops(actor, layout) {
353
+ const selfLoops = [];
144
354
  const effects = actor.options?.effects;
145
355
  if (effects) {
146
- for (const [stateName, fns] of Object.entries(effects)) if (Array.isArray(fns) && fns.length > 0) {
147
- const sid = nodeId(pathPrefix, stateName);
148
- edges.push({
149
- id: `${sid}-effect:${stateName}-${sid}`,
150
- source: sid,
151
- target: sid,
356
+ for (const [stateName, effectFns] of Object.entries(effects)) if (Array.isArray(effectFns) && effectFns.length > 0) {
357
+ const loopId = nodeId({
358
+ prefix: layout.pathPrefix,
359
+ name: stateName
360
+ });
361
+ selfLoops.push({
362
+ id: `${loopId}-effect:${stateName}-${loopId}`,
363
+ source: loopId,
364
+ target: loopId,
152
365
  label: `effect:${stateName}`,
153
- isActive: activeSet.has(sid),
154
- isInternal: true
366
+ isActive: layout.activeSet.has(loopId),
367
+ isInternal: true,
368
+ contexts: [],
369
+ effects: effectFns.flatMap((entry) => readEffect(entry))
155
370
  });
156
371
  }
157
372
  }
158
- return edges;
373
+ return selfLoops;
159
374
  }
160
- function buildForActor(actor, traversal) {
375
+ function buildForActor(actor, layout) {
161
376
  if (!actor) return {
162
377
  nodes: [],
163
378
  edges: []
164
379
  };
165
- const nodes = addNodesForActor(actor, traversal.pathPrefix, traversal.activeSet);
166
- const edges = addEdgesForActor(actor, traversal);
167
- const region = recurseRegions(actor, traversal);
380
+ const nodes = addNodesForActor(actor, {
381
+ pathPrefix: layout.traversal.pathPrefix,
382
+ activeSet: layout.traversal.activeSet
383
+ });
384
+ const actorEdges = addEdgesForActor(actor, { traversal: layout.traversal });
385
+ const region = recurseRegions(actor, { traversal: layout.traversal });
168
386
  nodes.push(...region.nodes);
169
- edges.push(...region.edges);
170
- edges.push(...addEffectSelfLoops(actor, traversal.pathPrefix, traversal.activeSet));
387
+ actorEdges.push(...region.edges);
388
+ actorEdges.push(...addEffectSelfLoops(actor, {
389
+ pathPrefix: layout.traversal.pathPrefix,
390
+ activeSet: layout.traversal.activeSet
391
+ }));
171
392
  return {
172
393
  nodes,
173
- edges
394
+ edges: actorEdges
174
395
  };
175
396
  }
176
- function addInitialNode(actor, nodes, edges) {
177
- const initialName = actor.options?.initial?.name;
397
+ function addInitialNode(actor, layout) {
398
+ const initialName = parseInitialName(actor.options);
178
399
  if (!initialName) return;
179
- const initNodeId = nodeId("", INITIAL_NODE_ID);
180
- const targetId = nodeId("", initialName);
181
- nodes.push({
400
+ const initNodeId = INITIAL_NODE_ID;
401
+ const targetId = nodeId({
402
+ prefix: "",
403
+ name: initialName
404
+ });
405
+ layout.nodes.push({
182
406
  id: initNodeId,
183
407
  label: "",
184
408
  isActive: false,
185
409
  isFinal: false,
186
410
  isInitial: true
187
411
  });
188
- edges.push({
412
+ layout.graphEdges.push({
189
413
  id: `${initNodeId}->${targetId}`,
190
414
  source: initNodeId,
191
415
  target: targetId,
192
416
  label: "",
193
- isActive: true
417
+ isActive: true,
418
+ contexts: []
194
419
  });
195
420
  }
196
- function collectActorsFromSnapshot(snapshot) {
197
- const activeSet = /* @__PURE__ */ new Set();
198
- collectActiveStates(snapshot, "", activeSet);
199
- return activeSet;
200
- }
201
- function collectNamedContexts(options) {
202
- return options?.sampleContexts ?? (options?.sampleContext ? { default: options.sampleContext } : void 0);
421
+ function collectNamedContexts(named) {
422
+ return named?.sampleContexts ?? (named?.sampleContext ? { default: named.sampleContext } : void 0);
203
423
  }
204
424
  function buildGraph(actor, options) {
205
425
  if (!actor) return {
206
426
  nodes: [],
207
427
  edges: []
208
428
  };
209
- return Either.match(Either.from(() => {
429
+ return Either.getOrElse(Either.from(() => {
210
430
  const activeSet = collectActorsFromSnapshot(actor.snapshot());
211
- const contexts = collectNamedContexts(options) ?? { default: { ...actor.context } };
212
- const { nodes, edges } = buildForActor(actor, {
431
+ const contexts = collectNamedContexts(options) ?? { default: parseContextRecord(actor.context) };
432
+ const built = buildForActor(actor, { traversal: {
213
433
  actor,
214
434
  pathPrefix: "",
215
435
  activeSet,
216
436
  internalIds: options?.internalIds,
217
437
  contexts,
218
438
  contextNames: Object.keys(contexts)
439
+ } });
440
+ addInitialNode(actor, {
441
+ nodes: built.nodes,
442
+ graphEdges: built.edges
219
443
  });
220
- addInitialNode(actor, nodes, edges);
221
444
  return {
222
- nodes,
223
- edges
445
+ nodes: built.nodes,
446
+ edges: built.edges
224
447
  };
225
- }), (error) => {
226
- throw error;
227
- }, (result) => result);
228
- }
229
- //#endregion
230
- //#region src/history.ts
231
- const entriesMap = /* @__PURE__ */ new WeakMap();
232
- function entriesOfType(entries, type) {
233
- return entries.filter((e) => e.type === type).map((e) => e.data);
234
- }
235
- var History = class {
236
- constructor() {
237
- entriesMap.set(this, []);
238
- }
239
- append(entry) {
240
- entriesMap.get(this).push(entry);
241
- }
242
- entries() {
243
- return entriesMap.get(this);
244
- }
245
- stateVisits() {
246
- return entriesOfType(entriesMap.get(this), "state_visit");
247
- }
248
- transitions() {
249
- return entriesOfType(entriesMap.get(this), "transition");
250
- }
251
- effects() {
252
- return entriesOfType(entriesMap.get(this), "effect");
253
- }
254
- sends() {
255
- return entriesOfType(entriesMap.get(this), "send");
256
- }
257
- visitedStates() {
258
- const result = /* @__PURE__ */ new Set();
259
- for (const visit of this.stateVisits()) result.add(visit.stateName);
260
- return result;
261
- }
262
- firedTransitions() {
263
- const result = /* @__PURE__ */ new Set();
264
- for (const t of this.transitions()) result.add(`${t.from}:${t.event}`);
265
- return result;
266
- }
267
- reset() {
268
- const arr = entriesMap.get(this);
269
- arr.length = 0;
448
+ }), { onLeft: () => ({
449
+ nodes: [],
450
+ edges: []
451
+ }) });
452
+ }
453
+ // src/instrument.ts
454
+ function wrapWithProxy(actor, wrapped) {
455
+ function forwardOn(event, options) {
456
+ return actor.on(event, options);
270
457
  }
271
- };
272
- //#endregion
273
- //#region src/instrument.ts
274
- function wrapWithProxy(actor, history) {
275
- const origSend = actor.send.bind(actor);
276
458
  return {
277
459
  get history() {
278
- return history;
460
+ return wrapped.history;
279
461
  },
280
462
  get state() {
281
463
  return actor.state;
@@ -293,181 +475,226 @@ function wrapWithProxy(actor, history) {
293
475
  return actor.options;
294
476
  },
295
477
  send(event) {
296
- trackSendEvent(history, event);
297
- origSend(event);
478
+ trackSendEvent({
479
+ history: wrapped.history,
480
+ sentEvent: event
481
+ });
482
+ actor.send(event);
298
483
  },
299
484
  snapshot() {
300
485
  return actor.snapshot();
301
486
  },
302
- on: actor.on.bind(actor),
487
+ on: forwardOn,
303
488
  inject: actor.inject.bind(actor),
304
489
  dispose: actor.dispose.bind(actor),
305
490
  recover: actor.recover.bind(actor),
306
491
  settled: actor.settled.bind(actor)
307
492
  };
308
493
  }
309
- function recordTransition(history, rec) {
310
- history.append({
494
+ function recordTransition(recorder) {
495
+ recorder.history.append({
311
496
  type: "transition",
312
497
  data: {
313
- from: rec.from,
314
- event: rec.event,
315
- to: rec.to,
316
- timestamp: Date.now()
498
+ from: recorder.record.from,
499
+ event: recorder.record.event,
500
+ to: recorder.record.to
317
501
  }
318
502
  });
319
- if (!rec.transitioned) return;
320
- history.append({
503
+ if (!recorder.record.transitioned) return;
504
+ recorder.history.append({
321
505
  type: "state_visit",
322
- data: {
323
- stateName: rec.to,
324
- timestamp: Date.now()
325
- }
506
+ data: { stateName: recorder.record.to }
326
507
  });
327
- if ((rec.effects[rec.to] ?? []).length > 0) history.append({
508
+ for (const effectName of recorder.record.effectNames) recorder.history.append({
328
509
  type: "effect",
329
510
  data: {
330
- stateName: rec.to,
331
- timestamp: Date.now()
511
+ stateName: recorder.record.to,
512
+ effectName
332
513
  }
333
514
  });
334
515
  }
335
- function trackSendEvent(history, event) {
336
- const eventId = event?.type ?? "unknown";
337
- history.append({
516
+ function trackSendEvent(tracking) {
517
+ const eventType = typeof tracking.sentEvent === "object" && tracking.sentEvent !== null && "type" in tracking.sentEvent ? tracking.sentEvent.type : "unknown";
518
+ tracking.history.append({
338
519
  type: "send",
339
- data: {
340
- event: eventId,
341
- timestamp: Date.now()
342
- }
520
+ data: { event: String(eventType) }
343
521
  });
344
522
  }
523
+ function prefixId(identity) {
524
+ return identity.prefix ? `${identity.prefix}.${identity.name}` : identity.name;
525
+ }
345
526
  function instrument(actor) {
346
- const history = new History();
347
- history.append({
527
+ const history = createHistory();
528
+ const wrapped = wrapWithProxy(actor, { history });
529
+ attach(actor, {
530
+ history,
531
+ prefix: ""
532
+ });
533
+ return wrapped;
534
+ }
535
+ function attach(childActor, branch) {
536
+ branch.history.append({
348
537
  type: "state_visit",
349
- data: {
350
- stateName: actor.state.name,
351
- timestamp: Date.now()
352
- }
538
+ data: { stateName: prefixId({
539
+ prefix: branch.prefix,
540
+ name: childActor.state.name
541
+ }) }
353
542
  });
354
- const wrapped = wrapWithProxy(actor, history);
355
- const effectsByState = actor.options?.effects ?? {};
356
- actor.on("transition", ({ event, from, to, transitioned }) => {
357
- recordTransition(history, {
358
- from,
359
- to,
360
- event: event.type,
361
- transitioned,
362
- effects: effectsByState
543
+ childActor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
544
+ recordTransition({
545
+ history: branch.history,
546
+ record: {
547
+ from: prefixId({
548
+ prefix: branch.prefix,
549
+ name: from
550
+ }),
551
+ to: prefixId({
552
+ prefix: branch.prefix,
553
+ name: to
554
+ }),
555
+ event: event.type,
556
+ transitioned,
557
+ effectNames: effects
558
+ }
363
559
  });
560
+ } });
561
+ for (const [regionName, regionChild] of Object.entries(childActor.regions ?? {})) attach(regionChild, {
562
+ history: branch.history,
563
+ prefix: prefixId({
564
+ prefix: branch.prefix,
565
+ name: regionName
566
+ })
364
567
  });
365
- return wrapped;
366
568
  }
367
- //#endregion
368
- //#region src/algorithms.ts
569
+ // src/algorithms.ts
369
570
  function buildAdjacency(graph) {
370
- const adj = {};
371
- for (const node of graph.nodes) adj[node.id] = [];
372
- for (const edge of graph.edges) if (adj[edge.source]) adj[edge.source].push({
571
+ const adjacency = {};
572
+ for (const node of graph.nodes) adjacency[node.id] = [];
573
+ for (const edge of graph.edges) if (adjacency[edge.source]) adjacency[edge.source].push({
373
574
  target: edge.target,
374
575
  label: edge.label
375
576
  });
376
- return adj;
577
+ return adjacency;
377
578
  }
378
- function bfsWalk(adj, start, visit) {
579
+ function bfsWalk(adjacency, walk) {
379
580
  const visited = {};
380
- const queue = [start];
581
+ const queue = [walk.start];
381
582
  while (queue.length > 0) {
382
583
  const current = queue.shift();
383
- if (visited[current]) continue;
584
+ if (current === void 0 || visited[current]) continue;
384
585
  visited[current] = true;
385
- if (visit(current)) break;
386
- const neighbors = adj[current] || [];
387
- for (let i = 0; i < neighbors.length; i++) {
388
- const target = neighbors[i].target;
389
- if (!visited[target]) queue.push(target);
390
- }
586
+ if (walk.visit(current)) break;
587
+ for (const neighbor of adjacency[current] ?? []) if (!visited[neighbor.target]) queue.push(neighbor.target);
391
588
  }
392
589
  return visited;
393
590
  }
394
- function reachable(graph, fromId, toId) {
395
- return bfsWalk(buildAdjacency(graph), fromId, (node) => node === toId)[toId] === true;
591
+ function reachable(graph, endpoints) {
592
+ return bfsWalk(buildAdjacency(graph), {
593
+ start: endpoints.fromId,
594
+ visit: (nodeId) => nodeId === endpoints.toId
595
+ })[endpoints.toId] === true;
396
596
  }
397
- function allPaths(graph, fromId, toId) {
398
- const adj = buildAdjacency(graph);
597
+ function allPaths(graph, endpoints) {
598
+ const adjacency = buildAdjacency(graph);
399
599
  const results = [];
400
600
  const visited = {};
401
- function dfs(current, path) {
402
- if (current === toId) {
403
- results.push(path.slice());
601
+ function dfs(cursor) {
602
+ if (cursor.nodeId === endpoints.toId) {
603
+ results.push(cursor.path.slice());
404
604
  return;
405
605
  }
406
- if (visited[current]) return;
407
- visited[current] = true;
408
- const neighbors = adj[current] || [];
409
- for (let i = 0; i < neighbors.length; i++) {
410
- path.push(neighbors[i].target);
411
- dfs(neighbors[i].target, path);
412
- path.pop();
606
+ if (visited[cursor.nodeId]) return;
607
+ visited[cursor.nodeId] = true;
608
+ for (const neighbor of adjacency[cursor.nodeId] ?? []) {
609
+ cursor.path.push(neighbor.target);
610
+ dfs({
611
+ nodeId: neighbor.target,
612
+ path: cursor.path
613
+ });
614
+ cursor.path.pop();
413
615
  }
414
- delete visited[current];
616
+ delete visited[cursor.nodeId];
415
617
  }
416
- dfs(fromId, [fromId]);
618
+ dfs({
619
+ nodeId: endpoints.fromId,
620
+ path: [endpoints.fromId]
621
+ });
417
622
  return results;
418
623
  }
419
624
  function findCycles(graph) {
420
- const adj = buildAdjacency(graph);
625
+ const adjacency = buildAdjacency(graph);
421
626
  const cycles = [];
627
+ const seen = /* @__PURE__ */ new Set();
422
628
  const visited = {};
423
629
  const inStack = {};
424
630
  const path = [];
425
- function dfs(node) {
426
- if (inStack[node]) {
427
- const cycleStart = path.indexOf(node);
428
- if (cycleStart !== -1) cycles.push(path.slice(cycleStart).concat(node));
631
+ /**
632
+ * A cycle ends with its start node (e.g. the list a, b, a). The body is
633
+ * every element but the last. Rotate the body so it starts at the
634
+ * lexicographically smallest id, giving every rotation of a cycle the same
635
+ * canonical key.
636
+ */
637
+ function rotateToMin(cycleNodes) {
638
+ const body = cycleNodes.slice(0, -1);
639
+ let minIndex = 0;
640
+ let minValue = body[0] ?? "";
641
+ for (const [index, candidate] of body.entries()) if (candidate < minValue) {
642
+ minValue = candidate;
643
+ minIndex = index;
644
+ }
645
+ return body.slice(minIndex).concat(body.slice(0, minIndex)).join(":");
646
+ }
647
+ function dfs(nodeId) {
648
+ if (inStack[nodeId]) {
649
+ const cycleStart = path.indexOf(nodeId);
650
+ if (cycleStart !== -1) {
651
+ const cycle = path.slice(cycleStart).concat(nodeId);
652
+ const key = rotateToMin(cycle);
653
+ if (!seen.has(key)) {
654
+ seen.add(key);
655
+ cycles.push(cycle);
656
+ }
657
+ }
429
658
  return;
430
659
  }
431
- if (visited[node]) return;
432
- visited[node] = true;
433
- inStack[node] = true;
434
- path.push(node);
435
- const neighbors = adj[node] || [];
436
- for (let i = 0; i < neighbors.length; i++) dfs(neighbors[i].target);
660
+ if (visited[nodeId]) return;
661
+ visited[nodeId] = true;
662
+ inStack[nodeId] = true;
663
+ path.push(nodeId);
664
+ for (const neighbor of adjacency[nodeId] ?? []) dfs(neighbor.target);
437
665
  path.pop();
438
- delete inStack[node];
666
+ delete inStack[nodeId];
667
+ delete visited[nodeId];
439
668
  }
440
669
  for (const node of graph.nodes) dfs(node.id);
441
670
  return cycles;
442
671
  }
443
- function unreachableNodes(graph, fromId) {
444
- const visited = bfsWalk(buildAdjacency(graph), fromId, () => false);
672
+ function unreachableNodes(graph, options) {
673
+ const visited = bfsWalk(buildAdjacency(graph), {
674
+ start: options.fromId,
675
+ visit: () => false
676
+ });
445
677
  const result = [];
446
678
  for (const node of graph.nodes) if (!visited[node.id]) result.push(node.id);
447
679
  return result;
448
680
  }
449
- function shortestPath(graph, fromId, toId) {
450
- const adj = buildAdjacency(graph);
681
+ function shortestPath(graph, endpoints) {
682
+ const adjacency = buildAdjacency(graph);
451
683
  const visited = {};
452
684
  const queue = [{
453
- node: fromId,
454
- path: [fromId]
685
+ node: endpoints.fromId,
686
+ path: [endpoints.fromId]
455
687
  }];
456
688
  while (queue.length > 0) {
457
- const { node: current, path } = queue.shift();
458
- if (current === toId) return path;
459
- if (visited[current]) continue;
460
- visited[current] = true;
461
- const neighbors = adj[current] || [];
462
- for (let i = 0; i < neighbors.length; i++) {
463
- const target = neighbors[i].target;
464
- if (!visited[target]) queue.push({
465
- node: target,
466
- path: path.concat(target)
467
- });
468
- }
689
+ const candidate = queue.shift();
690
+ if (candidate === void 0) break;
691
+ if (candidate.node === endpoints.toId) return candidate.path;
692
+ if (visited[candidate.node]) continue;
693
+ visited[candidate.node] = true;
694
+ for (const neighbor of adjacency[candidate.node] ?? []) if (!visited[neighbor.target]) queue.push({
695
+ node: neighbor.target,
696
+ path: candidate.path.concat(neighbor.target)
697
+ });
469
698
  }
470
- return null;
471
699
  }
472
- //#endregion
473
- export { History, INITIAL_NODE_ID, allPaths, buildGraph, collectActiveStates, findCycles, instrument, reachable, shortestPath, unreachableNodes };
700
+ export { ANY_NODE_NAME, INITIAL_NODE_ID, allPaths, buildGraph, collectActiveStates, createHistory, findCycles, instrument, isAnyNode, parseContextRecord, parseInitialName, parseInternalEventIds, parseStates, parseTransitionMap, reachable, shortestPath, unreachableNodes };