@mantaq/traversal 0.1.2 → 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
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,66 +51,75 @@ 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;
87
- on(event: "error", fn: (info: ErrorInfo) => void): () => void;
88
- on(event: "output", fn: (event: InternalEvent) => void): () => void;
120
+ on<E extends SubscriberEventName>(event: E, options: {
121
+ fn: SubscriberHandler<C, E>;
122
+ }): () => void;
89
123
  recover(target: {
90
124
  state: AnyActor["state"];
91
125
  context: C;
@@ -98,10 +132,21 @@ interface InstrumentedActor<C = Record<string, unknown>> {
98
132
  declare function instrument<C>(actor: AnyActor<C>): InstrumentedActor<C>;
99
133
  //#endregion
100
134
  //#region src/algorithms.d.ts
101
- declare function reachable(graph: ActorGraph, fromId: string, toId: string): boolean;
102
- 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[][];
103
143
  declare function findCycles(graph: ActorGraph): string[][];
104
- declare function unreachableNodes(graph: ActorGraph, fromId: string): string[];
105
- 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;
106
151
  //#endregion
107
- 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,181 +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,
303
442
  inject: actor.inject.bind(actor),
304
443
  dispose: actor.dispose.bind(actor),
305
444
  recover: actor.recover.bind(actor),
306
445
  settled: actor.settled.bind(actor)
307
446
  };
308
447
  }
309
- function recordTransition(history, rec) {
310
- history.append({
448
+ function recordTransition(recorder) {
449
+ recorder.history.append({
311
450
  type: "transition",
312
451
  data: {
313
- from: rec.from,
314
- event: rec.event,
315
- to: rec.to,
316
- timestamp: Date.now()
452
+ from: recorder.record.from,
453
+ event: recorder.record.event,
454
+ to: recorder.record.to
317
455
  }
318
456
  });
319
- if (!rec.transitioned) return;
320
- history.append({
457
+ if (!recorder.record.transitioned) return;
458
+ recorder.history.append({
321
459
  type: "state_visit",
322
- data: {
323
- stateName: rec.to,
324
- timestamp: Date.now()
325
- }
460
+ data: { stateName: recorder.record.to }
326
461
  });
327
- if ((rec.effects[rec.to] ?? []).length > 0) history.append({
462
+ for (const effectName of recorder.record.effectNames) recorder.history.append({
328
463
  type: "effect",
329
464
  data: {
330
- stateName: rec.to,
331
- timestamp: Date.now()
465
+ stateName: recorder.record.to,
466
+ effectName
332
467
  }
333
468
  });
334
469
  }
335
- function trackSendEvent(history, event) {
336
- const eventId = event?.type ?? "unknown";
337
- 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({
338
473
  type: "send",
339
- data: {
340
- event: eventId,
341
- timestamp: Date.now()
342
- }
474
+ data: { event: String(eventType) }
343
475
  });
344
476
  }
477
+ function prefixId(identity) {
478
+ return identity.prefix ? `${identity.prefix}.${identity.name}` : identity.name;
479
+ }
345
480
  function instrument(actor) {
346
- const history = new History();
347
- 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({
348
491
  type: "state_visit",
349
- data: {
350
- stateName: actor.state.name,
351
- timestamp: Date.now()
352
- }
492
+ data: { stateName: prefixId({
493
+ prefix: branch.prefix,
494
+ name: childActor.state.name
495
+ }) }
353
496
  });
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
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
+ }
363
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
+ })
364
521
  });
365
- return wrapped;
366
522
  }
367
523
  //#endregion
368
524
  //#region src/algorithms.ts
369
525
  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({
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({
373
529
  target: edge.target,
374
530
  label: edge.label
375
531
  });
376
- return adj;
532
+ return adjacency;
377
533
  }
378
- function bfsWalk(adj, start, visit) {
534
+ function bfsWalk(adjacency, walk) {
379
535
  const visited = {};
380
- const queue = [start];
536
+ const queue = [walk.start];
381
537
  while (queue.length > 0) {
382
538
  const current = queue.shift();
383
- if (visited[current]) continue;
539
+ if (current === void 0 || visited[current]) continue;
384
540
  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
- }
541
+ if (walk.visit(current)) break;
542
+ for (const neighbor of adjacency[current] ?? []) if (!visited[neighbor.target]) queue.push(neighbor.target);
391
543
  }
392
544
  return visited;
393
545
  }
394
- function reachable(graph, fromId, toId) {
395
- 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;
396
551
  }
397
- function allPaths(graph, fromId, toId) {
398
- const adj = buildAdjacency(graph);
552
+ function allPaths(graph, endpoints) {
553
+ const adjacency = buildAdjacency(graph);
399
554
  const results = [];
400
555
  const visited = {};
401
- function dfs(current, path) {
402
- if (current === toId) {
403
- results.push(path.slice());
556
+ function dfs(cursor) {
557
+ if (cursor.nodeId === endpoints.toId) {
558
+ results.push(cursor.path.slice());
404
559
  return;
405
560
  }
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();
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();
413
570
  }
414
- delete visited[current];
571
+ delete visited[cursor.nodeId];
415
572
  }
416
- dfs(fromId, [fromId]);
573
+ dfs({
574
+ nodeId: endpoints.fromId,
575
+ path: [endpoints.fromId]
576
+ });
417
577
  return results;
418
578
  }
419
579
  function findCycles(graph) {
420
- const adj = buildAdjacency(graph);
580
+ const adjacency = buildAdjacency(graph);
421
581
  const cycles = [];
582
+ const seen = /* @__PURE__ */ new Set();
422
583
  const visited = {};
423
584
  const inStack = {};
424
585
  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));
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
+ }
429
613
  return;
430
614
  }
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);
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);
437
620
  path.pop();
438
- delete inStack[node];
621
+ delete inStack[nodeId];
622
+ delete visited[nodeId];
439
623
  }
440
624
  for (const node of graph.nodes) dfs(node.id);
441
625
  return cycles;
442
626
  }
443
- function unreachableNodes(graph, fromId) {
444
- 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
+ });
445
632
  const result = [];
446
633
  for (const node of graph.nodes) if (!visited[node.id]) result.push(node.id);
447
634
  return result;
448
635
  }
449
- function shortestPath(graph, fromId, toId) {
450
- const adj = buildAdjacency(graph);
636
+ function shortestPath(graph, endpoints) {
637
+ const adjacency = buildAdjacency(graph);
451
638
  const visited = {};
452
639
  const queue = [{
453
- node: fromId,
454
- path: [fromId]
640
+ node: endpoints.fromId,
641
+ path: [endpoints.fromId]
455
642
  }];
456
643
  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
- }
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
+ });
469
653
  }
470
- return null;
471
654
  }
472
655
  //#endregion
473
- 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.2",
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.3.0",
24
- "@mantaq/utils": "0.0.3"
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
  },