@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 +151 -4
- package/dist/index.d.mts +79 -34
- package/dist/index.mjs +452 -269
- package/package.json +4 -4
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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():
|
|
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:
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
102
|
-
|
|
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,
|
|
105
|
-
|
|
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,
|
|
6
|
-
if (!snapshot
|
|
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(
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
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,
|
|
17
|
-
return states.map((
|
|
18
|
-
const
|
|
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:
|
|
21
|
-
label:
|
|
22
|
-
isActive: activeSet.has(
|
|
23
|
-
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,
|
|
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:
|
|
34
|
-
|
|
144
|
+
context: Context({
|
|
145
|
+
get: () => options.context,
|
|
146
|
+
set: () => {}
|
|
147
|
+
}),
|
|
148
|
+
actor: options.actor
|
|
35
149
|
});
|
|
36
|
-
return {
|
|
37
|
-
targetName: result
|
|
38
|
-
emitNames: result
|
|
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
|
|
43
|
-
const undetermined = !handled.targetName;
|
|
44
|
-
const targetId = handled.targetName ? nodeId(
|
|
45
|
-
|
|
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(
|
|
180
|
+
function processStateTransitions(options) {
|
|
61
181
|
const stateTransitionedEvents = /* @__PURE__ */ new Set();
|
|
62
|
-
for (const [eventId, handler] of Object.entries(stateTransitions)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
81
|
-
for (const [eventId, handler] of Object.entries(anyTransitions)) {
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const handled = Either.getOrElse(invokeHandler(handler,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
106
|
-
|
|
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(
|
|
110
|
-
|
|
111
|
-
const anyTransitions = transitions["Any"];
|
|
112
|
-
const edges
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
264
|
+
return allEdges;
|
|
119
265
|
}
|
|
120
|
-
function addNodesForActor(actor,
|
|
121
|
-
return buildNodesFromStates(actor.options
|
|
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,
|
|
124
|
-
return buildEdgesFromTransitions(
|
|
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,
|
|
279
|
+
function recurseRegions(actor, layout) {
|
|
127
280
|
const nodes = [];
|
|
128
|
-
const
|
|
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(
|
|
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
|
-
|
|
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,
|
|
143
|
-
const
|
|
298
|
+
function addEffectSelfLoops(actor, layout) {
|
|
299
|
+
const selfLoops = [];
|
|
144
300
|
const effects = actor.options?.effects;
|
|
145
301
|
if (effects) {
|
|
146
|
-
for (const [stateName,
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
154
|
-
isInternal: true
|
|
312
|
+
isActive: layout.activeSet.has(loopId),
|
|
313
|
+
isInternal: true,
|
|
314
|
+
contexts: []
|
|
155
315
|
});
|
|
156
316
|
}
|
|
157
317
|
}
|
|
158
|
-
return
|
|
318
|
+
return selfLoops;
|
|
159
319
|
}
|
|
160
|
-
function buildForActor(actor,
|
|
320
|
+
function buildForActor(actor, layout) {
|
|
161
321
|
if (!actor) return {
|
|
162
322
|
nodes: [],
|
|
163
323
|
edges: []
|
|
164
324
|
};
|
|
165
|
-
const nodes = addNodesForActor(actor,
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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,
|
|
177
|
-
const initialName = actor.options
|
|
342
|
+
function addInitialNode(actor, layout) {
|
|
343
|
+
const initialName = parseInitialName(actor.options);
|
|
178
344
|
if (!initialName) return;
|
|
179
|
-
const initNodeId =
|
|
180
|
-
const targetId = nodeId(
|
|
181
|
-
|
|
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
|
-
|
|
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,
|
|
368
|
+
collectActiveStates(snapshot, {
|
|
369
|
+
prefix: "",
|
|
370
|
+
activeSet
|
|
371
|
+
});
|
|
199
372
|
return activeSet;
|
|
200
373
|
}
|
|
201
|
-
function collectNamedContexts(
|
|
202
|
-
return
|
|
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.
|
|
382
|
+
return Either.getOrElse(Either.from(() => {
|
|
210
383
|
const activeSet = collectActorsFromSnapshot(actor.snapshot());
|
|
211
|
-
const contexts = collectNamedContexts(options) ?? { default:
|
|
212
|
-
const
|
|
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
|
-
}), (
|
|
226
|
-
|
|
227
|
-
|
|
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,
|
|
275
|
-
|
|
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(
|
|
297
|
-
|
|
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:
|
|
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(
|
|
310
|
-
history.append({
|
|
448
|
+
function recordTransition(recorder) {
|
|
449
|
+
recorder.history.append({
|
|
311
450
|
type: "transition",
|
|
312
451
|
data: {
|
|
313
|
-
from:
|
|
314
|
-
event:
|
|
315
|
-
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 (!
|
|
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
|
-
|
|
462
|
+
for (const effectName of recorder.record.effectNames) recorder.history.append({
|
|
328
463
|
type: "effect",
|
|
329
464
|
data: {
|
|
330
|
-
stateName:
|
|
331
|
-
|
|
465
|
+
stateName: recorder.record.to,
|
|
466
|
+
effectName
|
|
332
467
|
}
|
|
333
468
|
});
|
|
334
469
|
}
|
|
335
|
-
function trackSendEvent(
|
|
336
|
-
const
|
|
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 =
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
492
|
+
data: { stateName: prefixId({
|
|
493
|
+
prefix: branch.prefix,
|
|
494
|
+
name: childActor.state.name
|
|
495
|
+
}) }
|
|
353
496
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
371
|
-
for (const node of graph.nodes)
|
|
372
|
-
for (const edge of graph.edges) if (
|
|
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
|
|
532
|
+
return adjacency;
|
|
377
533
|
}
|
|
378
|
-
function bfsWalk(
|
|
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
|
|
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,
|
|
395
|
-
return bfsWalk(buildAdjacency(graph),
|
|
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,
|
|
398
|
-
const
|
|
552
|
+
function allPaths(graph, endpoints) {
|
|
553
|
+
const adjacency = buildAdjacency(graph);
|
|
399
554
|
const results = [];
|
|
400
555
|
const visited = {};
|
|
401
|
-
function dfs(
|
|
402
|
-
if (
|
|
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[
|
|
407
|
-
visited[
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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[
|
|
571
|
+
delete visited[cursor.nodeId];
|
|
415
572
|
}
|
|
416
|
-
dfs(
|
|
573
|
+
dfs({
|
|
574
|
+
nodeId: endpoints.fromId,
|
|
575
|
+
path: [endpoints.fromId]
|
|
576
|
+
});
|
|
417
577
|
return results;
|
|
418
578
|
}
|
|
419
579
|
function findCycles(graph) {
|
|
420
|
-
const
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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[
|
|
432
|
-
visited[
|
|
433
|
-
inStack[
|
|
434
|
-
path.push(
|
|
435
|
-
const
|
|
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[
|
|
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,
|
|
444
|
-
const visited = bfsWalk(buildAdjacency(graph),
|
|
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,
|
|
450
|
-
const
|
|
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
|
|
458
|
-
if (
|
|
459
|
-
if (
|
|
460
|
-
visited[
|
|
461
|
-
|
|
462
|
-
for (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
24
|
-
"@mantaq/utils": "0.0.
|
|
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": "
|
|
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
|
},
|