@typeonce/effect-machine 0.1.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.
@@ -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,32 +272,99 @@ 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");
297
+ export const getSnapshotByPath = (snapshot, path, parents) => {
298
+ if (snapshot.path === path) {
299
+ return Option.some(snapshot);
300
+ }
301
+ if (!path.startsWith(`${snapshot.path}.`)) {
302
+ return Option.none();
303
+ }
304
+ if (parents !== undefined) {
305
+ parents[snapshot.path] = snapshot.value;
306
+ }
307
+ if (hasProperty(snapshot, "state") && isSnapshot(snapshot.state)) {
308
+ return getSnapshotByPath(snapshot.state, path, parents);
309
+ }
310
+ if (hasProperty(snapshot, "states") && typeof snapshot.states === "object" && snapshot.states !== null) {
311
+ for (const child of Object.values(snapshot.states)) {
312
+ if (isSnapshot(child)) {
313
+ const result = getSnapshotByPath(child, path, parents);
314
+ if (Option.isSome(result)) {
315
+ return result;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ return Option.none();
321
+ };
322
+ const MachineProtocolTypeId = Symbol.for("effect/Machine/protocol");
323
+ const getProtocolSchemas = (machine) => {
324
+ const protocol = machine[MachineProtocolTypeId];
325
+ if (protocol === undefined) {
326
+ throw new Error("Machine protocol is unavailable");
327
+ }
328
+ return protocol;
329
+ };
330
+ const setProtocolSchemas = (machine, protocol) => {
331
+ Object.defineProperty(machine, MachineProtocolTypeId, {
332
+ value: protocol,
333
+ enumerable: false
334
+ });
335
+ };
336
+ export const setProtocol = (machine) => {
337
+ setProtocolSchemas(machine, {
338
+ event: Schema.Union([...machine.events, ...machine.internalEvents]),
339
+ emit: Schema.Union(machine.emits)
340
+ });
341
+ };
342
+ export const copyProtocol = (source, target) => setProtocolSchemas(target, getProtocolSchemas(source));
110
343
  export const getEventName = (event) => hasProperty(event, "_tag") ? String(event._tag) : undefined;
111
- export const decodeBoundary = Effect.fnUntraced(function* (machine, schema, value, options) {
112
- return yield* Schema.decodeUnknownEffect(Schema.toType(schema))(value).pipe(Effect.mapError((cause) => new MachineSchemaDecodeError({
113
- machineId: machine.id,
114
- boundary: options.boundary,
115
- cause,
116
- ...(options.state === undefined ? {} : { state: options.state }),
117
- ...(options.event === undefined ? {} : { event: options.event })
118
- })));
119
- });
344
+ export const decodeBoundary = (machine, schema, value, options) => Schema.decodeUnknownEffect(Schema.toType(schema))(value).pipe(Effect.mapError((cause) => new MachineSchemaDecodeError({
345
+ machineId: machine.id,
346
+ boundary: options.boundary,
347
+ cause,
348
+ ...(options.state === undefined ? {} : { state: options.state }),
349
+ ...(options.event === undefined ? {} : { event: options.event })
350
+ })));
120
351
  export const decodeInput = (machine, schema, value) => decodeBoundary(machine, schema, value, { boundary: "input" });
121
352
  export const decodeEvent = (machine, event) => {
122
353
  const eventName = getEventName(event);
123
- return decodeBoundary(machine, Schema.Union(machine.events), event, eventName === undefined ? { boundary: "event" } : { boundary: "event", event: eventName });
354
+ return decodeBoundary(machine, getProtocolSchemas(machine).event, event, eventName === undefined ? { boundary: "event" } : { boundary: "event", event: eventName });
124
355
  };
125
356
  export const decodeEmit = (machine, event) => {
126
357
  const eventName = getEventName(event);
127
- return decodeBoundary(machine, Schema.Union(machine.emits), event, eventName === undefined ? { boundary: "emit" } : { boundary: "emit", event: eventName });
358
+ return decodeBoundary(machine, getProtocolSchemas(machine).emit, event, eventName === undefined ? { boundary: "emit" } : { boundary: "emit", event: eventName });
128
359
  };
129
- export const decodeStateValue = (machine, node, value) => decodeBoundary(machine, node.schema, value, { boundary: "state", state: node.path });
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 });
130
368
  export const decodeOutputValue = (machine, node, value) => node.output === undefined
131
369
  ? Effect.succeed(value)
132
370
  : decodeBoundary(machine, node.output, value, { boundary: "output", state: node.path });
@@ -137,6 +375,12 @@ export const getNode = (machine, path) => {
137
375
  }
138
376
  return node;
139
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
+ };
140
384
  export const hasOwn = (u, key) => Object.prototype.hasOwnProperty.call(u, key);
141
385
  export const isDescendantOf = (path, ancestor) => path.startsWith(`${ancestor}.`);
142
386
  export const isPathInSubtree = (path, ancestor) => path === ancestor || isDescendantOf(path, ancestor);
@@ -180,7 +424,7 @@ export const getRootPath = (machine, configuration) => {
180
424
  };
181
425
  export const getActiveValue = (configuration, path) => {
182
426
  if (!configuration.values.has(path)) {
183
- 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(", ")})`);
184
428
  }
185
429
  return configuration.values.get(path);
186
430
  };
@@ -193,6 +437,10 @@ export const getParentValues = (machine, configuration, path) => {
193
437
  }
194
438
  return parents;
195
439
  };
440
+ export const getParentValue = (machine, configuration, path) => {
441
+ const parent = getNode(machine, path).parent;
442
+ return parent === undefined ? undefined : getActiveValue(configuration, parent);
443
+ };
196
444
  export const getInitialEntryPaths = (machine, configuration) => {
197
445
  const visit = (path) => {
198
446
  if (!configuration.active.has(path)) {
@@ -234,15 +482,25 @@ export const snapshotFromPath = (machine, configuration, path) => {
234
482
  };
235
483
  export const snapshotFromConfiguration = (machine, configuration) => {
236
484
  const snapshot = snapshotFromPath(machine, configuration, getRootPath(machine, configuration));
237
- const root = getRootPath(machine, configuration);
238
- const retainPartialOutputs = !isActiveFinalNode(machine, configuration, root);
239
485
  const completed = Array.from(configuration.outputs)
240
- .filter(([path]) => retainPartialOutputs || hasCompletionHandler(machine, path))
241
486
  .map(([path, output]) => ({ path, output }));
242
487
  if (completed.length > 0) {
243
488
  ;
244
489
  snapshot.completed = completed;
245
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
+ }
246
504
  return snapshot;
247
505
  };
248
506
  export const configurationFromSnapshot = (machine, snapshot) => {
@@ -251,7 +509,7 @@ export const configurationFromSnapshot = (machine, snapshot) => {
251
509
  const snapshotOutputs = snapshot.completed;
252
510
  const visit = (current) => {
253
511
  const node = getNode(machine, String(current.path));
254
- if (!Schema.is(node.schema)(current.value)) {
512
+ if (!Schema.is(getStateNodeSchema(node))(current.value)) {
255
513
  throw new Error(`Machine expected snapshot for "${node.path}" to match its schema`);
256
514
  }
257
515
  active.add(node.path);
@@ -294,7 +552,7 @@ export const configurationFromSnapshot = (machine, snapshot) => {
294
552
  }
295
553
  }
296
554
  }
297
- return { active, values, outputs };
555
+ return { active, values, outputs, history: historyFromSnapshot(machine, snapshot) };
298
556
  };
299
557
  export const normalizeConfiguration = (machine, state) => configurationFromSnapshot(machine, state);
300
558
  export const configurationFromSnapshotEffect = Effect.fnUntraced(function* (machine, snapshot) {
@@ -344,7 +602,12 @@ export const configurationFromSnapshotEffect = Effect.fnUntraced(function* (mach
344
602
  }
345
603
  }
346
604
  }
347
- return { active, values, outputs };
605
+ return {
606
+ active,
607
+ values,
608
+ outputs,
609
+ history: yield* historyFromSnapshotEffect(machine, snapshot)
610
+ };
348
611
  });
349
612
  export const normalizeConfigurationEffect = (machine, state) => configurationFromSnapshotEffect(machine, state).pipe(Effect.catchCause((cause) => {
350
613
  const error = Cause.findErrorOption(cause);
@@ -374,6 +637,85 @@ export const validateInitialConfiguration = (machine, configuration) => {
374
637
  }
375
638
  }
376
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
+ };
377
719
  export const configurationFromTargetPathEffect = Effect.fnUntraced(function* (machine, current, path, value, providedValues) {
378
720
  const node = getNode(machine, path);
379
721
  const active = new Set();
@@ -421,20 +763,83 @@ export const configurationFromTargetPathEffect = Effect.fnUntraced(function* (ma
421
763
  if (node.type === "compound" || node.type === "parallel") {
422
764
  throw new Error(`Machine target "${node.path}" must include an active child state`);
423
765
  }
424
- return { active, values, outputs };
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
+ };
425
820
  });
426
821
  export const normalizeTargetConfigurationEffect = (machine, current, target) => {
427
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
+ }
428
830
  return configurationFromTargetPathEffect(machine, current, target.path, target.value, target.values);
429
831
  }
430
832
  if (isSnapshot(target)) {
431
- return normalizeConfigurationEffect(machine, target);
833
+ return normalizeConfigurationEffect(machine, target).pipe(Effect.map((configuration) => ({
834
+ ...configuration,
835
+ history: new Map([...current.history, ...configuration.history])
836
+ })));
432
837
  }
433
838
  throw new Error("Machine expected transition target to be a snapshot or target builder result");
434
839
  };
435
840
  export const getStateConfigByPath = (machine, path) => machine.handlers[path];
436
841
  export const getActiveChildPath = (machine, configuration, path) => getNode(machine, path).children.find((child) => configuration.active.has(child));
437
- export const isDirectFinalPath = (machine, path) => getNode(machine, path).type === "final" || getStateConfigByPath(machine, path)?.type === "final";
842
+ export const isDirectFinalPath = (machine, path) => getNode(machine, path).type === "final";
438
843
  export const hasCompletionHandler = (machine, path) => getStateConfigByPath(machine, path)?.onDone !== undefined;
439
844
  export const isActiveFinalNode = (machine, configuration, path) => {
440
845
  if (!configuration.active.has(path)) {
@@ -478,6 +883,7 @@ export const resolveFinalOutputEffect = Effect.fnUntraced(function* (machine, co
478
883
  const node = getNode(machine, path);
479
884
  const output = getStateConfigByPath(machine, path)?.output?.({
480
885
  state: getActiveValue(configuration, path),
886
+ parent: getParentValue(machine, configuration, path),
481
887
  parents: getParentValues(machine, configuration, path),
482
888
  event,
483
889
  outputs
@@ -488,6 +894,13 @@ export const completeActiveFinalNodeEffect = Effect.fnUntraced(function* (machin
488
894
  if (!configuration.active.has(path)) {
489
895
  return undefined;
490
896
  }
897
+ if (outputs.has(path)) {
898
+ return {
899
+ path,
900
+ output: outputs.get(path),
901
+ isNew: false
902
+ };
903
+ }
491
904
  const node = getNode(machine, path);
492
905
  if (node.type === "compound") {
493
906
  const child = getActiveChildPath(machine, configuration, path);
@@ -540,7 +953,8 @@ export const completeConfigurationEffect = Effect.fnUntraced(function* (machine,
540
953
  const completed = {
541
954
  active: configuration.active,
542
955
  values: configuration.values,
543
- outputs
956
+ outputs,
957
+ history: configuration.history
544
958
  };
545
959
  for (const path of Array.from(completed.active).sort((left, right) => {
546
960
  const depth = pathDepth(machine, right) - pathDepth(machine, left);
@@ -559,6 +973,11 @@ const EncodedSnapshotSchema = Schema.Struct({
559
973
  completed: Schema.optional(Schema.Array(Schema.Struct({
560
974
  path: Schema.String,
561
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)
562
981
  })))
563
982
  });
564
983
  const encodeBoundary = (machine, schema, value, options) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new MachineSchemaEncodeError({
@@ -616,7 +1035,7 @@ const failDecodeCause = (machine, cause) => {
616
1035
  export const encodeSnapshot = (machine, snapshot) => Effect.gen(function* () {
617
1036
  const configuration = yield* normalizeConfigurationEffect(machine, snapshot).pipe(Effect.mapError((error) => new MachineSchemaEncodeError({
618
1037
  machineId: machine.id,
619
- boundary: error.boundary === "state" ? "state" : "configuration",
1038
+ boundary: error.boundary === "state" || error.boundary === "history" ? error.boundary : "configuration",
620
1039
  ...(error.state === undefined ? {} : { state: error.state }),
621
1040
  cause: error.cause
622
1041
  })));
@@ -635,7 +1054,7 @@ export const encodeSnapshot = (machine, snapshot) => Effect.gen(function* () {
635
1054
  const node = getNode(machine, path);
636
1055
  active.push({
637
1056
  path,
638
- value: yield* encodeBoundary(machine, node.schema, getActiveValue(configuration, path), {
1057
+ value: yield* encodeBoundary(machine, getStateNodeSchema(node), getActiveValue(configuration, path), {
639
1058
  boundary: "state",
640
1059
  state: path
641
1060
  })
@@ -655,10 +1074,62 @@ export const encodeSnapshot = (machine, snapshot) => Effect.gen(function* () {
655
1074
  ...(encodedOutput === undefined ? {} : { output: encodedOutput })
656
1075
  });
657
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
+ }
658
1128
  return {
659
1129
  _tag: "MachineSnapshot",
660
1130
  active,
661
- ...(completed.length === 0 ? {} : { completed })
1131
+ ...(completed.length === 0 ? {} : { completed }),
1132
+ ...(Object.keys(history).length === 0 ? {} : { history })
662
1133
  };
663
1134
  }).pipe(Effect.catchCause((cause) => failEncodeCause(machine, cause)));
664
1135
  export const decodeSnapshot = (machine, encoded) => Effect.gen(function* () {
@@ -675,15 +1146,91 @@ export const decodeSnapshot = (machine, encoded) => Effect.gen(function* () {
675
1146
  }
676
1147
  const node = getNode(machine, entry.path);
677
1148
  active.add(entry.path);
678
- values.set(entry.path, yield* decodeEncodedBoundary(machine, node.schema, entry.value, {
1149
+ values.set(entry.path, yield* decodeEncodedBoundary(machine, getStateNodeSchema(node), entry.value, {
679
1150
  boundary: "state",
680
1151
  state: entry.path
681
1152
  }));
682
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
+ }
683
1229
  const configuration = {
684
1230
  active,
685
1231
  values,
686
- outputs: new Map()
1232
+ outputs: new Map(),
1233
+ history
687
1234
  };
688
1235
  const snapshot = validateEncodedConfiguration(machine, configuration);
689
1236
  const completions = [];