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