@typeonce/effect-machine 0.6.0 → 0.6.1
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/package.json +5 -5
- package/src/Machine.ts +6873 -0
- package/src/index.ts +1 -0
- package/src/internal/machine/activities.ts +108 -0
- package/src/internal/machine/atom.ts +636 -0
- package/src/internal/machine/cluster.ts +394 -0
- package/src/internal/machine/command.ts +58 -0
- package/src/internal/machine/commandRuntime.ts +43 -0
- package/src/internal/machine/configuration.ts +1331 -0
- package/src/internal/machine/errors.ts +87 -0
- package/src/internal/machine/executionPlan.ts +996 -0
- package/src/internal/machine/invocation.ts +119 -0
- package/src/internal/machine/machine.ts +1747 -0
- package/src/internal/machine/planner.ts +1933 -0
- package/src/internal/machine/process.ts +906 -0
- package/src/internal/machine/protocol.ts +322 -0
- package/src/internal/machine/readiness.ts +10 -0
- package/src/internal/machine/runtime.ts +2512 -0
- package/src/internal/machine/serialization.ts +498 -0
- package/src/internal/machine/stateDefinition.ts +270 -0
- package/src/internal/machine/symbols.ts +2 -0
- package/src/internal/machine/topology.ts +479 -0
- package/src/internal/testing/machine/arbitrary.ts +102 -0
- package/src/internal/testing/machine/exploration.ts +331 -0
- package/src/internal/testing/machine/finiteModel.ts +1498 -0
- package/src/internal/testing/machine/invariant.ts +372 -0
- package/src/internal/testing/machine/probe.ts +79 -0
- package/src/internal/testing/machine/referenceModel.ts +1505 -0
- package/src/internal/testing/machine/runtime.ts +1710 -0
- package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
- package/src/internal/testing/machine/trace.ts +150 -0
- package/src/internal/testing/machine/verification.ts +1890 -0
- package/src/testing/MachineTest.ts +2067 -0
- package/src/testing/index.ts +7 -0
- package/src/unstable/cluster/ClusterMachine.ts +390 -0
- package/src/unstable/cluster/index.ts +1 -0
- package/src/unstable/reactivity/AtomMachine.ts +649 -0
- package/src/unstable/reactivity/index.ts +1 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal encoded snapshot serialization.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Cause from "effect/Cause"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as Option from "effect/Option"
|
|
10
|
+
import * as Schema from "effect/Schema"
|
|
11
|
+
import type { Machine } from "../../Machine.js"
|
|
12
|
+
import {
|
|
13
|
+
type ActiveConfiguration,
|
|
14
|
+
compareDocumentOrder,
|
|
15
|
+
configurationFromSnapshot,
|
|
16
|
+
getActiveChildPath,
|
|
17
|
+
getActiveValue,
|
|
18
|
+
getPathToRoot,
|
|
19
|
+
type HistoryRecord,
|
|
20
|
+
isActiveFinalNode,
|
|
21
|
+
isPathInSubtree,
|
|
22
|
+
normalizeConfigurationEffect,
|
|
23
|
+
snapshotFromConfiguration,
|
|
24
|
+
validateHistoryRecordControl
|
|
25
|
+
} from "./configuration.js"
|
|
26
|
+
import { MachineSchemaDecodeError, MachineSchemaEncodeError } from "./errors.js"
|
|
27
|
+
import { decodeBoundary } from "./protocol.js"
|
|
28
|
+
import { getNode, getStateNodeSchema } from "./topology.js"
|
|
29
|
+
|
|
30
|
+
const EncodedSnapshotSchema = Schema.Struct({
|
|
31
|
+
_tag: Schema.Literal("MachineSnapshot"),
|
|
32
|
+
active: Schema.Array(Schema.Struct({
|
|
33
|
+
path: Schema.String,
|
|
34
|
+
value: Schema.Unknown
|
|
35
|
+
})),
|
|
36
|
+
completed: Schema.optional(Schema.Array(Schema.Struct({
|
|
37
|
+
path: Schema.String,
|
|
38
|
+
output: Schema.optional(Schema.Unknown)
|
|
39
|
+
}))),
|
|
40
|
+
history: Schema.optional(Schema.Record(
|
|
41
|
+
Schema.String,
|
|
42
|
+
Schema.Struct({
|
|
43
|
+
mode: Schema.Literals(["shallow", "deep"]),
|
|
44
|
+
active: Schema.Array(Schema.String),
|
|
45
|
+
values: Schema.Record(Schema.String, Schema.Unknown)
|
|
46
|
+
})
|
|
47
|
+
))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const encodeBoundary = (
|
|
51
|
+
machine: Machine.Any,
|
|
52
|
+
schema: Schema.Top,
|
|
53
|
+
value: unknown,
|
|
54
|
+
options: {
|
|
55
|
+
readonly boundary: "state" | "output" | "history"
|
|
56
|
+
readonly state: string
|
|
57
|
+
}
|
|
58
|
+
): Effect.Effect<unknown, MachineSchemaEncodeError, unknown> =>
|
|
59
|
+
Schema.encodeUnknownEffect(schema)(value).pipe(
|
|
60
|
+
Effect.mapError((cause) =>
|
|
61
|
+
new MachineSchemaEncodeError({
|
|
62
|
+
machineId: machine.id,
|
|
63
|
+
boundary: options.boundary,
|
|
64
|
+
state: options.state,
|
|
65
|
+
cause
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const decodeEncodedBoundary = (
|
|
71
|
+
machine: Machine.Any,
|
|
72
|
+
schema: Schema.Top,
|
|
73
|
+
value: unknown,
|
|
74
|
+
options: {
|
|
75
|
+
readonly boundary: "state" | "output" | "history"
|
|
76
|
+
readonly state: string
|
|
77
|
+
}
|
|
78
|
+
): Effect.Effect<unknown, MachineSchemaDecodeError, unknown> =>
|
|
79
|
+
Schema.decodeUnknownEffect(schema)(value).pipe(
|
|
80
|
+
Effect.mapError((cause) =>
|
|
81
|
+
new MachineSchemaDecodeError({
|
|
82
|
+
machineId: machine.id,
|
|
83
|
+
boundary: options.boundary,
|
|
84
|
+
state: options.state,
|
|
85
|
+
cause
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const getCompletionSchema = (
|
|
91
|
+
machine: Machine.Any,
|
|
92
|
+
configuration: ActiveConfiguration,
|
|
93
|
+
path: string
|
|
94
|
+
): Schema.Top => {
|
|
95
|
+
const node = getNode(machine, path)
|
|
96
|
+
if (node.type === "compound") {
|
|
97
|
+
const child = getActiveChildPath(machine, configuration, path)
|
|
98
|
+
if (child === undefined) {
|
|
99
|
+
throw new Error(`Machine expected completed state "${path}" to have an active child`)
|
|
100
|
+
}
|
|
101
|
+
return getCompletionSchema(machine, configuration, child)
|
|
102
|
+
}
|
|
103
|
+
return node.output ?? Schema.Void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Defensively validates and normalizes an in-memory logical snapshot. Unlike
|
|
107
|
+
* the transport decoder this consumes decoded schema values. */
|
|
108
|
+
export const normalizeSnapshotEffect = <const States extends Machine.StateSchemas>(
|
|
109
|
+
machine: Machine.Any,
|
|
110
|
+
snapshot: Machine.Snapshot<States>
|
|
111
|
+
): Effect.Effect<Machine.Snapshot<States>, MachineSchemaDecodeError> =>
|
|
112
|
+
Effect.gen(function*() {
|
|
113
|
+
const configuration = yield* normalizeConfigurationEffect(machine, snapshot)
|
|
114
|
+
const outputs = new Map<string, unknown>()
|
|
115
|
+
const completionPaths = new Set<string>()
|
|
116
|
+
const completions = snapshot.completed ?? []
|
|
117
|
+
if (!Array.isArray(completions)) {
|
|
118
|
+
throw new Error("Machine snapshot completion metadata must be an array")
|
|
119
|
+
}
|
|
120
|
+
for (const completion of completions) {
|
|
121
|
+
if (
|
|
122
|
+
typeof completion !== "object" || completion === null ||
|
|
123
|
+
typeof (completion as { readonly path?: unknown }).path !== "string"
|
|
124
|
+
) {
|
|
125
|
+
throw new Error("Machine snapshot contains malformed completion metadata")
|
|
126
|
+
}
|
|
127
|
+
const path = completion.path
|
|
128
|
+
if (completionPaths.has(path)) {
|
|
129
|
+
throw new Error(`Machine snapshot contains duplicate completion "${path}"`)
|
|
130
|
+
}
|
|
131
|
+
if (!configuration.active.has(path) || !isActiveFinalNode(machine, configuration, path)) {
|
|
132
|
+
throw new Error(`Machine snapshot contains invalid completion "${path}"`)
|
|
133
|
+
}
|
|
134
|
+
completionPaths.add(path)
|
|
135
|
+
outputs.set(
|
|
136
|
+
path,
|
|
137
|
+
yield* decodeBoundary(machine, getCompletionSchema(machine, configuration, path), completion.output, {
|
|
138
|
+
boundary: "output",
|
|
139
|
+
state: path
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return snapshotFromConfiguration<States>(machine, { ...configuration, outputs })
|
|
144
|
+
}).pipe(Effect.catchCause((cause) => failDecodeCause(machine, cause)))
|
|
145
|
+
|
|
146
|
+
const validateEncodedConfiguration = (
|
|
147
|
+
machine: Machine.Any,
|
|
148
|
+
configuration: ActiveConfiguration
|
|
149
|
+
): Machine.Snapshot<any> => {
|
|
150
|
+
const snapshot = snapshotFromConfiguration(machine, configuration)
|
|
151
|
+
const normalized = configurationFromSnapshot(machine, snapshot)
|
|
152
|
+
if (
|
|
153
|
+
normalized.active.size !== configuration.active.size ||
|
|
154
|
+
Array.from(configuration.active).some((path) => !normalized.active.has(path))
|
|
155
|
+
) {
|
|
156
|
+
throw new Error("Machine encoded snapshot contains states outside its active configuration")
|
|
157
|
+
}
|
|
158
|
+
return snapshot
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const failEncodeCause = (
|
|
162
|
+
machine: Machine.Any,
|
|
163
|
+
cause: Cause.Cause<unknown>
|
|
164
|
+
): Effect.Effect<never, MachineSchemaEncodeError> => {
|
|
165
|
+
const error = Cause.findErrorOption(cause)
|
|
166
|
+
return Option.isSome(error) && error.value instanceof MachineSchemaEncodeError
|
|
167
|
+
? Effect.fail(error.value)
|
|
168
|
+
: Effect.fail(
|
|
169
|
+
new MachineSchemaEncodeError({
|
|
170
|
+
machineId: machine.id,
|
|
171
|
+
boundary: "configuration",
|
|
172
|
+
cause
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const failDecodeCause = (
|
|
178
|
+
machine: Machine.Any,
|
|
179
|
+
cause: Cause.Cause<unknown>
|
|
180
|
+
): Effect.Effect<never, MachineSchemaDecodeError> => {
|
|
181
|
+
const error = Cause.findErrorOption(cause)
|
|
182
|
+
return Option.isSome(error) && error.value instanceof MachineSchemaDecodeError
|
|
183
|
+
? Effect.fail(error.value)
|
|
184
|
+
: Effect.fail(
|
|
185
|
+
new MachineSchemaDecodeError({
|
|
186
|
+
machineId: machine.id,
|
|
187
|
+
boundary: "configuration",
|
|
188
|
+
cause
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const encodeSnapshot = (
|
|
194
|
+
machine: Machine.Any,
|
|
195
|
+
snapshot: Machine.Snapshot<any>
|
|
196
|
+
): Effect.Effect<Machine.EncodedSnapshot, MachineSchemaEncodeError, unknown> =>
|
|
197
|
+
Effect.gen(function*() {
|
|
198
|
+
const configuration = yield* normalizeConfigurationEffect(machine, snapshot).pipe(
|
|
199
|
+
Effect.mapError((error) =>
|
|
200
|
+
new MachineSchemaEncodeError({
|
|
201
|
+
machineId: machine.id,
|
|
202
|
+
boundary: error.boundary === "state" || error.boundary === "history" ? error.boundary : "configuration",
|
|
203
|
+
...(error.state === undefined ? {} : { state: error.state }),
|
|
204
|
+
cause: error.cause
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
const completionPaths = new Set<string>()
|
|
209
|
+
for (const completion of snapshot.completed ?? []) {
|
|
210
|
+
if (completionPaths.has(completion.path)) {
|
|
211
|
+
throw new Error(`Machine snapshot contains duplicate completion "${completion.path}"`)
|
|
212
|
+
}
|
|
213
|
+
if (!configuration.active.has(completion.path) || !isActiveFinalNode(machine, configuration, completion.path)) {
|
|
214
|
+
throw new Error(`Machine snapshot contains invalid completion "${completion.path}"`)
|
|
215
|
+
}
|
|
216
|
+
completionPaths.add(completion.path)
|
|
217
|
+
}
|
|
218
|
+
const active: Array<Machine.EncodedSnapshotState> = []
|
|
219
|
+
for (
|
|
220
|
+
const path of Array.from(configuration.active).sort((left, right) => compareDocumentOrder(machine, left, right))
|
|
221
|
+
) {
|
|
222
|
+
const node = getNode(machine, path)
|
|
223
|
+
active.push({
|
|
224
|
+
path,
|
|
225
|
+
value: yield* encodeBoundary(machine, getStateNodeSchema(node), getActiveValue(configuration, path), {
|
|
226
|
+
boundary: "state",
|
|
227
|
+
state: path
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const completed: Array<Machine.EncodedSnapshotCompletion> = []
|
|
233
|
+
for (
|
|
234
|
+
const [path, output] of Array.from(configuration.outputs).sort(([left], [right]) =>
|
|
235
|
+
compareDocumentOrder(machine, left, right)
|
|
236
|
+
)
|
|
237
|
+
) {
|
|
238
|
+
if (!configuration.active.has(path) || !isActiveFinalNode(machine, configuration, path)) {
|
|
239
|
+
throw new Error(`Machine encoded snapshot contains invalid completion "${path}"`)
|
|
240
|
+
}
|
|
241
|
+
const encodedOutput = yield* encodeBoundary(
|
|
242
|
+
machine,
|
|
243
|
+
getCompletionSchema(machine, configuration, path),
|
|
244
|
+
output,
|
|
245
|
+
{
|
|
246
|
+
boundary: "output",
|
|
247
|
+
state: path
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
completed.push({
|
|
251
|
+
path,
|
|
252
|
+
...(encodedOutput === undefined ? {} : { output: encodedOutput })
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const history: Record<string, Machine.EncodedSnapshotHistoryEntry> = {}
|
|
257
|
+
for (
|
|
258
|
+
const [historyPath, record] of Array.from(configuration.history).sort(([left], [right]) =>
|
|
259
|
+
left.localeCompare(right)
|
|
260
|
+
)
|
|
261
|
+
) {
|
|
262
|
+
const historyNode = machine.stateNodes.byPath.get(historyPath)
|
|
263
|
+
if (
|
|
264
|
+
historyNode === undefined || historyNode.type !== "history" || historyNode.parent !== record.parent ||
|
|
265
|
+
historyNode.history !== record.mode
|
|
266
|
+
) {
|
|
267
|
+
return yield* Effect.fail(
|
|
268
|
+
new MachineSchemaEncodeError({
|
|
269
|
+
machineId: machine.id,
|
|
270
|
+
boundary: "history",
|
|
271
|
+
state: historyPath,
|
|
272
|
+
cause: Cause.die(new Error(`Machine snapshot contains invalid history record "${historyPath}"`))
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
validateHistoryRecordControl(machine, record)
|
|
278
|
+
} catch (cause) {
|
|
279
|
+
return yield* Effect.fail(
|
|
280
|
+
new MachineSchemaEncodeError({
|
|
281
|
+
machineId: machine.id,
|
|
282
|
+
boundary: "history",
|
|
283
|
+
state: historyPath,
|
|
284
|
+
cause: Cause.die(cause)
|
|
285
|
+
})
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
const encodedValues: Record<string, unknown> = {}
|
|
289
|
+
for (const path of record.active) {
|
|
290
|
+
const stateNode = machine.stateNodes.byPath.get(path)
|
|
291
|
+
if (
|
|
292
|
+
stateNode === undefined || stateNode.type === "history" || stateNode.type === "choice" ||
|
|
293
|
+
!record.values.has(path) ||
|
|
294
|
+
!(isPathInSubtree(path, record.parent) || getPathToRoot(machine, record.parent).includes(path))
|
|
295
|
+
) {
|
|
296
|
+
return yield* Effect.fail(
|
|
297
|
+
new MachineSchemaEncodeError({
|
|
298
|
+
machineId: machine.id,
|
|
299
|
+
boundary: "history",
|
|
300
|
+
state: path,
|
|
301
|
+
cause: Cause.die(new Error(`Machine snapshot contains invalid remembered state "${path}"`))
|
|
302
|
+
})
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
encodedValues[path] = yield* encodeBoundary(
|
|
306
|
+
machine,
|
|
307
|
+
getStateNodeSchema(stateNode),
|
|
308
|
+
record.values.get(path),
|
|
309
|
+
{ boundary: "history", state: path }
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
if (record.values.size !== record.active.size) {
|
|
313
|
+
return yield* Effect.fail(
|
|
314
|
+
new MachineSchemaEncodeError({
|
|
315
|
+
machineId: machine.id,
|
|
316
|
+
boundary: "history",
|
|
317
|
+
state: historyPath,
|
|
318
|
+
cause: Cause.die(new Error(`Machine history record "${historyPath}" contains values outside its paths`))
|
|
319
|
+
})
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
history[historyPath] = {
|
|
323
|
+
mode: record.mode,
|
|
324
|
+
active: Array.from(record.active).sort((left, right) => compareDocumentOrder(machine, left, right)),
|
|
325
|
+
values: encodedValues
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
_tag: "MachineSnapshot" as const,
|
|
331
|
+
active,
|
|
332
|
+
...(completed.length === 0 ? {} : { completed }),
|
|
333
|
+
...(Object.keys(history).length === 0 ? {} : { history })
|
|
334
|
+
}
|
|
335
|
+
}).pipe(Effect.catchCause((cause) => failEncodeCause(machine, cause)))
|
|
336
|
+
|
|
337
|
+
export const decodeSnapshot = (
|
|
338
|
+
machine: Machine.Any,
|
|
339
|
+
encoded: unknown
|
|
340
|
+
): Effect.Effect<Machine.Snapshot<any>, MachineSchemaDecodeError, unknown> =>
|
|
341
|
+
Effect.gen(function*() {
|
|
342
|
+
const decoded = yield* Schema.decodeUnknownEffect(EncodedSnapshotSchema)(encoded).pipe(
|
|
343
|
+
Effect.mapError((cause) =>
|
|
344
|
+
new MachineSchemaDecodeError({
|
|
345
|
+
machineId: machine.id,
|
|
346
|
+
boundary: "configuration",
|
|
347
|
+
cause
|
|
348
|
+
})
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
const active = new Set<string>()
|
|
352
|
+
const values = new Map<string, unknown>()
|
|
353
|
+
for (const entry of decoded.active) {
|
|
354
|
+
if (active.has(entry.path)) {
|
|
355
|
+
throw new Error(`Machine encoded snapshot contains duplicate state "${entry.path}"`)
|
|
356
|
+
}
|
|
357
|
+
const node = getNode(machine, entry.path)
|
|
358
|
+
active.add(entry.path)
|
|
359
|
+
values.set(
|
|
360
|
+
entry.path,
|
|
361
|
+
yield* decodeEncodedBoundary(machine, getStateNodeSchema(node), entry.value, {
|
|
362
|
+
boundary: "state",
|
|
363
|
+
state: entry.path
|
|
364
|
+
})
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const history = new Map<string, HistoryRecord>()
|
|
369
|
+
for (const [historyPath, encodedRecord] of Object.entries(decoded.history ?? {})) {
|
|
370
|
+
const historyNode = machine.stateNodes.byPath.get(historyPath)
|
|
371
|
+
if (
|
|
372
|
+
historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined ||
|
|
373
|
+
historyNode.history !== encodedRecord.mode
|
|
374
|
+
) {
|
|
375
|
+
return yield* Effect.fail(
|
|
376
|
+
new MachineSchemaDecodeError({
|
|
377
|
+
machineId: machine.id,
|
|
378
|
+
boundary: "history",
|
|
379
|
+
state: historyPath,
|
|
380
|
+
cause: Cause.die(new Error(`Machine encoded snapshot contains invalid history record "${historyPath}"`))
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
const rememberedActive = new Set<string>()
|
|
385
|
+
const rememberedValues = new Map<string, unknown>()
|
|
386
|
+
for (const path of encodedRecord.active) {
|
|
387
|
+
if (rememberedActive.has(path)) {
|
|
388
|
+
return yield* Effect.fail(
|
|
389
|
+
new MachineSchemaDecodeError({
|
|
390
|
+
machineId: machine.id,
|
|
391
|
+
boundary: "history",
|
|
392
|
+
state: path,
|
|
393
|
+
cause: Cause.die(new Error(`Machine encoded history contains duplicate state "${path}"`))
|
|
394
|
+
})
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
const stateNode = machine.stateNodes.byPath.get(path)
|
|
398
|
+
if (
|
|
399
|
+
stateNode === undefined || stateNode.type === "history" || stateNode.type === "choice" ||
|
|
400
|
+
!Object.prototype.hasOwnProperty.call(encodedRecord.values, path) ||
|
|
401
|
+
!(isPathInSubtree(path, historyNode.parent) || getPathToRoot(machine, historyNode.parent).includes(path))
|
|
402
|
+
) {
|
|
403
|
+
return yield* Effect.fail(
|
|
404
|
+
new MachineSchemaDecodeError({
|
|
405
|
+
machineId: machine.id,
|
|
406
|
+
boundary: "history",
|
|
407
|
+
state: path,
|
|
408
|
+
cause: Cause.die(new Error(`Machine encoded snapshot contains invalid remembered state "${path}"`))
|
|
409
|
+
})
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
rememberedActive.add(path)
|
|
413
|
+
rememberedValues.set(
|
|
414
|
+
path,
|
|
415
|
+
yield* decodeEncodedBoundary(machine, getStateNodeSchema(stateNode), encodedRecord.values[path], {
|
|
416
|
+
boundary: "history",
|
|
417
|
+
state: path
|
|
418
|
+
})
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
if (Object.keys(encodedRecord.values).length !== rememberedActive.size) {
|
|
422
|
+
return yield* Effect.fail(
|
|
423
|
+
new MachineSchemaDecodeError({
|
|
424
|
+
machineId: machine.id,
|
|
425
|
+
boundary: "history",
|
|
426
|
+
state: historyPath,
|
|
427
|
+
cause: Cause.die(new Error(`Machine encoded history "${historyPath}" contains values outside its paths`))
|
|
428
|
+
})
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
if (!rememberedActive.has(historyNode.parent)) {
|
|
432
|
+
return yield* Effect.fail(
|
|
433
|
+
new MachineSchemaDecodeError({
|
|
434
|
+
machineId: machine.id,
|
|
435
|
+
boundary: "history",
|
|
436
|
+
state: historyPath,
|
|
437
|
+
cause: Cause.die(new Error(`Machine encoded history "${historyPath}" does not contain its parent state`))
|
|
438
|
+
})
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
const record: HistoryRecord = {
|
|
442
|
+
mode: encodedRecord.mode,
|
|
443
|
+
parent: historyNode.parent,
|
|
444
|
+
active: rememberedActive,
|
|
445
|
+
values: rememberedValues
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
validateHistoryRecordControl(machine, record)
|
|
449
|
+
} catch (cause) {
|
|
450
|
+
return yield* Effect.fail(
|
|
451
|
+
new MachineSchemaDecodeError({
|
|
452
|
+
machineId: machine.id,
|
|
453
|
+
boundary: "history",
|
|
454
|
+
state: historyPath,
|
|
455
|
+
cause: Cause.die(cause)
|
|
456
|
+
})
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
history.set(historyPath, record)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const configuration: ActiveConfiguration = {
|
|
463
|
+
active,
|
|
464
|
+
values,
|
|
465
|
+
outputs: new Map(),
|
|
466
|
+
history
|
|
467
|
+
}
|
|
468
|
+
const snapshot = validateEncodedConfiguration(machine, configuration)
|
|
469
|
+
const completions: Array<Machine.SnapshotCompletion> = []
|
|
470
|
+
const completionPaths = new Set<string>()
|
|
471
|
+
for (const completion of decoded.completed ?? []) {
|
|
472
|
+
if (completionPaths.has(completion.path)) {
|
|
473
|
+
throw new Error(`Machine encoded snapshot contains duplicate completion "${completion.path}"`)
|
|
474
|
+
}
|
|
475
|
+
if (!active.has(completion.path) || !isActiveFinalNode(machine, configuration, completion.path)) {
|
|
476
|
+
throw new Error(`Machine encoded snapshot contains invalid completion "${completion.path}"`)
|
|
477
|
+
}
|
|
478
|
+
completionPaths.add(completion.path)
|
|
479
|
+
completions.push({
|
|
480
|
+
path: completion.path,
|
|
481
|
+
output: yield* decodeEncodedBoundary(
|
|
482
|
+
machine,
|
|
483
|
+
getCompletionSchema(machine, configuration, completion.path),
|
|
484
|
+
completion.output,
|
|
485
|
+
{
|
|
486
|
+
boundary: "output",
|
|
487
|
+
state: completion.path
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
if (completions.length > 0) {
|
|
493
|
+
;(snapshot as Machine.AtomicSnapshot<string, unknown> & {
|
|
494
|
+
completed: ReadonlyArray<Machine.SnapshotCompletion>
|
|
495
|
+
}).completed = completions
|
|
496
|
+
}
|
|
497
|
+
return snapshot
|
|
498
|
+
}).pipe(Effect.catchCause((cause) => failDecodeCause(machine, cause)))
|