@mantaq/traversal 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,8 +1,155 @@
1
1
  # @mantaq/traversal
2
2
 
3
- Graph extraction, traversal algorithms, and runtime tracking for
3
+ Graph extraction, traversal algorithms, and runtime coverage tracking for
4
4
  [@mantaq/core](https://www.npmjs.com/package/@mantaq/core) actors.
5
5
 
6
- Build the state-machine graph from an actor, walk it with traversal
7
- algorithms, and instrument coverage as the actor runs — useful for
8
- verifying that every declared transition is reachable and exercised.
6
+ Use it to:
7
+
8
+ - **Inspect** a state machine statically. Turn an actor into a
9
+ `nodes`/`edges` graph of its states and transitions.
10
+ - **Reason** about that graph. Which states are reachable, what paths exist
11
+ between two states, and whether any declared transition can never fire.
12
+ - **Verify** coverage at runtime. Wrap an actor so every visited state,
13
+ fired transition, and executed effect is recorded as it runs.
14
+
15
+ All three pieces work on the same `ActorGraph` shape, so you can build the
16
+ graph once and walk it however your check needs.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install @mantaq/traversal
22
+ ```
23
+
24
+ > `@mantaq/traversal` depends on `@mantaq/core` and `@mantaq/utils`, which are
25
+ > resolved as workspace packages in this repo.
26
+
27
+ ## Building the graph
28
+
29
+ `buildGraph` walks an actor (and any nested regions) and returns an
30
+ `ActorGraph`:
31
+
32
+ ```ts
33
+ import { buildGraph } from "@mantaq/traversal";
34
+ import { createActor } from "@mantaq/core";
35
+
36
+ const actor = createActor(myMachine);
37
+ const graph = buildGraph(actor);
38
+ // graph.nodes: GraphNode[] — one per declared state (+ an `__initial__` node)
39
+ // graph.edges: GraphEdge[] — one per (state, event) handler
40
+ ```
41
+
42
+ Each `GraphNode` carries `id`, `label`, `isActive`, `isFinal`, and
43
+ `isInitial`. Each `GraphEdge` carries `source`, `target`, `label` (the event
44
+ that triggers it), `isActive`, `isInternal`, and `isUndetermined`.
45
+
46
+ An edge is **undetermined** when its handler runs but does not declare a target
47
+ state for the sampled context (e.g. a guard that resolves only at runtime). The
48
+ graph is a _static_ approximation: it invokes each handler with a sample
49
+ context to discover the transitions it _could_ take, so it documents intent
50
+ and reachability, not live execution.
51
+
52
+ ### Sampling contexts
53
+
54
+ Handlers may branch on context. Pass one or more named sample contexts to see
55
+ how the graph differs per context shape:
56
+
57
+ ```ts
58
+ const graph = buildGraph(actor, {
59
+ sampleContexts: {
60
+ guest: { loggedIn: false },
61
+ member: { loggedIn: true },
62
+ },
63
+ });
64
+ ```
65
+
66
+ Each context name is recorded on the relevant edges (`edge.contexts`), so you
67
+ can tell which branches belong to which context. With a single context, use
68
+ `sampleContext` instead.
69
+
70
+ ### Constants
71
+
72
+ - `INITIAL_NODE_ID`. The synthetic id of the graph's entry node (the edge
73
+ from it points to the actor's initial state).
74
+
75
+ ## Walking the graph
76
+
77
+ The algorithms module operates on an `ActorGraph` and is pure. No actor
78
+ needed:
79
+
80
+ ```ts
81
+ import { reachable, allPaths, findCycles, unreachableNodes, shortestPath } from "@mantaq/traversal";
82
+ ```
83
+
84
+ | Function | Returns | Use |
85
+ | ----------------------------------- | ------------------ | --------------------------------------------------- |
86
+ | `reachable(graph, fromId, toId)` | `boolean` | Is `toId` reachable from `fromId`? |
87
+ | `allPaths(graph, fromId, toId)` | `string[][]` | Every distinct path of node ids between two states. |
88
+ | `findCycles(graph)` | `string[][]` | Every cycle in the graph, as node-id loops. |
89
+ | `unreachableNodes(graph, fromId)` | `string[]` | Nodes no walk from `fromId` can ever reach. |
90
+ | `shortestPath(graph, fromId, toId)` | `string[] \| null` | The fewest-edge path, or `null` if unreachable. |
91
+
92
+ A common check is "every declared state is reachable from the initial state":
93
+
94
+ ```ts
95
+ import { INITIAL_NODE_ID, unreachableNodes } from "@mantaq/traversal";
96
+
97
+ const dead = unreachableNodes(graph, INITIAL_NODE_ID);
98
+ if (dead.length > 0) {
99
+ throw new Error(`Unreachable states: ${dead.join(", ")}`);
100
+ }
101
+ ```
102
+
103
+ ## Tracking coverage at runtime
104
+
105
+ `instrument` wraps an actor so it records everything it does into a `History`.
106
+ The wrapped actor has the same surface as the original (`send`, `on`,
107
+ `snapshot`, `context`, `regions`, `inject`, `dispose`, `recover`, `settled`)
108
+ plus a `history` property.
109
+
110
+ ```ts
111
+ import { instrument } from "@mantaq/traversal";
112
+
113
+ const wrapped = instrument(actor);
114
+ wrapped.send({ type: "START" });
115
+
116
+ const history = wrapped.history;
117
+ history.visitedStates(); // Set<string> — every state the actor entered
118
+ history.firedTransitions(); // Set<string> — "from:event" for each fired transition
119
+ history.effects(); // EffectRecord[] — effects executed per state
120
+ history.transitions(); // TransitionRecord[] — full from/event/to log
121
+ history.sends(); // { event: string }[] — every event sent in
122
+ history.entries(); // HistoryEntry[] — the raw, ordered record
123
+ ```
124
+
125
+ `History` records state visits, transitions, effects, and sends. Call
126
+ `history.reset()` to reuse a wrapper across independent runs.
127
+
128
+ Combine static and runtime views to assert coverage. E.g. that every
129
+ reachable state in the graph was actually visited by a scenario:
130
+
131
+ ```ts
132
+ const graph = buildGraph(actor);
133
+ const wrapped = instrument(actor);
134
+ runScenario(wrapped); // exercise the actor
135
+
136
+ const graphStates = new Set(graph.nodes.map((n) => n.id));
137
+ const missed = [...wrapped.history.visitedStates()].filter((s) => !graphStates.has(s));
138
+ ```
139
+
140
+ ## Types
141
+
142
+ ```ts
143
+ import type {
144
+ ActorGraph,
145
+ GraphNode,
146
+ GraphEdge,
147
+ StateVisit,
148
+ TransitionRecord,
149
+ EffectRecord,
150
+ HistoryEntry,
151
+ } from "@mantaq/traversal";
152
+ ```
153
+
154
+ `ActorGraph` is `{ nodes: GraphNode[]; edges: GraphEdge[] }`. See the package
155
+ source for the full field shapes.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,30 @@
1
- import { AnyActor, Context, Snapshot, TransitionInfo } from "@mantaq/core";
1
+ import { AnyActor, Context, ErrorInfo, InternalEvent, Snapshot, TransitionInfo } from "@mantaq/core";
2
2
  //#region src/types.d.ts
3
+ /** Synthetic event object for graph introspection — matches minimal InternalEvent shape */
4
+ interface SyntheticEvent {
5
+ type: string;
6
+ payload?: unknown;
7
+ }
8
+ /** Simplified handler return — matches TransitionResult with erased generics */
9
+ interface HandlerResult {
10
+ state?: {
11
+ name?: string;
12
+ };
13
+ emit?: Array<{
14
+ type?: string;
15
+ }>;
16
+ }
17
+ type TransitionHandler = (event: SyntheticEvent, options: {
18
+ context: Context<Record<string, unknown>>;
19
+ actor: AnyActor<unknown>;
20
+ }) => HandlerResult | undefined;
21
+ /** Map of state/Any → event → handler */
22
+ type TransitionDispatchMap = Record<string, Record<string, TransitionHandler>>;
23
+ /** State definition — matches AnyActor.options.states element type */
24
+ type StateDef = {
25
+ name: string;
26
+ isFinal: boolean;
27
+ };
3
28
  interface GraphNode {
4
29
  id: string;
5
30
  label: string;
@@ -18,7 +43,7 @@ interface GraphEdge {
18
43
  payload?: {
19
44
  action?: string;
20
45
  };
21
- contexts?: string[];
46
+ contexts: string[];
22
47
  }
23
48
  interface ActorGraph {
24
49
  nodes: GraphNode[];
@@ -26,78 +51,102 @@ interface ActorGraph {
26
51
  }
27
52
  interface StateVisit {
28
53
  stateName: string;
29
- timestamp: number;
30
54
  }
31
55
  interface TransitionRecord {
32
56
  from: string;
33
57
  event: string;
34
58
  to: string | undefined;
35
- timestamp: number;
36
59
  }
37
60
  interface EffectRecord {
38
61
  stateName: string;
39
- timestamp: number;
62
+ effectName: string;
40
63
  }
64
+ type SendRecord = {
65
+ event: string;
66
+ };
41
67
  interface HistoryEntry {
42
68
  type: "state_visit" | "transition" | "effect" | "send";
43
- data: StateVisit | TransitionRecord | EffectRecord | {
44
- event: string;
45
- timestamp: number;
46
- };
69
+ data: StateVisit | TransitionRecord | EffectRecord | SendRecord;
47
70
  }
48
71
  //#endregion
49
- //#region src/graph.d.ts
50
- declare const INITIAL_NODE_ID = "__initial__";
51
- declare function collectActiveStates(snapshot: Snapshot, prefix: string, activeSet: Set<string>): void;
52
- declare function buildGraph<C>(actor: AnyActor<C>, options?: {
53
- internalIds?: Set<string>;
54
- sampleContext?: Record<string, unknown>;
55
- sampleContexts?: Record<string, Record<string, unknown>>;
56
- }): ActorGraph;
57
- //#endregion
58
72
  //#region src/history.d.ts
59
- declare class History {
60
- constructor();
73
+ interface History {
61
74
  append(entry: HistoryEntry): void;
62
75
  entries(): readonly HistoryEntry[];
63
76
  stateVisits(): StateVisit[];
64
77
  transitions(): TransitionRecord[];
65
78
  effects(): EffectRecord[];
66
- sends(): Array<{
67
- event: string;
68
- timestamp: number;
69
- }>;
79
+ sends(): SendRecord[];
70
80
  visitedStates(): Set<string>;
71
81
  firedTransitions(): Set<string>;
72
82
  reset(): void;
73
83
  }
84
+ /**
85
+ * History records the runtime trace of an instrumented actor. A closure over
86
+ * the entry list keeps the recorder mutable without exposing the raw array.
87
+ */
88
+ declare function createHistory(): History;
89
+ //#endregion
90
+ //#region src/graph.d.ts
91
+ declare const INITIAL_NODE_ID = "__initial__";
92
+ declare function collectActiveStates(snapshot: Snapshot, options: {
93
+ prefix: string;
94
+ activeSet: Set<string>;
95
+ }): void;
96
+ declare function buildGraph<C>(actor: AnyActor<C>, options?: {
97
+ internalIds?: Set<string>;
98
+ sampleContext?: Record<string, unknown>;
99
+ sampleContexts?: Record<string, Record<string, unknown>>;
100
+ }): ActorGraph;
101
+ //#endregion
102
+ //#region src/parse-graph.d.ts
103
+ declare function parseStates(options: unknown): ReadonlyArray<StateDef>;
104
+ declare function parseTransitionMap(source: unknown): TransitionDispatchMap;
105
+ declare function parseInitialName(options: unknown): string | undefined;
106
+ declare function parseContextRecord(context: unknown): Record<string, unknown>;
107
+ declare function parseInternalEventIds(options: unknown): Set<string>;
74
108
  //#endregion
75
109
  //#region src/instrument.d.ts
110
+ type SubscriberEventName = "change" | "done" | "transition" | "error" | "output";
111
+ type SubscriberHandler<C, E extends SubscriberEventName> = E extends "change" ? (snapshot: Snapshot<C>, prev: Snapshot<C>) => void : E extends "done" ? () => void : E extends "transition" ? (info: TransitionInfo) => void : E extends "error" ? (info: ErrorInfo) => void : E extends "output" ? (event: InternalEvent) => void : never;
76
112
  interface InstrumentedActor<C = Record<string, unknown>> {
77
113
  history: History;
78
- send(event: unknown): void;
114
+ send(event: InternalEvent): void;
79
115
  state: AnyActor["state"];
80
116
  snapshot(): Snapshot<C>;
81
117
  regions: Record<string, AnyActor>;
82
118
  context?: C;
83
119
  clock: AnyActor["clock"];
84
- on(event: "change", fn: (snapshot: Snapshot<C>, prev: Snapshot<C>) => void): () => void;
85
- on(event: "transition", fn: (info: TransitionInfo) => void): () => void;
86
- on(event: "done", fn: () => void): () => void;
120
+ on<E extends SubscriberEventName>(event: E, options: {
121
+ fn: SubscriberHandler<C, E>;
122
+ }): () => void;
87
123
  recover(target: {
88
124
  state: AnyActor["state"];
89
125
  context: C;
90
126
  }): void;
91
127
  settled(): Promise<void>;
92
128
  options?: AnyActor["options"];
129
+ inject(event: InternalEvent): void;
130
+ dispose(): void;
93
131
  }
94
132
  declare function instrument<C>(actor: AnyActor<C>): InstrumentedActor<C>;
95
133
  //#endregion
96
134
  //#region src/algorithms.d.ts
97
- declare function reachable(graph: ActorGraph, fromId: string, toId: string): boolean;
98
- declare function allPaths(graph: ActorGraph, fromId: string, toId: string): string[][];
135
+ declare function reachable(graph: ActorGraph, endpoints: {
136
+ fromId: string;
137
+ toId: string;
138
+ }): boolean;
139
+ declare function allPaths(graph: ActorGraph, endpoints: {
140
+ fromId: string;
141
+ toId: string;
142
+ }): string[][];
99
143
  declare function findCycles(graph: ActorGraph): string[][];
100
- declare function unreachableNodes(graph: ActorGraph, fromId: string): string[];
101
- declare function shortestPath(graph: ActorGraph, fromId: string, toId: string): string[] | null;
144
+ declare function unreachableNodes(graph: ActorGraph, options: {
145
+ fromId: string;
146
+ }): string[];
147
+ declare function shortestPath(graph: ActorGraph, endpoints: {
148
+ fromId: string;
149
+ toId: string;
150
+ }): string[] | undefined;
102
151
  //#endregion
103
- export { type ActorGraph, type EffectRecord, type GraphEdge, type GraphNode, History, type HistoryEntry, INITIAL_NODE_ID, type InstrumentedActor, type StateVisit, type TransitionRecord, allPaths, buildGraph, collectActiveStates, findCycles, instrument, reachable, shortestPath, unreachableNodes };
152
+ export { type ActorGraph, type EffectRecord, type GraphEdge, type GraphNode, type History, type HistoryEntry, INITIAL_NODE_ID, type InstrumentedActor, type SendRecord, type StateDef, type StateVisit, type SyntheticEvent, type TransitionDispatchMap, type TransitionHandler, type TransitionRecord, allPaths, buildGraph, collectActiveStates, createHistory, findCycles, instrument, parseContextRecord, parseInitialName, parseInternalEventIds, parseStates, parseTransitionMap, reachable, shortestPath, unreachableNodes };
package/dist/index.mjs CHANGED
@@ -1,281 +1,417 @@
1
- import { Context } from "@mantaq/core";
1
+ import { Context, isStateRef } from "@mantaq/core";
2
2
  import { Either } from "@mantaq/utils";
3
+ //#region 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
+ //#endregion
57
+ //#region src/is-handler-result.ts
58
+ function isHandlerResult(value) {
59
+ if (typeof value !== "object" || value === null) return false;
60
+ const candidate = value;
61
+ 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));
62
+ }
63
+ //#endregion
64
+ //#region src/parse-graph.ts
65
+ function parseStates(options) {
66
+ const states = options?.states;
67
+ if (!Array.isArray(states)) return [];
68
+ return states.filter((stateEntry) => typeof stateEntry === "object" && stateEntry !== null && typeof stateEntry.name === "string" && typeof stateEntry.isFinal === "boolean");
69
+ }
70
+ function parseTransitionMap(source) {
71
+ const dispatchMap = {};
72
+ if (typeof source !== "object" || source === null) return dispatchMap;
73
+ for (const [stateName, eventMap] of Object.entries(source)) {
74
+ if (typeof eventMap !== "object" || eventMap === null) continue;
75
+ for (const [eventType, handler] of Object.entries(eventMap)) {
76
+ if (typeof handler !== "function") continue;
77
+ (dispatchMap[stateName] ??= {})[eventType] = handler;
78
+ }
79
+ }
80
+ return dispatchMap;
81
+ }
82
+ function parseInitialName(options) {
83
+ const initial = options?.initial;
84
+ if (initial === void 0 || initial === null) return void 0;
85
+ if (isStateRef(initial)) return initial.name;
86
+ const wrapped = initial;
87
+ return isStateRef(wrapped.state) ? wrapped.state.name : void 0;
88
+ }
89
+ function parseContextRecord(context) {
90
+ if (context === void 0 || context === null) return {};
91
+ return { ...context };
92
+ }
93
+ function parseInternalEventIds(options) {
94
+ const internal = options?.internal;
95
+ const eventIds = /* @__PURE__ */ new Set();
96
+ if (!Array.isArray(internal)) return eventIds;
97
+ for (const entry of internal) {
98
+ if (typeof entry !== "object" || entry === null || !("type" in entry)) continue;
99
+ const eventType = entry.type;
100
+ if (typeof eventType === "string") eventIds.add(eventType);
101
+ }
102
+ return eventIds;
103
+ }
104
+ //#endregion
3
105
  //#region src/graph.ts
4
106
  const INITIAL_NODE_ID = "__initial__";
5
- function collectActiveStates(snapshot, prefix, activeSet) {
6
- if (!snapshot || !snapshot.path) return;
107
+ function collectActiveStates(snapshot, options) {
108
+ if (!snapshot.path) return;
7
109
  const currentName = snapshot.path[snapshot.path.length - 1];
8
110
  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);
111
+ const fullId = nodeId({
112
+ prefix: options.prefix,
113
+ name: currentName
114
+ });
115
+ options.activeSet.add(fullId);
116
+ if (snapshot.regions && !snapshot.done && !snapshot.error) for (const [regionName, regionSnapshot] of Object.entries(snapshot.regions)) collectActiveStates(regionSnapshot, {
117
+ prefix: regionName,
118
+ activeSet: options.activeSet
119
+ });
12
120
  }
13
- function nodeId(prefix, name) {
14
- return prefix ? `${prefix}.${name}` : name;
121
+ function nodeId(identity) {
122
+ return identity.prefix ? `${identity.prefix}.${identity.name}` : identity.name;
15
123
  }
16
- function buildNodesFromStates(states, activeSet, pathPrefix) {
17
- return states.map((stateRef) => {
18
- const nid = nodeId(pathPrefix, stateRef.name);
124
+ function buildNodesFromStates(states, options) {
125
+ return states.map((stateEntry) => {
126
+ const graphId = nodeId({
127
+ prefix: options.pathPrefix,
128
+ name: stateEntry.name
129
+ });
19
130
  return {
20
- id: nid,
21
- label: stateRef.name,
22
- isActive: activeSet.has(nid),
23
- isFinal: stateRef.isFinal
131
+ id: graphId,
132
+ label: stateEntry.name,
133
+ isActive: options.activeSet.has(graphId),
134
+ isFinal: stateEntry.isFinal
24
135
  };
25
136
  });
26
137
  }
27
- function invokeHandler(handler, eventId, context, actor) {
138
+ function invokeHandler(handler, options) {
28
139
  return Either.from(() => {
29
140
  const result = handler({
30
- type: eventId,
141
+ type: options.eventId,
31
142
  payload: {}
32
143
  }, {
33
- context: new Context(() => context, () => {}),
34
- actor
144
+ context: Context({
145
+ get: () => options.context,
146
+ set: () => {}
147
+ }),
148
+ actor: options.actor
35
149
  });
36
- return {
37
- targetName: result?.state?.name,
38
- emitNames: result?.emit?.map((e) => e.type).filter((type) => Boolean(type)) ?? []
150
+ return isHandlerResult(result) ? {
151
+ targetName: result.state?.name,
152
+ emitNames: result.emit?.map((emitted) => emitted.type).filter((eventType) => Boolean(eventType)) ?? []
153
+ } : {
154
+ targetName: void 0,
155
+ emitNames: []
39
156
  };
40
157
  });
41
158
  }
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}`;
159
+ function upsertEdge(sink) {
160
+ const undetermined = !sink.handled.targetName;
161
+ const targetId = sink.handled.targetName ? nodeId({
162
+ prefix: sink.traversal.pathPrefix,
163
+ name: sink.handled.targetName
164
+ }) : `${sink.sourceId}-undetermined-${sink.eventId}`;
165
+ const edgeId = `${sink.sourceId}-${sink.eventId}-${targetId}`;
46
166
  const existing = sink.edgeMap.get(edgeId);
47
167
  if (existing) existing.contexts.push(sink.ctxName);
48
168
  else sink.edgeMap.set(edgeId, {
49
169
  id: edgeId,
50
170
  source: sink.sourceId,
51
171
  target: targetId,
52
- label: eventId,
53
- isActive: traversal.activeSet.has(sink.sourceId),
54
- isInternal: traversal.internalIds?.has(eventId),
172
+ label: sink.eventId,
173
+ isActive: sink.traversal.activeSet.has(sink.sourceId),
174
+ isInternal: sink.traversal.internalIds?.has(sink.eventId),
55
175
  isUndetermined: undetermined,
56
176
  contexts: [sink.ctxName],
57
- ...handled.emitNames.length > 0 && { payload: { action: `emit(${handled.emitNames.join(", ")})` } }
177
+ ...sink.handled.emitNames.length > 0 && { payload: { action: `emit(${sink.handled.emitNames.join(", ")})` } }
58
178
  });
59
179
  }
60
- function processStateTransitions(stateTransitions, pass) {
180
+ function processStateTransitions(options) {
61
181
  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);
76
- }
182
+ for (const [eventId, handler] of Object.entries(options.stateTransitions)) for (const ctxName of options.pass.traversal.contextNames) {
183
+ const sampleContext = options.pass.traversal.contexts[ctxName];
184
+ if (sampleContext === void 0) continue;
185
+ const handled = Either.getOrElse(invokeHandler(handler, {
186
+ eventId,
187
+ context: sampleContext,
188
+ actor: options.pass.traversal.actor
189
+ }), { onLeft: () => ({
190
+ targetName: void 0,
191
+ emitNames: []
192
+ }) });
193
+ if (handled.targetName) stateTransitionedEvents.add(eventId);
194
+ upsertEdge({
195
+ edgeMap: options.pass.edgeMap,
196
+ sourceId: options.pass.sourceId,
197
+ ctxName,
198
+ traversal: options.pass.traversal,
199
+ eventId,
200
+ handled
201
+ });
77
202
  }
78
203
  return stateTransitionedEvents;
79
204
  }
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), () => ({
205
+ function mergeAnyTransitions(options) {
206
+ for (const [eventId, handler] of Object.entries(options.anyTransitions)) {
207
+ if (options.stateTransitionedEvents.has(eventId)) continue;
208
+ for (const ctxName of options.pass.traversal.contextNames) {
209
+ const sampleContext = options.pass.traversal.contexts[ctxName];
210
+ if (sampleContext === void 0) continue;
211
+ const handled = Either.getOrElse(invokeHandler(handler, {
212
+ eventId,
213
+ context: sampleContext,
214
+ actor: options.pass.traversal.actor
215
+ }), { onLeft: () => ({
87
216
  targetName: void 0,
88
217
  emitNames: []
89
- }));
218
+ }) });
90
219
  upsertEdge({
91
- edgeMap: pass.edgeMap,
92
- sourceId: pass.sourceId,
93
- ctxName
94
- }, pass.traversal, eventId, handled);
220
+ edgeMap: options.pass.edgeMap,
221
+ sourceId: options.pass.sourceId,
222
+ ctxName,
223
+ traversal: options.pass.traversal,
224
+ eventId,
225
+ handled
226
+ });
95
227
  }
96
228
  }
97
229
  }
98
- function collectTransitionsForState(stateTransitions, anyTransitions, sourceId, traversal) {
230
+ function collectTransitionsForState(edges) {
99
231
  const edgeMap = /* @__PURE__ */ new Map();
100
232
  const pass = {
101
233
  edgeMap,
102
- sourceId,
103
- traversal
234
+ sourceId: edges.sourceId,
235
+ traversal: edges.traversal
104
236
  };
105
- const stateTransitionedEvents = stateTransitions ? processStateTransitions(stateTransitions, pass) : /* @__PURE__ */ new Set();
106
- if (anyTransitions) mergeAnyTransitions(anyTransitions, stateTransitionedEvents, pass);
237
+ const stateTransitionedEvents = edges.stateTransitions ? processStateTransitions({
238
+ stateTransitions: edges.stateTransitions,
239
+ pass
240
+ }) : /* @__PURE__ */ new Set();
241
+ if (edges.anyTransitions) mergeAnyTransitions({
242
+ anyTransitions: edges.anyTransitions,
243
+ stateTransitionedEvents,
244
+ pass
245
+ });
107
246
  return edgeMap;
108
247
  }
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());
248
+ function buildEdgesFromTransitions(edges) {
249
+ const allEdges = [];
250
+ const anyTransitions = edges.transitions["Any"];
251
+ for (const stateEntry of edges.states) {
252
+ const sourceId = nodeId({
253
+ prefix: edges.traversal.pathPrefix,
254
+ name: stateEntry.name
255
+ });
256
+ const edgeMap = collectTransitionsForState({
257
+ stateTransitions: edges.transitions[stateEntry.name],
258
+ anyTransitions,
259
+ sourceId,
260
+ traversal: edges.traversal
261
+ });
262
+ allEdges.push(...edgeMap.values());
117
263
  }
118
- return edges;
264
+ return allEdges;
119
265
  }
120
- function addNodesForActor(actor, pathPrefix, activeSet) {
121
- return buildNodesFromStates(actor.options?.states ?? [], activeSet, pathPrefix);
266
+ function addNodesForActor(actor, layout) {
267
+ return buildNodesFromStates(parseStates(actor.options), {
268
+ activeSet: layout.activeSet,
269
+ pathPrefix: layout.pathPrefix
270
+ });
122
271
  }
123
- function addEdgesForActor(actor, traversal) {
124
- return buildEdgesFromTransitions(actor.options?.states ?? [], actor.options?.transitions, traversal);
272
+ function addEdgesForActor(actor, layout) {
273
+ return buildEdgesFromTransitions({
274
+ states: parseStates(actor.options),
275
+ transitions: parseTransitionMap(actor.options?.transitions),
276
+ traversal: layout.traversal
277
+ });
125
278
  }
126
- function recurseRegions(actor, traversal) {
279
+ function recurseRegions(actor, layout) {
127
280
  const nodes = [];
128
- const edges = [];
281
+ const childEdges = [];
129
282
  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
- });
283
+ const child = buildForActor(childActor, { traversal: {
284
+ ...layout.traversal,
285
+ pathPrefix: nodeId({
286
+ prefix: layout.traversal.pathPrefix,
287
+ name: regionName
288
+ })
289
+ } });
134
290
  nodes.push(...child.nodes);
135
- edges.push(...child.edges);
291
+ childEdges.push(...child.edges);
136
292
  }
137
293
  return {
138
294
  nodes,
139
- edges
295
+ edges: childEdges
140
296
  };
141
297
  }
142
- function addEffectSelfLoops(actor, pathPrefix, activeSet) {
143
- const edges = [];
298
+ function addEffectSelfLoops(actor, layout) {
299
+ const selfLoops = [];
144
300
  const effects = actor.options?.effects;
145
301
  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,
302
+ for (const [stateName, effectFns] of Object.entries(effects)) if (Array.isArray(effectFns) && effectFns.length > 0) {
303
+ const loopId = nodeId({
304
+ prefix: layout.pathPrefix,
305
+ name: stateName
306
+ });
307
+ selfLoops.push({
308
+ id: `${loopId}-effect:${stateName}-${loopId}`,
309
+ source: loopId,
310
+ target: loopId,
152
311
  label: `effect:${stateName}`,
153
- isActive: activeSet.has(sid),
154
- isInternal: true
312
+ isActive: layout.activeSet.has(loopId),
313
+ isInternal: true,
314
+ contexts: []
155
315
  });
156
316
  }
157
317
  }
158
- return edges;
318
+ return selfLoops;
159
319
  }
160
- function buildForActor(actor, traversal) {
320
+ function buildForActor(actor, layout) {
161
321
  if (!actor) return {
162
322
  nodes: [],
163
323
  edges: []
164
324
  };
165
- const nodes = addNodesForActor(actor, traversal.pathPrefix, traversal.activeSet);
166
- const edges = addEdgesForActor(actor, traversal);
167
- const region = recurseRegions(actor, traversal);
325
+ const nodes = addNodesForActor(actor, {
326
+ pathPrefix: layout.traversal.pathPrefix,
327
+ activeSet: layout.traversal.activeSet
328
+ });
329
+ const actorEdges = addEdgesForActor(actor, { traversal: layout.traversal });
330
+ const region = recurseRegions(actor, { traversal: layout.traversal });
168
331
  nodes.push(...region.nodes);
169
- edges.push(...region.edges);
170
- edges.push(...addEffectSelfLoops(actor, traversal.pathPrefix, traversal.activeSet));
332
+ actorEdges.push(...region.edges);
333
+ actorEdges.push(...addEffectSelfLoops(actor, {
334
+ pathPrefix: layout.traversal.pathPrefix,
335
+ activeSet: layout.traversal.activeSet
336
+ }));
171
337
  return {
172
338
  nodes,
173
- edges
339
+ edges: actorEdges
174
340
  };
175
341
  }
176
- function addInitialNode(actor, nodes, edges) {
177
- const initialName = actor.options?.initial?.name;
342
+ function addInitialNode(actor, layout) {
343
+ const initialName = parseInitialName(actor.options);
178
344
  if (!initialName) return;
179
- const initNodeId = nodeId("", INITIAL_NODE_ID);
180
- const targetId = nodeId("", initialName);
181
- nodes.push({
345
+ const initNodeId = INITIAL_NODE_ID;
346
+ const targetId = nodeId({
347
+ prefix: "",
348
+ name: initialName
349
+ });
350
+ layout.nodes.push({
182
351
  id: initNodeId,
183
352
  label: "",
184
353
  isActive: false,
185
354
  isFinal: false,
186
355
  isInitial: true
187
356
  });
188
- edges.push({
357
+ layout.graphEdges.push({
189
358
  id: `${initNodeId}->${targetId}`,
190
359
  source: initNodeId,
191
360
  target: targetId,
192
361
  label: "",
193
- isActive: true
362
+ isActive: true,
363
+ contexts: []
194
364
  });
195
365
  }
196
366
  function collectActorsFromSnapshot(snapshot) {
197
367
  const activeSet = /* @__PURE__ */ new Set();
198
- collectActiveStates(snapshot, "", activeSet);
368
+ collectActiveStates(snapshot, {
369
+ prefix: "",
370
+ activeSet
371
+ });
199
372
  return activeSet;
200
373
  }
201
- function collectNamedContexts(options) {
202
- return options?.sampleContexts ?? (options?.sampleContext ? { default: options.sampleContext } : void 0);
374
+ function collectNamedContexts(named) {
375
+ return named?.sampleContexts ?? (named?.sampleContext ? { default: named.sampleContext } : void 0);
203
376
  }
204
377
  function buildGraph(actor, options) {
205
378
  if (!actor) return {
206
379
  nodes: [],
207
380
  edges: []
208
381
  };
209
- return Either.match(Either.from(() => {
382
+ return Either.getOrElse(Either.from(() => {
210
383
  const activeSet = collectActorsFromSnapshot(actor.snapshot());
211
- const contexts = collectNamedContexts(options) ?? { default: { ...actor.context } };
212
- const { nodes, edges } = buildForActor(actor, {
384
+ const contexts = collectNamedContexts(options) ?? { default: parseContextRecord(actor.context) };
385
+ const built = buildForActor(actor, { traversal: {
213
386
  actor,
214
387
  pathPrefix: "",
215
388
  activeSet,
216
389
  internalIds: options?.internalIds,
217
390
  contexts,
218
391
  contextNames: Object.keys(contexts)
392
+ } });
393
+ addInitialNode(actor, {
394
+ nodes: built.nodes,
395
+ graphEdges: built.edges
219
396
  });
220
- addInitialNode(actor, nodes, edges);
221
397
  return {
222
- nodes,
223
- edges
398
+ nodes: built.nodes,
399
+ edges: built.edges
224
400
  };
225
- }), (error) => {
226
- throw error;
227
- }, (result) => result);
401
+ }), { onLeft: () => ({
402
+ nodes: [],
403
+ edges: []
404
+ }) });
228
405
  }
229
406
  //#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;
270
- }
271
- };
272
- //#endregion
273
407
  //#region src/instrument.ts
274
- function wrapWithProxy(actor, history) {
275
- const origSend = actor.send.bind(actor);
408
+ function wrapWithProxy(actor, wrapped) {
409
+ function forwardOn(event, options) {
410
+ return actor.on(event, options);
411
+ }
276
412
  return {
277
413
  get history() {
278
- return history;
414
+ return wrapped.history;
279
415
  },
280
416
  get state() {
281
417
  return actor.state;
@@ -293,179 +429,228 @@ function wrapWithProxy(actor, history) {
293
429
  return actor.options;
294
430
  },
295
431
  send(event) {
296
- trackSendEvent(history, event);
297
- origSend(event);
432
+ trackSendEvent({
433
+ history: wrapped.history,
434
+ sentEvent: event
435
+ });
436
+ actor.send(event);
298
437
  },
299
438
  snapshot() {
300
439
  return actor.snapshot();
301
440
  },
302
- on: actor.on.bind(actor),
441
+ on: forwardOn,
442
+ inject: actor.inject.bind(actor),
443
+ dispose: actor.dispose.bind(actor),
303
444
  recover: actor.recover.bind(actor),
304
445
  settled: actor.settled.bind(actor)
305
446
  };
306
447
  }
307
- function recordTransition(history, rec) {
308
- history.append({
448
+ function recordTransition(recorder) {
449
+ recorder.history.append({
309
450
  type: "transition",
310
451
  data: {
311
- from: rec.from,
312
- event: rec.event,
313
- to: rec.to,
314
- timestamp: Date.now()
452
+ from: recorder.record.from,
453
+ event: recorder.record.event,
454
+ to: recorder.record.to
315
455
  }
316
456
  });
317
- if (!rec.transitioned) return;
318
- history.append({
457
+ if (!recorder.record.transitioned) return;
458
+ recorder.history.append({
319
459
  type: "state_visit",
320
- data: {
321
- stateName: rec.to,
322
- timestamp: Date.now()
323
- }
460
+ data: { stateName: recorder.record.to }
324
461
  });
325
- if ((rec.effects[rec.to] ?? []).length > 0) history.append({
462
+ for (const effectName of recorder.record.effectNames) recorder.history.append({
326
463
  type: "effect",
327
464
  data: {
328
- stateName: rec.to,
329
- timestamp: Date.now()
465
+ stateName: recorder.record.to,
466
+ effectName
330
467
  }
331
468
  });
332
469
  }
333
- function trackSendEvent(history, event) {
334
- const eventId = event?.type ?? "unknown";
335
- history.append({
470
+ function trackSendEvent(tracking) {
471
+ const eventType = typeof tracking.sentEvent === "object" && tracking.sentEvent !== null && "type" in tracking.sentEvent ? tracking.sentEvent.type : "unknown";
472
+ tracking.history.append({
336
473
  type: "send",
337
- data: {
338
- event: eventId,
339
- timestamp: Date.now()
340
- }
474
+ data: { event: String(eventType) }
341
475
  });
342
476
  }
477
+ function prefixId(identity) {
478
+ return identity.prefix ? `${identity.prefix}.${identity.name}` : identity.name;
479
+ }
343
480
  function instrument(actor) {
344
- const history = new History();
345
- history.append({
481
+ const history = createHistory();
482
+ const wrapped = wrapWithProxy(actor, { history });
483
+ attach(actor, {
484
+ history,
485
+ prefix: ""
486
+ });
487
+ return wrapped;
488
+ }
489
+ function attach(childActor, branch) {
490
+ branch.history.append({
346
491
  type: "state_visit",
347
- data: {
348
- stateName: actor.state.name,
349
- timestamp: Date.now()
350
- }
492
+ data: { stateName: prefixId({
493
+ prefix: branch.prefix,
494
+ name: childActor.state.name
495
+ }) }
351
496
  });
352
- const wrapped = wrapWithProxy(actor, history);
353
- const effectsByState = actor.options?.effects ?? {};
354
- actor.on("transition", ({ event, from, to, transitioned }) => {
355
- recordTransition(history, {
356
- from,
357
- to,
358
- event: event.type,
359
- transitioned,
360
- effects: effectsByState
497
+ childActor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
498
+ recordTransition({
499
+ history: branch.history,
500
+ record: {
501
+ from: prefixId({
502
+ prefix: branch.prefix,
503
+ name: from
504
+ }),
505
+ to: prefixId({
506
+ prefix: branch.prefix,
507
+ name: to
508
+ }),
509
+ event: event.type,
510
+ transitioned,
511
+ effectNames: effects
512
+ }
361
513
  });
514
+ } });
515
+ for (const [regionName, regionChild] of Object.entries(childActor.regions ?? {})) attach(regionChild, {
516
+ history: branch.history,
517
+ prefix: prefixId({
518
+ prefix: branch.prefix,
519
+ name: regionName
520
+ })
362
521
  });
363
- return wrapped;
364
522
  }
365
523
  //#endregion
366
524
  //#region src/algorithms.ts
367
525
  function buildAdjacency(graph) {
368
- const adj = {};
369
- for (const node of graph.nodes) adj[node.id] = [];
370
- for (const edge of graph.edges) if (adj[edge.source]) adj[edge.source].push({
526
+ const adjacency = {};
527
+ for (const node of graph.nodes) adjacency[node.id] = [];
528
+ for (const edge of graph.edges) if (adjacency[edge.source]) adjacency[edge.source].push({
371
529
  target: edge.target,
372
530
  label: edge.label
373
531
  });
374
- return adj;
532
+ return adjacency;
375
533
  }
376
- function bfsWalk(adj, start, visit) {
534
+ function bfsWalk(adjacency, walk) {
377
535
  const visited = {};
378
- const queue = [start];
536
+ const queue = [walk.start];
379
537
  while (queue.length > 0) {
380
538
  const current = queue.shift();
381
- if (visited[current]) continue;
539
+ if (current === void 0 || visited[current]) continue;
382
540
  visited[current] = true;
383
- if (visit(current)) break;
384
- const neighbors = adj[current] || [];
385
- for (let i = 0; i < neighbors.length; i++) {
386
- const target = neighbors[i].target;
387
- if (!visited[target]) queue.push(target);
388
- }
541
+ if (walk.visit(current)) break;
542
+ for (const neighbor of adjacency[current] ?? []) if (!visited[neighbor.target]) queue.push(neighbor.target);
389
543
  }
390
544
  return visited;
391
545
  }
392
- function reachable(graph, fromId, toId) {
393
- return bfsWalk(buildAdjacency(graph), fromId, (node) => node === toId)[toId] === true;
546
+ function reachable(graph, endpoints) {
547
+ return bfsWalk(buildAdjacency(graph), {
548
+ start: endpoints.fromId,
549
+ visit: (nodeId) => nodeId === endpoints.toId
550
+ })[endpoints.toId] === true;
394
551
  }
395
- function allPaths(graph, fromId, toId) {
396
- const adj = buildAdjacency(graph);
552
+ function allPaths(graph, endpoints) {
553
+ const adjacency = buildAdjacency(graph);
397
554
  const results = [];
398
555
  const visited = {};
399
- function dfs(current, path) {
400
- if (current === toId) {
401
- results.push(path.slice());
556
+ function dfs(cursor) {
557
+ if (cursor.nodeId === endpoints.toId) {
558
+ results.push(cursor.path.slice());
402
559
  return;
403
560
  }
404
- if (visited[current]) return;
405
- visited[current] = true;
406
- const neighbors = adj[current] || [];
407
- for (let i = 0; i < neighbors.length; i++) {
408
- path.push(neighbors[i].target);
409
- dfs(neighbors[i].target, path);
410
- path.pop();
561
+ if (visited[cursor.nodeId]) return;
562
+ visited[cursor.nodeId] = true;
563
+ for (const neighbor of adjacency[cursor.nodeId] ?? []) {
564
+ cursor.path.push(neighbor.target);
565
+ dfs({
566
+ nodeId: neighbor.target,
567
+ path: cursor.path
568
+ });
569
+ cursor.path.pop();
411
570
  }
412
- delete visited[current];
571
+ delete visited[cursor.nodeId];
413
572
  }
414
- dfs(fromId, [fromId]);
573
+ dfs({
574
+ nodeId: endpoints.fromId,
575
+ path: [endpoints.fromId]
576
+ });
415
577
  return results;
416
578
  }
417
579
  function findCycles(graph) {
418
- const adj = buildAdjacency(graph);
580
+ const adjacency = buildAdjacency(graph);
419
581
  const cycles = [];
582
+ const seen = /* @__PURE__ */ new Set();
420
583
  const visited = {};
421
584
  const inStack = {};
422
585
  const path = [];
423
- function dfs(node) {
424
- if (inStack[node]) {
425
- const cycleStart = path.indexOf(node);
426
- if (cycleStart !== -1) cycles.push(path.slice(cycleStart).concat(node));
586
+ /**
587
+ * A cycle ends with its start node (e.g. the list a, b, a). The body is
588
+ * every element but the last. Rotate the body so it starts at the
589
+ * lexicographically smallest id, giving every rotation of a cycle the same
590
+ * canonical key.
591
+ */
592
+ function rotateToMin(cycleNodes) {
593
+ const body = cycleNodes.slice(0, -1);
594
+ let minIndex = 0;
595
+ let minValue = body[0] ?? "";
596
+ for (const [index, candidate] of body.entries()) if (candidate < minValue) {
597
+ minValue = candidate;
598
+ minIndex = index;
599
+ }
600
+ return body.slice(minIndex).concat(body.slice(0, minIndex)).join(":");
601
+ }
602
+ function dfs(nodeId) {
603
+ if (inStack[nodeId]) {
604
+ const cycleStart = path.indexOf(nodeId);
605
+ if (cycleStart !== -1) {
606
+ const cycle = path.slice(cycleStart).concat(nodeId);
607
+ const key = rotateToMin(cycle);
608
+ if (!seen.has(key)) {
609
+ seen.add(key);
610
+ cycles.push(cycle);
611
+ }
612
+ }
427
613
  return;
428
614
  }
429
- if (visited[node]) return;
430
- visited[node] = true;
431
- inStack[node] = true;
432
- path.push(node);
433
- const neighbors = adj[node] || [];
434
- for (let i = 0; i < neighbors.length; i++) dfs(neighbors[i].target);
615
+ if (visited[nodeId]) return;
616
+ visited[nodeId] = true;
617
+ inStack[nodeId] = true;
618
+ path.push(nodeId);
619
+ for (const neighbor of adjacency[nodeId] ?? []) dfs(neighbor.target);
435
620
  path.pop();
436
- delete inStack[node];
621
+ delete inStack[nodeId];
622
+ delete visited[nodeId];
437
623
  }
438
624
  for (const node of graph.nodes) dfs(node.id);
439
625
  return cycles;
440
626
  }
441
- function unreachableNodes(graph, fromId) {
442
- const visited = bfsWalk(buildAdjacency(graph), fromId, () => false);
627
+ function unreachableNodes(graph, options) {
628
+ const visited = bfsWalk(buildAdjacency(graph), {
629
+ start: options.fromId,
630
+ visit: () => false
631
+ });
443
632
  const result = [];
444
633
  for (const node of graph.nodes) if (!visited[node.id]) result.push(node.id);
445
634
  return result;
446
635
  }
447
- function shortestPath(graph, fromId, toId) {
448
- const adj = buildAdjacency(graph);
636
+ function shortestPath(graph, endpoints) {
637
+ const adjacency = buildAdjacency(graph);
449
638
  const visited = {};
450
639
  const queue = [{
451
- node: fromId,
452
- path: [fromId]
640
+ node: endpoints.fromId,
641
+ path: [endpoints.fromId]
453
642
  }];
454
643
  while (queue.length > 0) {
455
- const { node: current, path } = queue.shift();
456
- if (current === toId) return path;
457
- if (visited[current]) continue;
458
- visited[current] = true;
459
- const neighbors = adj[current] || [];
460
- for (let i = 0; i < neighbors.length; i++) {
461
- const target = neighbors[i].target;
462
- if (!visited[target]) queue.push({
463
- node: target,
464
- path: path.concat(target)
465
- });
466
- }
644
+ const candidate = queue.shift();
645
+ if (candidate === void 0) break;
646
+ if (candidate.node === endpoints.toId) return candidate.path;
647
+ if (visited[candidate.node]) continue;
648
+ visited[candidate.node] = true;
649
+ for (const neighbor of adjacency[candidate.node] ?? []) if (!visited[neighbor.target]) queue.push({
650
+ node: neighbor.target,
651
+ path: candidate.path.concat(neighbor.target)
652
+ });
467
653
  }
468
- return null;
469
654
  }
470
655
  //#endregion
471
- export { History, INITIAL_NODE_ID, allPaths, buildGraph, collectActiveStates, findCycles, instrument, reachable, shortestPath, unreachableNodes };
656
+ export { INITIAL_NODE_ID, allPaths, buildGraph, collectActiveStates, createHistory, findCycles, instrument, parseContextRecord, parseInitialName, parseInternalEventIds, parseStates, parseTransitionMap, reachable, shortestPath, unreachableNodes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantaq/traversal",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Graph extraction, traversal algorithms, and runtime tracking for mantaq actors.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,14 +20,14 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@mantaq/core": "0.2.0",
24
- "@mantaq/utils": "0.0.2"
23
+ "@mantaq/core": "0.4.0",
24
+ "@mantaq/utils": "0.0.4"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.2.0",
28
28
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
29
29
  "@vitest/coverage-v8": "4.1.10",
30
- "typescript": "^7.0.2",
30
+ "typescript": "6.0.3",
31
31
  "vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
32
32
  "vite-plus": "0.2.8"
33
33
  },