@typeonce/effect-machine 0.2.0 → 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 +145 -29
- package/dist/AtomMachine.d.ts +8 -6
- package/dist/AtomMachine.d.ts.map +1 -1
- package/dist/AtomMachine.js +1 -1
- package/dist/AtomMachine.js.map +1 -1
- package/dist/ClusterMachine.d.ts +5 -5
- package/dist/ClusterMachine.d.ts.map +1 -1
- package/dist/ClusterMachine.js +2 -2
- package/dist/ClusterMachine.js.map +1 -1
- package/dist/Machine.d.ts +561 -130
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +140 -35
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machineErrors.d.ts +2 -2
- package/dist/internal/machineErrors.d.ts.map +1 -1
- package/dist/internal/machineModel.d.ts +46 -3
- package/dist/internal/machineModel.d.ts.map +1 -1
- package/dist/internal/machineModel.js +508 -14
- package/dist/internal/machineModel.js.map +1 -1
- package/dist/internal/machinePlanner.d.ts.map +1 -1
- package/dist/internal/machinePlanner.js +186 -10
- package/dist/internal/machinePlanner.js.map +1 -1
- package/dist/internal/machineProcess.d.ts.map +1 -1
- package/dist/internal/machineProcess.js.map +1 -1
- package/dist/internal/machineRuntime.js.map +1 -1
- package/docs/agent-guide.md +104 -9
- package/package.json +5 -4
|
@@ -10,7 +10,169 @@ import { hasProperty } from "effect/Predicate";
|
|
|
10
10
|
import * as Schema from "effect/Schema";
|
|
11
11
|
import { MachineSchemaDecodeError, MachineSchemaEncodeError } from "./machineErrors.js";
|
|
12
12
|
export const TargetTypeId = "~effect/Machine/Target";
|
|
13
|
+
export const TargetSnapshotTypeId = Symbol("effect/Machine/TargetSnapshot");
|
|
14
|
+
export const StateInputTypeId = Symbol("effect/Machine/StateInput");
|
|
15
|
+
export const StateConstructionTypeId = Symbol("effect/Machine/StateConstruction");
|
|
16
|
+
export const HistoryTargetTypeId = Symbol("effect/Machine/HistoryTarget");
|
|
17
|
+
const validateHistoryRecordControl = (machine, record) => {
|
|
18
|
+
const ancestry = new Set(getPathToRoot(machine, record.parent));
|
|
19
|
+
const visit = (path) => {
|
|
20
|
+
const node = getNode(machine, path);
|
|
21
|
+
if (node.type === "compound") {
|
|
22
|
+
const children = node.children.filter((child) => record.active.has(child));
|
|
23
|
+
if (children.length !== 1) {
|
|
24
|
+
throw new Error(`Machine history expected compound state "${path}" to retain one active child`);
|
|
25
|
+
}
|
|
26
|
+
if (record.mode === "deep")
|
|
27
|
+
visit(children[0]);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (node.type === "parallel") {
|
|
31
|
+
for (const child of node.children) {
|
|
32
|
+
if (!record.active.has(child)) {
|
|
33
|
+
throw new Error(`Machine history expected parallel state "${path}" to retain region "${child}"`);
|
|
34
|
+
}
|
|
35
|
+
if (record.mode === "deep")
|
|
36
|
+
visit(child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
visit(record.parent);
|
|
41
|
+
for (const path of record.active) {
|
|
42
|
+
if (!ancestry.has(path) && !isPathInSubtree(path, record.parent)) {
|
|
43
|
+
throw new Error(`Machine history contains state "${path}" outside parent "${record.parent}"`);
|
|
44
|
+
}
|
|
45
|
+
if (record.mode === "shallow" && isDescendantOf(path, record.parent) &&
|
|
46
|
+
getNode(machine, path).parent !== record.parent) {
|
|
47
|
+
throw new Error(`Machine shallow history contains deep descendant "${path}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const historyFromSnapshot = (machine, snapshot) => {
|
|
52
|
+
const history = new Map();
|
|
53
|
+
for (const [path, entry] of Object.entries(snapshot.history ?? {})) {
|
|
54
|
+
const historyNode = getNode(machine, path);
|
|
55
|
+
if (historyNode.type !== "history" || historyNode.parent === undefined || historyNode.history !== entry.mode) {
|
|
56
|
+
throw new Error(`Machine snapshot contains invalid history record "${path}"`);
|
|
57
|
+
}
|
|
58
|
+
const active = new Set();
|
|
59
|
+
const values = new Map();
|
|
60
|
+
for (const activePath of entry.active) {
|
|
61
|
+
if (active.has(activePath) || !Object.prototype.hasOwnProperty.call(entry.values, activePath)) {
|
|
62
|
+
throw new Error(`Machine snapshot contains invalid remembered state "${activePath}"`);
|
|
63
|
+
}
|
|
64
|
+
const node = getNode(machine, activePath);
|
|
65
|
+
if (node.type === "history" ||
|
|
66
|
+
!(isPathInSubtree(activePath, historyNode.parent) ||
|
|
67
|
+
getPathToRoot(machine, historyNode.parent).includes(activePath)) ||
|
|
68
|
+
!Schema.is(getStateNodeSchema(node))(entry.values[activePath])) {
|
|
69
|
+
throw new Error(`Machine snapshot contains invalid remembered value for "${activePath}"`);
|
|
70
|
+
}
|
|
71
|
+
active.add(activePath);
|
|
72
|
+
values.set(activePath, entry.values[activePath]);
|
|
73
|
+
}
|
|
74
|
+
if (!active.has(historyNode.parent) || Object.keys(entry.values).length !== active.size) {
|
|
75
|
+
throw new Error(`Machine snapshot contains incomplete history record "${path}"`);
|
|
76
|
+
}
|
|
77
|
+
history.set(path, {
|
|
78
|
+
mode: entry.mode,
|
|
79
|
+
parent: historyNode.parent,
|
|
80
|
+
active,
|
|
81
|
+
values
|
|
82
|
+
});
|
|
83
|
+
validateHistoryRecordControl(machine, history.get(path));
|
|
84
|
+
}
|
|
85
|
+
return history;
|
|
86
|
+
};
|
|
87
|
+
const historyFromSnapshotEffect = Effect.fnUntraced(function* (machine, snapshot) {
|
|
88
|
+
const history = new Map();
|
|
89
|
+
for (const [path, entry] of Object.entries(snapshot.history ?? {})) {
|
|
90
|
+
const historyNode = machine.stateNodes.byPath.get(path);
|
|
91
|
+
if (historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined ||
|
|
92
|
+
historyNode.history !== entry.mode) {
|
|
93
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
94
|
+
machineId: machine.id,
|
|
95
|
+
boundary: "history",
|
|
96
|
+
state: path,
|
|
97
|
+
cause: Cause.die(new Error(`Machine snapshot contains invalid history record "${path}"`))
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
const active = new Set();
|
|
101
|
+
const values = new Map();
|
|
102
|
+
for (const activePath of entry.active) {
|
|
103
|
+
const node = machine.stateNodes.byPath.get(activePath);
|
|
104
|
+
if (active.has(activePath) || node === undefined || node.type === "history" ||
|
|
105
|
+
!Object.prototype.hasOwnProperty.call(entry.values, activePath) ||
|
|
106
|
+
!(isPathInSubtree(activePath, historyNode.parent) ||
|
|
107
|
+
getPathToRoot(machine, historyNode.parent).includes(activePath))) {
|
|
108
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
109
|
+
machineId: machine.id,
|
|
110
|
+
boundary: "history",
|
|
111
|
+
state: activePath,
|
|
112
|
+
cause: Cause.die(new Error(`Machine snapshot contains invalid remembered state "${activePath}"`))
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
active.add(activePath);
|
|
116
|
+
values.set(activePath, yield* decodeBoundary(machine, getStateNodeSchema(node), entry.values[activePath], {
|
|
117
|
+
boundary: "history",
|
|
118
|
+
state: activePath
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
if (!active.has(historyNode.parent) || Object.keys(entry.values).length !== active.size) {
|
|
122
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
123
|
+
machineId: machine.id,
|
|
124
|
+
boundary: "history",
|
|
125
|
+
state: path,
|
|
126
|
+
cause: Cause.die(new Error(`Machine snapshot contains incomplete history record "${path}"`))
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
history.set(path, {
|
|
130
|
+
mode: entry.mode,
|
|
131
|
+
parent: historyNode.parent,
|
|
132
|
+
active,
|
|
133
|
+
values
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
validateHistoryRecordControl(machine, history.get(path));
|
|
137
|
+
}
|
|
138
|
+
catch (cause) {
|
|
139
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
140
|
+
machineId: machine.id,
|
|
141
|
+
boundary: "history",
|
|
142
|
+
state: path,
|
|
143
|
+
cause: Cause.die(cause)
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return history;
|
|
148
|
+
});
|
|
149
|
+
const historyToSnapshot = (history) => {
|
|
150
|
+
const entries = {};
|
|
151
|
+
for (const [path, record] of history) {
|
|
152
|
+
entries[path] = {
|
|
153
|
+
mode: record.mode,
|
|
154
|
+
active: Array.from(record.active),
|
|
155
|
+
values: Object.fromEntries(record.values)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
};
|
|
160
|
+
export const makeHistoryTarget = (path, parent) => ({
|
|
161
|
+
[HistoryTargetTypeId]: HistoryTargetTypeId,
|
|
162
|
+
path,
|
|
163
|
+
parent
|
|
164
|
+
});
|
|
165
|
+
export const isHistoryTarget = (u) => hasProperty(u, HistoryTargetTypeId);
|
|
13
166
|
export const getStateNodeDefinition = (path, definition) => {
|
|
167
|
+
if (!Schema.isSchema(definition) && definition.type === "history") {
|
|
168
|
+
return {
|
|
169
|
+
schema: undefined,
|
|
170
|
+
output: undefined,
|
|
171
|
+
type: "history",
|
|
172
|
+
initial: undefined,
|
|
173
|
+
states: undefined
|
|
174
|
+
};
|
|
175
|
+
}
|
|
14
176
|
if (Schema.isSchema(definition)) {
|
|
15
177
|
return {
|
|
16
178
|
schema: definition,
|
|
@@ -78,11 +240,20 @@ export const compileStateNodes = (states) => {
|
|
|
78
240
|
parent,
|
|
79
241
|
children: [],
|
|
80
242
|
initial: definition.initial === undefined ? undefined : `${path}.${definition.initial}`,
|
|
243
|
+
history: definition.type === "history"
|
|
244
|
+
? (tree[key].history === "deep" ? "deep" : "shallow")
|
|
245
|
+
: undefined,
|
|
81
246
|
order
|
|
82
247
|
};
|
|
83
248
|
byPath.set(path, node);
|
|
84
|
-
paths.push(path);
|
|
85
249
|
order += 1;
|
|
250
|
+
if (definition.type === "history") {
|
|
251
|
+
if (parent === undefined) {
|
|
252
|
+
throw new Error(`Machine history state "${path}" must belong to a parent state`);
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
paths.push(path);
|
|
86
257
|
if (definition.states !== undefined) {
|
|
87
258
|
const children = compile(definition.states, path);
|
|
88
259
|
if (node.type === "compound" && (node.initial === undefined || !children.includes(node.initial))) {
|
|
@@ -101,11 +272,27 @@ export const compileStateNodes = (states) => {
|
|
|
101
272
|
};
|
|
102
273
|
export const makeTarget = (path, value, options) => ({
|
|
103
274
|
[TargetTypeId]: TargetTypeId,
|
|
275
|
+
[TargetSnapshotTypeId]: options?.snapshot,
|
|
104
276
|
path,
|
|
105
277
|
value,
|
|
106
278
|
values: options?.values
|
|
107
279
|
});
|
|
108
280
|
export const isTarget = (u) => hasProperty(u, TargetTypeId);
|
|
281
|
+
export const makeStateInput = (input) => ({
|
|
282
|
+
[StateInputTypeId]: StateInputTypeId,
|
|
283
|
+
input
|
|
284
|
+
});
|
|
285
|
+
const isStateInput = (u) => hasProperty(u, StateInputTypeId);
|
|
286
|
+
export const markStateConstruction = (value) => {
|
|
287
|
+
if ((typeof value === "object" && value !== null) || typeof value === "function") {
|
|
288
|
+
Object.defineProperty(value, StateConstructionTypeId, {
|
|
289
|
+
value: StateConstructionTypeId,
|
|
290
|
+
enumerable: false
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
};
|
|
295
|
+
export const isStateConstruction = (u) => hasProperty(u, StateConstructionTypeId);
|
|
109
296
|
export const isSnapshot = (u) => hasProperty(u, "path") && hasProperty(u, "value");
|
|
110
297
|
export const getSnapshotByPath = (snapshot, path, parents) => {
|
|
111
298
|
if (snapshot.path === path) {
|
|
@@ -170,7 +357,14 @@ export const decodeEmit = (machine, event) => {
|
|
|
170
357
|
const eventName = getEventName(event);
|
|
171
358
|
return decodeBoundary(machine, getProtocolSchemas(machine).emit, event, eventName === undefined ? { boundary: "emit" } : { boundary: "emit", event: eventName });
|
|
172
359
|
};
|
|
173
|
-
export const decodeStateValue = (machine, node, value) =>
|
|
360
|
+
export const decodeStateValue = (machine, node, value) => isStateInput(value)
|
|
361
|
+
? getStateNodeSchema(node).makeEffect(value.input).pipe(Effect.mapError((cause) => new MachineSchemaDecodeError({
|
|
362
|
+
machineId: machine.id,
|
|
363
|
+
boundary: "state",
|
|
364
|
+
state: node.path,
|
|
365
|
+
cause
|
|
366
|
+
})))
|
|
367
|
+
: decodeBoundary(machine, getStateNodeSchema(node), value, { boundary: "state", state: node.path });
|
|
174
368
|
export const decodeOutputValue = (machine, node, value) => node.output === undefined
|
|
175
369
|
? Effect.succeed(value)
|
|
176
370
|
: decodeBoundary(machine, node.output, value, { boundary: "output", state: node.path });
|
|
@@ -181,6 +375,12 @@ export const getNode = (machine, path) => {
|
|
|
181
375
|
}
|
|
182
376
|
return node;
|
|
183
377
|
};
|
|
378
|
+
export const getStateNodeSchema = (node) => {
|
|
379
|
+
if (node.schema === undefined || node.type === "history") {
|
|
380
|
+
throw new Error(`Machine history state "${node.path}" has no active value schema`);
|
|
381
|
+
}
|
|
382
|
+
return node.schema;
|
|
383
|
+
};
|
|
184
384
|
export const hasOwn = (u, key) => Object.prototype.hasOwnProperty.call(u, key);
|
|
185
385
|
export const isDescendantOf = (path, ancestor) => path.startsWith(`${ancestor}.`);
|
|
186
386
|
export const isPathInSubtree = (path, ancestor) => path === ancestor || isDescendantOf(path, ancestor);
|
|
@@ -224,7 +424,7 @@ export const getRootPath = (machine, configuration) => {
|
|
|
224
424
|
};
|
|
225
425
|
export const getActiveValue = (configuration, path) => {
|
|
226
426
|
if (!configuration.values.has(path)) {
|
|
227
|
-
throw new Error(`Machine expected active state "${path}" to have a value`);
|
|
427
|
+
throw new Error(`Machine expected active state "${path}" to have a value (available: ${Array.from(configuration.values.keys()).join(", ")})`);
|
|
228
428
|
}
|
|
229
429
|
return configuration.values.get(path);
|
|
230
430
|
};
|
|
@@ -288,6 +488,19 @@ export const snapshotFromConfiguration = (machine, configuration) => {
|
|
|
288
488
|
;
|
|
289
489
|
snapshot.completed = completed;
|
|
290
490
|
}
|
|
491
|
+
if (configuration.history.size > 0) {
|
|
492
|
+
Object.assign(snapshot, { history: historyToSnapshot(configuration.history) });
|
|
493
|
+
}
|
|
494
|
+
return snapshot;
|
|
495
|
+
};
|
|
496
|
+
/** Creates a targetable subtree snapshot while carrying machine-level history
|
|
497
|
+
* metadata on that subtree root. This is used when a nested history target
|
|
498
|
+
* must preserve active ancestors and unaffected parallel regions. */
|
|
499
|
+
export const snapshotFromConfigurationAtPath = (machine, configuration, path) => {
|
|
500
|
+
const snapshot = snapshotFromPath(machine, configuration, path);
|
|
501
|
+
if (configuration.history.size > 0) {
|
|
502
|
+
Object.assign(snapshot, { history: historyToSnapshot(configuration.history) });
|
|
503
|
+
}
|
|
291
504
|
return snapshot;
|
|
292
505
|
};
|
|
293
506
|
export const configurationFromSnapshot = (machine, snapshot) => {
|
|
@@ -296,7 +509,7 @@ export const configurationFromSnapshot = (machine, snapshot) => {
|
|
|
296
509
|
const snapshotOutputs = snapshot.completed;
|
|
297
510
|
const visit = (current) => {
|
|
298
511
|
const node = getNode(machine, String(current.path));
|
|
299
|
-
if (!Schema.is(node
|
|
512
|
+
if (!Schema.is(getStateNodeSchema(node))(current.value)) {
|
|
300
513
|
throw new Error(`Machine expected snapshot for "${node.path}" to match its schema`);
|
|
301
514
|
}
|
|
302
515
|
active.add(node.path);
|
|
@@ -339,7 +552,7 @@ export const configurationFromSnapshot = (machine, snapshot) => {
|
|
|
339
552
|
}
|
|
340
553
|
}
|
|
341
554
|
}
|
|
342
|
-
return { active, values, outputs };
|
|
555
|
+
return { active, values, outputs, history: historyFromSnapshot(machine, snapshot) };
|
|
343
556
|
};
|
|
344
557
|
export const normalizeConfiguration = (machine, state) => configurationFromSnapshot(machine, state);
|
|
345
558
|
export const configurationFromSnapshotEffect = Effect.fnUntraced(function* (machine, snapshot) {
|
|
@@ -389,7 +602,12 @@ export const configurationFromSnapshotEffect = Effect.fnUntraced(function* (mach
|
|
|
389
602
|
}
|
|
390
603
|
}
|
|
391
604
|
}
|
|
392
|
-
return {
|
|
605
|
+
return {
|
|
606
|
+
active,
|
|
607
|
+
values,
|
|
608
|
+
outputs,
|
|
609
|
+
history: yield* historyFromSnapshotEffect(machine, snapshot)
|
|
610
|
+
};
|
|
393
611
|
});
|
|
394
612
|
export const normalizeConfigurationEffect = (machine, state) => configurationFromSnapshotEffect(machine, state).pipe(Effect.catchCause((cause) => {
|
|
395
613
|
const error = Cause.findErrorOption(cause);
|
|
@@ -419,6 +637,85 @@ export const validateInitialConfiguration = (machine, configuration) => {
|
|
|
419
637
|
}
|
|
420
638
|
}
|
|
421
639
|
};
|
|
640
|
+
/** Capture every history register whose owning parent exits in this microstep.
|
|
641
|
+
* The control record is deliberately independent from effects/actions: it is
|
|
642
|
+
* part of the logical snapshot and is therefore preserved by pure planning. */
|
|
643
|
+
export const captureHistory = (machine, current, next, exitPaths) => {
|
|
644
|
+
if (exitPaths.length === 0) {
|
|
645
|
+
return next;
|
|
646
|
+
}
|
|
647
|
+
const exited = new Set(exitPaths);
|
|
648
|
+
const history = new Map(next.history);
|
|
649
|
+
for (const node of machine.stateNodes.byPath.values()) {
|
|
650
|
+
if (node.type !== "history" || node.parent === undefined || !exited.has(node.parent)) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const mode = node.history === "deep" ? "deep" : "shallow";
|
|
654
|
+
const active = new Set();
|
|
655
|
+
for (const ancestor of getPathToRoot(machine, node.parent)) {
|
|
656
|
+
if (current.active.has(ancestor)) {
|
|
657
|
+
active.add(ancestor);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const path of current.active) {
|
|
661
|
+
if (path === node.parent ||
|
|
662
|
+
(mode === "deep" && isDescendantOf(path, node.parent)) ||
|
|
663
|
+
(mode === "shallow" && getNode(machine, path).parent === node.parent)) {
|
|
664
|
+
active.add(path);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const values = new Map();
|
|
668
|
+
for (const path of active) {
|
|
669
|
+
values.set(path, getActiveValue(current, path));
|
|
670
|
+
}
|
|
671
|
+
history.set(node.path, {
|
|
672
|
+
mode,
|
|
673
|
+
parent: node.parent,
|
|
674
|
+
active,
|
|
675
|
+
values
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
active: next.active,
|
|
680
|
+
values: next.values,
|
|
681
|
+
outputs: next.outputs,
|
|
682
|
+
history
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
export const getHistoryRecord = (configuration, path) => configuration.history.get(path);
|
|
686
|
+
/** Builds the remembered portion of a configuration. Shallow records are
|
|
687
|
+
* intentionally incomplete below their direct child; the planner completes
|
|
688
|
+
* them by invoking only the required typed initializers. */
|
|
689
|
+
export const configurationFromHistoryRecord = (machine, current, record) => {
|
|
690
|
+
const active = new Set(record.active);
|
|
691
|
+
const values = new Map(record.values);
|
|
692
|
+
const outputs = new Map();
|
|
693
|
+
const ancestors = getPathToRoot(machine, record.parent);
|
|
694
|
+
const ancestorSet = new Set(ancestors);
|
|
695
|
+
// A history transition can occur while an ancestor parallel state remains
|
|
696
|
+
// active. Its unaffected regions retain their current configuration.
|
|
697
|
+
for (const ancestor of ancestors) {
|
|
698
|
+
const node = getNode(machine, ancestor);
|
|
699
|
+
if (node.type !== "parallel") {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
for (const child of node.children) {
|
|
703
|
+
if (ancestorSet.has(child) || record.active.has(child) || !current.active.has(child)) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
for (const path of current.active) {
|
|
707
|
+
if (isPathInSubtree(path, child)) {
|
|
708
|
+
active.add(path);
|
|
709
|
+
if (current.values.has(path))
|
|
710
|
+
values.set(path, current.values.get(path));
|
|
711
|
+
if (current.outputs.has(path))
|
|
712
|
+
outputs.set(path, current.outputs.get(path));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return { active, values, outputs, history: current.history };
|
|
718
|
+
};
|
|
422
719
|
export const configurationFromTargetPathEffect = Effect.fnUntraced(function* (machine, current, path, value, providedValues) {
|
|
423
720
|
const node = getNode(machine, path);
|
|
424
721
|
const active = new Set();
|
|
@@ -466,14 +763,77 @@ export const configurationFromTargetPathEffect = Effect.fnUntraced(function* (ma
|
|
|
466
763
|
if (node.type === "compound" || node.type === "parallel") {
|
|
467
764
|
throw new Error(`Machine target "${node.path}" must include an active child state`);
|
|
468
765
|
}
|
|
469
|
-
return {
|
|
766
|
+
return {
|
|
767
|
+
active,
|
|
768
|
+
values,
|
|
769
|
+
outputs,
|
|
770
|
+
history: current.history
|
|
771
|
+
};
|
|
772
|
+
});
|
|
773
|
+
export const configurationFromTargetSnapshotEffect = Effect.fnUntraced(function* (machine, current, snapshot, providedValues) {
|
|
774
|
+
const subtree = yield* configurationFromSnapshotEffect(machine, snapshot);
|
|
775
|
+
const active = new Set(subtree.active);
|
|
776
|
+
const values = new Map(subtree.values);
|
|
777
|
+
const outputs = new Map(subtree.outputs);
|
|
778
|
+
const paths = getPathToRoot(machine, String(snapshot.path));
|
|
779
|
+
const pathSet = new Set(paths);
|
|
780
|
+
for (const ancestor of paths.slice(0, -1)) {
|
|
781
|
+
const node = getNode(machine, ancestor);
|
|
782
|
+
active.add(ancestor);
|
|
783
|
+
if (providedValues !== undefined && hasOwn(providedValues, ancestor)) {
|
|
784
|
+
values.set(ancestor, yield* decodeStateValue(machine, node, providedValues[ancestor]));
|
|
785
|
+
}
|
|
786
|
+
else if (current.values.has(ancestor)) {
|
|
787
|
+
values.set(ancestor, current.values.get(ancestor));
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
throw new Error(`Machine target "${snapshot.path}" requires a value for ancestor state "${ancestor}"`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
for (const ancestor of paths.slice(0, -1)) {
|
|
794
|
+
const ancestorNode = getNode(machine, ancestor);
|
|
795
|
+
if (ancestorNode.type === "parallel") {
|
|
796
|
+
for (const child of ancestorNode.children) {
|
|
797
|
+
if (pathSet.has(child) || !current.active.has(child)) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
for (const activePath of current.active) {
|
|
801
|
+
if (isPathInSubtree(activePath, child)) {
|
|
802
|
+
active.add(activePath);
|
|
803
|
+
if (current.values.has(activePath)) {
|
|
804
|
+
values.set(activePath, current.values.get(activePath));
|
|
805
|
+
}
|
|
806
|
+
if (current.outputs.has(activePath)) {
|
|
807
|
+
outputs.set(activePath, current.outputs.get(activePath));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
active,
|
|
816
|
+
values,
|
|
817
|
+
outputs,
|
|
818
|
+
history: new Map([...current.history, ...subtree.history])
|
|
819
|
+
};
|
|
470
820
|
});
|
|
471
821
|
export const normalizeTargetConfigurationEffect = (machine, current, target) => {
|
|
472
822
|
if (isTarget(target)) {
|
|
823
|
+
const snapshot = target[TargetSnapshotTypeId];
|
|
824
|
+
if (snapshot !== undefined) {
|
|
825
|
+
if (String(snapshot.path) !== String(target.path)) {
|
|
826
|
+
throw new Error(`Machine expected target snapshot path to be "${target.path}"`);
|
|
827
|
+
}
|
|
828
|
+
return configurationFromTargetSnapshotEffect(machine, current, snapshot, target.values);
|
|
829
|
+
}
|
|
473
830
|
return configurationFromTargetPathEffect(machine, current, target.path, target.value, target.values);
|
|
474
831
|
}
|
|
475
832
|
if (isSnapshot(target)) {
|
|
476
|
-
return normalizeConfigurationEffect(machine, target)
|
|
833
|
+
return normalizeConfigurationEffect(machine, target).pipe(Effect.map((configuration) => ({
|
|
834
|
+
...configuration,
|
|
835
|
+
history: new Map([...current.history, ...configuration.history])
|
|
836
|
+
})));
|
|
477
837
|
}
|
|
478
838
|
throw new Error("Machine expected transition target to be a snapshot or target builder result");
|
|
479
839
|
};
|
|
@@ -593,7 +953,8 @@ export const completeConfigurationEffect = Effect.fnUntraced(function* (machine,
|
|
|
593
953
|
const completed = {
|
|
594
954
|
active: configuration.active,
|
|
595
955
|
values: configuration.values,
|
|
596
|
-
outputs
|
|
956
|
+
outputs,
|
|
957
|
+
history: configuration.history
|
|
597
958
|
};
|
|
598
959
|
for (const path of Array.from(completed.active).sort((left, right) => {
|
|
599
960
|
const depth = pathDepth(machine, right) - pathDepth(machine, left);
|
|
@@ -612,6 +973,11 @@ const EncodedSnapshotSchema = Schema.Struct({
|
|
|
612
973
|
completed: Schema.optional(Schema.Array(Schema.Struct({
|
|
613
974
|
path: Schema.String,
|
|
614
975
|
output: Schema.optional(Schema.Unknown)
|
|
976
|
+
}))),
|
|
977
|
+
history: Schema.optional(Schema.Record(Schema.String, Schema.Struct({
|
|
978
|
+
mode: Schema.Literals(["shallow", "deep"]),
|
|
979
|
+
active: Schema.Array(Schema.String),
|
|
980
|
+
values: Schema.Record(Schema.String, Schema.Unknown)
|
|
615
981
|
})))
|
|
616
982
|
});
|
|
617
983
|
const encodeBoundary = (machine, schema, value, options) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new MachineSchemaEncodeError({
|
|
@@ -669,7 +1035,7 @@ const failDecodeCause = (machine, cause) => {
|
|
|
669
1035
|
export const encodeSnapshot = (machine, snapshot) => Effect.gen(function* () {
|
|
670
1036
|
const configuration = yield* normalizeConfigurationEffect(machine, snapshot).pipe(Effect.mapError((error) => new MachineSchemaEncodeError({
|
|
671
1037
|
machineId: machine.id,
|
|
672
|
-
boundary: error.boundary === "state"
|
|
1038
|
+
boundary: error.boundary === "state" || error.boundary === "history" ? error.boundary : "configuration",
|
|
673
1039
|
...(error.state === undefined ? {} : { state: error.state }),
|
|
674
1040
|
cause: error.cause
|
|
675
1041
|
})));
|
|
@@ -688,7 +1054,7 @@ export const encodeSnapshot = (machine, snapshot) => Effect.gen(function* () {
|
|
|
688
1054
|
const node = getNode(machine, path);
|
|
689
1055
|
active.push({
|
|
690
1056
|
path,
|
|
691
|
-
value: yield* encodeBoundary(machine, node
|
|
1057
|
+
value: yield* encodeBoundary(machine, getStateNodeSchema(node), getActiveValue(configuration, path), {
|
|
692
1058
|
boundary: "state",
|
|
693
1059
|
state: path
|
|
694
1060
|
})
|
|
@@ -708,10 +1074,62 @@ export const encodeSnapshot = (machine, snapshot) => Effect.gen(function* () {
|
|
|
708
1074
|
...(encodedOutput === undefined ? {} : { output: encodedOutput })
|
|
709
1075
|
});
|
|
710
1076
|
}
|
|
1077
|
+
const history = {};
|
|
1078
|
+
for (const [historyPath, record] of Array.from(configuration.history).sort(([left], [right]) => left.localeCompare(right))) {
|
|
1079
|
+
const historyNode = machine.stateNodes.byPath.get(historyPath);
|
|
1080
|
+
if (historyNode === undefined || historyNode.type !== "history" || historyNode.parent !== record.parent ||
|
|
1081
|
+
historyNode.history !== record.mode) {
|
|
1082
|
+
return yield* Effect.fail(new MachineSchemaEncodeError({
|
|
1083
|
+
machineId: machine.id,
|
|
1084
|
+
boundary: "history",
|
|
1085
|
+
state: historyPath,
|
|
1086
|
+
cause: Cause.die(new Error(`Machine snapshot contains invalid history record "${historyPath}"`))
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
validateHistoryRecordControl(machine, record);
|
|
1091
|
+
}
|
|
1092
|
+
catch (cause) {
|
|
1093
|
+
return yield* Effect.fail(new MachineSchemaEncodeError({
|
|
1094
|
+
machineId: machine.id,
|
|
1095
|
+
boundary: "history",
|
|
1096
|
+
state: historyPath,
|
|
1097
|
+
cause: Cause.die(cause)
|
|
1098
|
+
}));
|
|
1099
|
+
}
|
|
1100
|
+
const encodedValues = {};
|
|
1101
|
+
for (const path of record.active) {
|
|
1102
|
+
const stateNode = machine.stateNodes.byPath.get(path);
|
|
1103
|
+
if (stateNode === undefined || stateNode.type === "history" || !record.values.has(path) ||
|
|
1104
|
+
!(isPathInSubtree(path, record.parent) || getPathToRoot(machine, record.parent).includes(path))) {
|
|
1105
|
+
return yield* Effect.fail(new MachineSchemaEncodeError({
|
|
1106
|
+
machineId: machine.id,
|
|
1107
|
+
boundary: "history",
|
|
1108
|
+
state: path,
|
|
1109
|
+
cause: Cause.die(new Error(`Machine snapshot contains invalid remembered state "${path}"`))
|
|
1110
|
+
}));
|
|
1111
|
+
}
|
|
1112
|
+
encodedValues[path] = yield* encodeBoundary(machine, getStateNodeSchema(stateNode), record.values.get(path), { boundary: "history", state: path });
|
|
1113
|
+
}
|
|
1114
|
+
if (record.values.size !== record.active.size) {
|
|
1115
|
+
return yield* Effect.fail(new MachineSchemaEncodeError({
|
|
1116
|
+
machineId: machine.id,
|
|
1117
|
+
boundary: "history",
|
|
1118
|
+
state: historyPath,
|
|
1119
|
+
cause: Cause.die(new Error(`Machine history record "${historyPath}" contains values outside its paths`))
|
|
1120
|
+
}));
|
|
1121
|
+
}
|
|
1122
|
+
history[historyPath] = {
|
|
1123
|
+
mode: record.mode,
|
|
1124
|
+
active: Array.from(record.active).sort((left, right) => compareDocumentOrder(machine, left, right)),
|
|
1125
|
+
values: encodedValues
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
711
1128
|
return {
|
|
712
1129
|
_tag: "MachineSnapshot",
|
|
713
1130
|
active,
|
|
714
|
-
...(completed.length === 0 ? {} : { completed })
|
|
1131
|
+
...(completed.length === 0 ? {} : { completed }),
|
|
1132
|
+
...(Object.keys(history).length === 0 ? {} : { history })
|
|
715
1133
|
};
|
|
716
1134
|
}).pipe(Effect.catchCause((cause) => failEncodeCause(machine, cause)));
|
|
717
1135
|
export const decodeSnapshot = (machine, encoded) => Effect.gen(function* () {
|
|
@@ -728,15 +1146,91 @@ export const decodeSnapshot = (machine, encoded) => Effect.gen(function* () {
|
|
|
728
1146
|
}
|
|
729
1147
|
const node = getNode(machine, entry.path);
|
|
730
1148
|
active.add(entry.path);
|
|
731
|
-
values.set(entry.path, yield* decodeEncodedBoundary(machine, node
|
|
1149
|
+
values.set(entry.path, yield* decodeEncodedBoundary(machine, getStateNodeSchema(node), entry.value, {
|
|
732
1150
|
boundary: "state",
|
|
733
1151
|
state: entry.path
|
|
734
1152
|
}));
|
|
735
1153
|
}
|
|
1154
|
+
const history = new Map();
|
|
1155
|
+
for (const [historyPath, encodedRecord] of Object.entries(decoded.history ?? {})) {
|
|
1156
|
+
const historyNode = machine.stateNodes.byPath.get(historyPath);
|
|
1157
|
+
if (historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined ||
|
|
1158
|
+
historyNode.history !== encodedRecord.mode) {
|
|
1159
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
1160
|
+
machineId: machine.id,
|
|
1161
|
+
boundary: "history",
|
|
1162
|
+
state: historyPath,
|
|
1163
|
+
cause: Cause.die(new Error(`Machine encoded snapshot contains invalid history record "${historyPath}"`))
|
|
1164
|
+
}));
|
|
1165
|
+
}
|
|
1166
|
+
const rememberedActive = new Set();
|
|
1167
|
+
const rememberedValues = new Map();
|
|
1168
|
+
for (const path of encodedRecord.active) {
|
|
1169
|
+
if (rememberedActive.has(path)) {
|
|
1170
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
1171
|
+
machineId: machine.id,
|
|
1172
|
+
boundary: "history",
|
|
1173
|
+
state: path,
|
|
1174
|
+
cause: Cause.die(new Error(`Machine encoded history contains duplicate state "${path}"`))
|
|
1175
|
+
}));
|
|
1176
|
+
}
|
|
1177
|
+
const stateNode = machine.stateNodes.byPath.get(path);
|
|
1178
|
+
if (stateNode === undefined || stateNode.type === "history" ||
|
|
1179
|
+
!Object.prototype.hasOwnProperty.call(encodedRecord.values, path) ||
|
|
1180
|
+
!(isPathInSubtree(path, historyNode.parent) || getPathToRoot(machine, historyNode.parent).includes(path))) {
|
|
1181
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
1182
|
+
machineId: machine.id,
|
|
1183
|
+
boundary: "history",
|
|
1184
|
+
state: path,
|
|
1185
|
+
cause: Cause.die(new Error(`Machine encoded snapshot contains invalid remembered state "${path}"`))
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
rememberedActive.add(path);
|
|
1189
|
+
rememberedValues.set(path, yield* decodeEncodedBoundary(machine, getStateNodeSchema(stateNode), encodedRecord.values[path], {
|
|
1190
|
+
boundary: "history",
|
|
1191
|
+
state: path
|
|
1192
|
+
}));
|
|
1193
|
+
}
|
|
1194
|
+
if (Object.keys(encodedRecord.values).length !== rememberedActive.size) {
|
|
1195
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
1196
|
+
machineId: machine.id,
|
|
1197
|
+
boundary: "history",
|
|
1198
|
+
state: historyPath,
|
|
1199
|
+
cause: Cause.die(new Error(`Machine encoded history "${historyPath}" contains values outside its paths`))
|
|
1200
|
+
}));
|
|
1201
|
+
}
|
|
1202
|
+
if (!rememberedActive.has(historyNode.parent)) {
|
|
1203
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
1204
|
+
machineId: machine.id,
|
|
1205
|
+
boundary: "history",
|
|
1206
|
+
state: historyPath,
|
|
1207
|
+
cause: Cause.die(new Error(`Machine encoded history "${historyPath}" does not contain its parent state`))
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
const record = {
|
|
1211
|
+
mode: encodedRecord.mode,
|
|
1212
|
+
parent: historyNode.parent,
|
|
1213
|
+
active: rememberedActive,
|
|
1214
|
+
values: rememberedValues
|
|
1215
|
+
};
|
|
1216
|
+
try {
|
|
1217
|
+
validateHistoryRecordControl(machine, record);
|
|
1218
|
+
}
|
|
1219
|
+
catch (cause) {
|
|
1220
|
+
return yield* Effect.fail(new MachineSchemaDecodeError({
|
|
1221
|
+
machineId: machine.id,
|
|
1222
|
+
boundary: "history",
|
|
1223
|
+
state: historyPath,
|
|
1224
|
+
cause: Cause.die(cause)
|
|
1225
|
+
}));
|
|
1226
|
+
}
|
|
1227
|
+
history.set(historyPath, record);
|
|
1228
|
+
}
|
|
736
1229
|
const configuration = {
|
|
737
1230
|
active,
|
|
738
1231
|
values,
|
|
739
|
-
outputs: new Map()
|
|
1232
|
+
outputs: new Map(),
|
|
1233
|
+
history
|
|
740
1234
|
};
|
|
741
1235
|
const snapshot = validateEncodedConfiguration(machine, configuration);
|
|
742
1236
|
const completions = [];
|