@typeonce/effect-machine 0.7.0 → 0.9.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 +85 -24
- package/dist/Machine.d.ts +545 -444
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +69 -172
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +5 -12
- package/dist/internal/machine/activities.d.ts.map +1 -1
- package/dist/internal/machine/activities.js +47 -17
- package/dist/internal/machine/activities.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +10 -4
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +103 -20
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +31 -0
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts +10 -2
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +93 -34
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/invocationEvent.d.ts +39 -0
- package/dist/internal/machine/invocationEvent.d.ts.map +1 -0
- package/dist/internal/machine/invocationEvent.js +43 -0
- package/dist/internal/machine/invocationEvent.js.map +1 -0
- package/dist/internal/machine/machine.d.ts +6 -50
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +30 -81
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +12 -1
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +90 -37
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +9 -0
- package/dist/internal/machine/protocol.d.ts.map +1 -1
- package/dist/internal/machine/protocol.js +114 -0
- package/dist/internal/machine/protocol.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts.map +1 -1
- package/dist/internal/machine/serialization.js +53 -21
- package/dist/internal/machine/serialization.js.map +1 -1
- package/dist/internal/machine/stateDefinition.d.ts +4 -4
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
- package/dist/internal/machine/stateDefinition.js +18 -11
- package/dist/internal/machine/stateDefinition.js.map +1 -1
- package/dist/internal/machine/symbols.d.ts +2 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -1
- package/dist/internal/machine/symbols.js +2 -0
- package/dist/internal/machine/symbols.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +5 -5
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +39 -13
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +10 -3
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +11 -5
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +174 -121
- package/package.json +3 -3
- package/src/Machine.ts +1336 -872
- package/src/internal/machine/activities.ts +48 -33
- package/src/internal/machine/atom.ts +13 -6
- package/src/internal/machine/configuration.ts +102 -23
- package/src/internal/machine/executionPlan.ts +35 -0
- package/src/internal/machine/invocation.ts +180 -49
- package/src/internal/machine/invocationEvent.ts +72 -0
- package/src/internal/machine/machine.ts +105 -403
- package/src/internal/machine/planner.ts +110 -48
- package/src/internal/machine/protocol.ts +149 -0
- package/src/internal/machine/serialization.ts +56 -31
- package/src/internal/machine/stateDefinition.ts +22 -11
- package/src/internal/machine/symbols.ts +3 -0
- package/src/internal/machine/topology.ts +44 -18
- package/src/internal/testing/machine/verification.ts +13 -3
- package/src/unstable/reactivity/AtomMachine.ts +12 -5
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
configurationFromHistoryRecord,
|
|
19
19
|
getActiveLeafPathFrom,
|
|
20
20
|
getActiveLeafPaths,
|
|
21
|
-
getActiveValue,
|
|
22
21
|
getHistoryRecord,
|
|
23
22
|
getInitialEntryPaths,
|
|
24
23
|
getLeafPath,
|
|
@@ -39,6 +38,7 @@ import {
|
|
|
39
38
|
validateInitialConfiguration
|
|
40
39
|
} from "./configuration.js"
|
|
41
40
|
import { InfiniteTransitionError, MachineSchemaDecodeError, StartupError } from "./errors.js"
|
|
41
|
+
import * as InvocationEvent from "./invocationEvent.js"
|
|
42
42
|
import { decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
|
|
43
43
|
import { InitialEventTypeId } from "./symbols.js"
|
|
44
44
|
import {
|
|
@@ -201,66 +201,76 @@ const completeHistoryConfiguration = (
|
|
|
201
201
|
if (node.initial === undefined) {
|
|
202
202
|
throw new Error(`Machine shallow history expected compound state "${path}" to have an initial child`)
|
|
203
203
|
}
|
|
204
|
-
const
|
|
205
|
-
if (initializer === undefined) {
|
|
206
|
-
throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`)
|
|
207
|
-
}
|
|
204
|
+
const child = getNode(machine, node.initial)
|
|
208
205
|
const current = {
|
|
209
206
|
active,
|
|
210
207
|
values,
|
|
211
208
|
outputs: configuration.outputs,
|
|
212
209
|
history: configuration.history
|
|
213
210
|
} as ActiveConfiguration
|
|
214
|
-
const initialized = collectStateInitializer(machine, initializer, {
|
|
215
|
-
state: getActiveValue(current, path),
|
|
216
|
-
parent: getParentValue(machine, current, path),
|
|
217
|
-
parents: getParentValues(machine, current, path),
|
|
218
|
-
event
|
|
219
|
-
})
|
|
220
|
-
const child = getNode(machine, node.initial)
|
|
221
211
|
active.add(child.path)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
212
|
+
if (child.schema !== undefined) {
|
|
213
|
+
const initializer = machine.handlers[path]?.initial
|
|
214
|
+
if (initializer === undefined) {
|
|
215
|
+
throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`)
|
|
216
|
+
}
|
|
217
|
+
const initialized = collectStateInitializer(machine, initializer, {
|
|
218
|
+
state: current.values.get(path),
|
|
219
|
+
parent: getParentValue(machine, current, path),
|
|
220
|
+
parents: getParentValues(machine, current, path),
|
|
221
|
+
event
|
|
222
|
+
})
|
|
223
|
+
values.set(child.path, decodeStateValueSync(machine, child, initialized.value))
|
|
224
|
+
commands.push(...initialized.commands)
|
|
225
|
+
raisedEvents.push(...initialized.raisedEvents)
|
|
226
|
+
emittedEvents.push(...initialized.emittedEvents)
|
|
227
|
+
}
|
|
226
228
|
changed = true
|
|
227
229
|
}
|
|
228
230
|
if (node.type === "parallel") {
|
|
229
231
|
const missing = node.children.filter((childPath) => !active.has(childPath))
|
|
230
232
|
if (missing.length > 0) {
|
|
231
|
-
const
|
|
232
|
-
if (initializer === undefined) {
|
|
233
|
-
throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`)
|
|
234
|
-
}
|
|
233
|
+
const valuedMissing = missing.filter((childPath) => getNode(machine, childPath).schema !== undefined)
|
|
235
234
|
const current = {
|
|
236
235
|
active,
|
|
237
236
|
values,
|
|
238
237
|
outputs: configuration.outputs,
|
|
239
238
|
history: configuration.history
|
|
240
239
|
} as ActiveConfiguration
|
|
241
|
-
const
|
|
242
|
-
|
|
240
|
+
const initializer = valuedMissing.length === 0 ? undefined : machine.handlers[path]?.initial
|
|
241
|
+
if (valuedMissing.length > 0 && initializer === undefined) {
|
|
242
|
+
throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`)
|
|
243
|
+
}
|
|
244
|
+
const initialized = initializer === undefined ? undefined : collectStateInitializer(machine, initializer, {
|
|
245
|
+
state: current.values.get(path),
|
|
243
246
|
parent: getParentValue(machine, current, path),
|
|
244
247
|
parents: getParentValues(machine, current, path),
|
|
245
248
|
event
|
|
246
249
|
})
|
|
247
|
-
if (typeof initialized.value !== "object" || initialized.value === null) {
|
|
250
|
+
if (initialized !== undefined && (typeof initialized.value !== "object" || initialized.value === null)) {
|
|
248
251
|
throw new Error(`Machine parallel state initializer for "${path}" must return its region values`)
|
|
249
252
|
}
|
|
250
253
|
for (const childPath of missing) {
|
|
251
254
|
const child = getNode(machine, childPath)
|
|
252
|
-
if (!Object.prototype.hasOwnProperty.call(initialized.value, child.key)) {
|
|
253
|
-
throw new Error(`Machine parallel state initializer for "${path}" must return region "${child.key}"`)
|
|
254
|
-
}
|
|
255
255
|
active.add(child.path)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
if (child.schema !== undefined) {
|
|
257
|
+
if (
|
|
258
|
+
initialized === undefined ||
|
|
259
|
+
!Object.prototype.hasOwnProperty.call(initialized.value as object, child.key)
|
|
260
|
+
) {
|
|
261
|
+
throw new Error(`Machine parallel state initializer for "${path}" must return region "${child.key}"`)
|
|
262
|
+
}
|
|
263
|
+
values.set(
|
|
264
|
+
child.path,
|
|
265
|
+
decodeStateValueSync(machine, child, (initialized.value as Record<string, unknown>)[child.key])
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (initialized !== undefined) {
|
|
270
|
+
commands.push(...initialized.commands)
|
|
271
|
+
raisedEvents.push(...initialized.raisedEvents)
|
|
272
|
+
emittedEvents.push(...initialized.emittedEvents)
|
|
260
273
|
}
|
|
261
|
-
commands.push(...initialized.commands)
|
|
262
|
-
raisedEvents.push(...initialized.raisedEvents)
|
|
263
|
-
emittedEvents.push(...initialized.emittedEvents)
|
|
264
274
|
changed = true
|
|
265
275
|
}
|
|
266
276
|
}
|
|
@@ -495,7 +505,7 @@ const makeStateActionContext = <
|
|
|
495
505
|
path: string,
|
|
496
506
|
event: Machine.LifecycleEvent<Events>
|
|
497
507
|
): Machine.StateActionContext<States, Events, Emits, StateId> => ({
|
|
498
|
-
state:
|
|
508
|
+
state: configuration.values.get(path) as Machine.StateByIdentifier<States, StateId>,
|
|
499
509
|
parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<States, StateId>,
|
|
500
510
|
parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
|
|
501
511
|
event
|
|
@@ -514,7 +524,7 @@ const makeTransitionContext = <
|
|
|
514
524
|
event: Machine.EventByTag<Events, EventTag>,
|
|
515
525
|
snapshot: Machine.Snapshot<States>
|
|
516
526
|
): Machine.HandlerContext<States, Events, Emits, StateId, EventTag, any, any> => ({
|
|
517
|
-
state:
|
|
527
|
+
state: configuration.values.get(path) as Machine.StateByIdentifier<States, StateId>,
|
|
518
528
|
parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<States, StateId>,
|
|
519
529
|
parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
|
|
520
530
|
event,
|
|
@@ -535,7 +545,7 @@ const makeDoneContext = <
|
|
|
535
545
|
output: unknown,
|
|
536
546
|
snapshot: Machine.Snapshot<States>
|
|
537
547
|
): Machine.DoneContext<States, Events, Emits, StateId> => ({
|
|
538
|
-
state:
|
|
548
|
+
state: configuration.values.get(path) as Machine.StateByIdentifier<States, StateId>,
|
|
539
549
|
parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<States, StateId>,
|
|
540
550
|
parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
|
|
541
551
|
event,
|
|
@@ -629,7 +639,7 @@ const selectAlwaysTransitions = <
|
|
|
629
639
|
Machine.AlwaysContext<States, Events, Emits, Machine.StateIdentifier<States>>
|
|
630
640
|
>,
|
|
631
641
|
context: {
|
|
632
|
-
state:
|
|
642
|
+
state: configuration.values.get(path) as Machine.StateByIdentifier<
|
|
633
643
|
States,
|
|
634
644
|
Machine.StateIdentifier<States>
|
|
635
645
|
>,
|
|
@@ -789,6 +799,54 @@ const selectEventTransitions = <
|
|
|
789
799
|
return selected
|
|
790
800
|
}
|
|
791
801
|
|
|
802
|
+
const selectInvocationTransition = <
|
|
803
|
+
const States extends Machine.StateSchemas,
|
|
804
|
+
const Events extends ReadonlyArray<Machine.TaggedSchema>,
|
|
805
|
+
const Emits extends ReadonlyArray<Machine.TaggedSchema>,
|
|
806
|
+
E,
|
|
807
|
+
R
|
|
808
|
+
>(
|
|
809
|
+
machine: Machine.Any,
|
|
810
|
+
configuration: ActiveConfiguration,
|
|
811
|
+
event: InvocationEvent.InvocationEvent
|
|
812
|
+
): ReadonlyArray<SelectedTransition<States, E, R, any>> => {
|
|
813
|
+
if (!configuration.active.has(event.path)) return []
|
|
814
|
+
const config = machine.handlers[event.path] as Machine.AnyStateConfig | undefined
|
|
815
|
+
const invoke = InvocationEvent.definitions(config?.invoke).find((definition) => {
|
|
816
|
+
const id = "child" in definition ? definition.child?.id : definition.id
|
|
817
|
+
return String(id) === event.id
|
|
818
|
+
})
|
|
819
|
+
if (invoke === undefined) return []
|
|
820
|
+
const handler = event.type === "done"
|
|
821
|
+
? invoke.onDone
|
|
822
|
+
: event.type === "failure"
|
|
823
|
+
? invoke.onFailure
|
|
824
|
+
: invoke.onSnapshot
|
|
825
|
+
const transition = normalizeTransition(handler)
|
|
826
|
+
if (transition === undefined) return []
|
|
827
|
+
const snapshot = snapshotFromConfiguration<States>(machine, configuration)
|
|
828
|
+
const context = {
|
|
829
|
+
state: configuration.values.get(event.path),
|
|
830
|
+
parent: getParentValue(machine, configuration, event.path),
|
|
831
|
+
parents: getParentValues(machine, configuration, event.path),
|
|
832
|
+
snapshot,
|
|
833
|
+
target: getTargetBuilder(machine, event.path),
|
|
834
|
+
id: event.id,
|
|
835
|
+
...(event.type === "done"
|
|
836
|
+
? { output: event.output }
|
|
837
|
+
: event.type === "failure"
|
|
838
|
+
? { error: event.error }
|
|
839
|
+
: { snapshot: event.snapshot })
|
|
840
|
+
}
|
|
841
|
+
return [{
|
|
842
|
+
sourcePath: event.path,
|
|
843
|
+
leafPath: getActiveLeafPathFrom(machine, configuration, event.path),
|
|
844
|
+
trigger: { type: "invoke", id: event.id, outcome: event.type },
|
|
845
|
+
transition: transition as unknown as MicrostepTransition<States, E, R, any>,
|
|
846
|
+
context
|
|
847
|
+
}]
|
|
848
|
+
}
|
|
849
|
+
|
|
792
850
|
export const getTargetNodePath = <const States extends Machine.StateSchemas>(
|
|
793
851
|
target:
|
|
794
852
|
| Machine.Snapshot<States>
|
|
@@ -919,7 +977,9 @@ const choicesFromTarget = (
|
|
|
919
977
|
return
|
|
920
978
|
}
|
|
921
979
|
if (typeof current !== "object" || current === null || !("path" in current) || !("value" in current)) return
|
|
922
|
-
|
|
980
|
+
if (current.value !== undefined) {
|
|
981
|
+
values[String(current.path)] = current.value
|
|
982
|
+
}
|
|
923
983
|
if ("state" in current) visit(current.state)
|
|
924
984
|
if ("states" in current && typeof current.states === "object" && current.states !== null) {
|
|
925
985
|
for (const state of Object.values(current.states)) visit(state)
|
|
@@ -1801,11 +1861,11 @@ const macrostepConfiguration = <
|
|
|
1801
1861
|
>(
|
|
1802
1862
|
machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
|
|
1803
1863
|
configuration: ActiveConfiguration,
|
|
1804
|
-
event: Machine.EventOf<Events>
|
|
1864
|
+
event: Machine.EventOf<Events> | InvocationEvent.InvocationEvent
|
|
1805
1865
|
) => {
|
|
1806
|
-
const decodedEvent = decodeEventSync<Events>(machine, event)
|
|
1866
|
+
const decodedEvent = InvocationEvent.isInvocationEvent(event) ? event : decodeEventSync<Events>(machine, event)
|
|
1807
1867
|
if (isActiveFinalConfiguration(machine, configuration)) {
|
|
1808
|
-
const completed = completeConfigurationSync(machine, configuration, decodedEvent)
|
|
1868
|
+
const completed = completeConfigurationSync(machine, configuration, decodedEvent as any)
|
|
1809
1869
|
const root = getRootPath(machine, completed.configuration)
|
|
1810
1870
|
if (!completed.configuration.outputs.has(root)) {
|
|
1811
1871
|
throw new Error("Machine reached a terminal configuration without a completed root output")
|
|
@@ -1820,11 +1880,13 @@ const macrostepConfiguration = <
|
|
|
1820
1880
|
}
|
|
1821
1881
|
}
|
|
1822
1882
|
|
|
1823
|
-
const selections =
|
|
1824
|
-
machine,
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1883
|
+
const selections = InvocationEvent.isInvocationEvent(decodedEvent)
|
|
1884
|
+
? selectInvocationTransition<States, Events, Emits, E, R>(machine, configuration, decodedEvent)
|
|
1885
|
+
: selectEventTransitions<States, Events, Emits, E, R>(
|
|
1886
|
+
machine,
|
|
1887
|
+
configuration,
|
|
1888
|
+
decodedEvent as Machine.EventByTag<Events, Machine.TagOf<Events[number]>>
|
|
1889
|
+
)
|
|
1828
1890
|
if (selections.length === 0) {
|
|
1829
1891
|
return {
|
|
1830
1892
|
next: configuration,
|
|
@@ -1838,14 +1900,14 @@ const macrostepConfiguration = <
|
|
|
1838
1900
|
const step = microstep(
|
|
1839
1901
|
machine,
|
|
1840
1902
|
configuration,
|
|
1841
|
-
decodedEvent,
|
|
1903
|
+
decodedEvent as any,
|
|
1842
1904
|
selections
|
|
1843
1905
|
)
|
|
1844
1906
|
const commands = [...step.commands]
|
|
1845
1907
|
const raisedEvents = [...step.raisedEvents]
|
|
1846
1908
|
const emittedEvents = [...step.emittedEvents]
|
|
1847
1909
|
const microsteps = [step]
|
|
1848
|
-
return settle(machine, step.next, decodedEvent, commands, raisedEvents, emittedEvents, microsteps)
|
|
1910
|
+
return settle(machine, step.next, decodedEvent as any, commands, raisedEvents, emittedEvents, microsteps)
|
|
1849
1911
|
}
|
|
1850
1912
|
|
|
1851
1913
|
const snapshotMacrostep = <
|
|
@@ -9,6 +9,7 @@ import * as Effect from "effect/Effect"
|
|
|
9
9
|
import { hasProperty } from "effect/Predicate"
|
|
10
10
|
import * as Result from "effect/Result"
|
|
11
11
|
import * as Schema from "effect/Schema"
|
|
12
|
+
import * as SchemaAST from "effect/SchemaAST"
|
|
12
13
|
import type { Machine } from "../../Machine.js"
|
|
13
14
|
import { MachineSchemaDecodeError } from "./errors.js"
|
|
14
15
|
import { getStateNodeSchema, isStateInput } from "./topology.js"
|
|
@@ -26,6 +27,15 @@ interface MachineProtocolSchemas {
|
|
|
26
27
|
readonly trustedEvents: WeakSet<object>
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export const EventConstructionTypeId: unique symbol = Symbol("effect/Machine/EventConstruction")
|
|
31
|
+
|
|
32
|
+
interface EventConstruction {
|
|
33
|
+
readonly [EventConstructionTypeId]: typeof EventConstructionTypeId
|
|
34
|
+
readonly _tag: PropertyKey
|
|
35
|
+
readonly schema: Machine.TaggedSchema
|
|
36
|
+
readonly input: unknown
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
type BoundaryDecoder = (value: unknown) => Effect.Effect<unknown, Schema.SchemaError, unknown>
|
|
30
40
|
|
|
31
41
|
type BoundaryResultDecoder = (value: unknown) => Result.Result<unknown, Schema.SchemaError>
|
|
@@ -111,6 +121,88 @@ export const copyProtocol = (source: Machine.Any, target: Machine.Any): void =>
|
|
|
111
121
|
export const getEventName = (event: unknown): string | undefined =>
|
|
112
122
|
hasProperty(event, "_tag") ? String(event._tag) : undefined
|
|
113
123
|
|
|
124
|
+
const makeEventConstruction = (
|
|
125
|
+
schema: Machine.TaggedSchema,
|
|
126
|
+
tag: PropertyKey,
|
|
127
|
+
input: unknown
|
|
128
|
+
): EventConstruction => ({
|
|
129
|
+
[EventConstructionTypeId]: EventConstructionTypeId,
|
|
130
|
+
_tag: tag,
|
|
131
|
+
schema,
|
|
132
|
+
input
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const isEventConstruction = (value: unknown): value is EventConstruction => hasProperty(value, EventConstructionTypeId)
|
|
136
|
+
|
|
137
|
+
export const eventConstructors = (
|
|
138
|
+
schemas: ReadonlyArray<Machine.TaggedSchema>
|
|
139
|
+
): Readonly<Record<PropertyKey, (...args: ReadonlyArray<unknown>) => EventConstruction>> => {
|
|
140
|
+
const leaves: Array<Machine.TaggedSchema> = []
|
|
141
|
+
const collect = (schema: Machine.TaggedSchema): void => {
|
|
142
|
+
if (hasProperty(schema, "cases") && typeof schema.cases === "object" && schema.cases !== null) {
|
|
143
|
+
for (const candidate of Reflect.ownKeys(schema.cases)) {
|
|
144
|
+
collect(Reflect.get(schema.cases, candidate) as Machine.TaggedSchema)
|
|
145
|
+
}
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (hasProperty(schema, "members") && Array.isArray(schema.members)) {
|
|
149
|
+
for (const member of schema.members) collect(member as Machine.TaggedSchema)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
leaves.push(schema)
|
|
153
|
+
}
|
|
154
|
+
for (const schema of schemas) collect(schema)
|
|
155
|
+
const constructors = Object.create(null) as Record<
|
|
156
|
+
PropertyKey,
|
|
157
|
+
(...args: ReadonlyArray<unknown>) => EventConstruction
|
|
158
|
+
>
|
|
159
|
+
const tags = (ast: SchemaAST.AST): ReadonlyArray<PropertyKey> => {
|
|
160
|
+
if (SchemaAST.isLiteral(ast)) {
|
|
161
|
+
return typeof ast.literal === "string" || typeof ast.literal === "number" ? [ast.literal] : []
|
|
162
|
+
}
|
|
163
|
+
if (SchemaAST.isUniqueSymbol(ast)) return [ast.symbol]
|
|
164
|
+
if (SchemaAST.isEnum(ast)) return ast.enums.map(([, value]) => value)
|
|
165
|
+
if (SchemaAST.isUnion(ast)) return ast.types.flatMap(tags)
|
|
166
|
+
if (SchemaAST.isSuspend(ast)) return tags(ast.thunk())
|
|
167
|
+
return []
|
|
168
|
+
}
|
|
169
|
+
const schemaTags = (schema: Machine.TaggedSchema): ReadonlyArray<PropertyKey> => {
|
|
170
|
+
const ast = SchemaAST.toType(schema.ast)
|
|
171
|
+
if (SchemaAST.isObjects(ast)) {
|
|
172
|
+
const tag = ast.propertySignatures.find(({ name }) => name === "_tag")
|
|
173
|
+
const discriminants = tag === undefined ? [] : tags(tag.type)
|
|
174
|
+
if (discriminants.length > 0) return discriminants
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return Schema.Union([schema]).pipe(Schema.toTaggedUnion("_tag")).discriminants
|
|
178
|
+
} catch {
|
|
179
|
+
return []
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const schema of leaves) {
|
|
183
|
+
const discriminants = schemaTags(schema)
|
|
184
|
+
if (discriminants.length === 0) {
|
|
185
|
+
throw new Error("Machine event constructors require finite literal or unique symbol event tags")
|
|
186
|
+
}
|
|
187
|
+
for (const tag of discriminants) {
|
|
188
|
+
if (hasProperty(constructors, tag)) {
|
|
189
|
+
throw new Error(`Duplicate machine event constructor tag: ${String(tag)}`)
|
|
190
|
+
}
|
|
191
|
+
Object.defineProperty(constructors, tag, {
|
|
192
|
+
value: (...args: ReadonlyArray<unknown>) => {
|
|
193
|
+
const fields = args.length === 0 ? {} : args[0]
|
|
194
|
+
const input = typeof fields === "object" && fields !== null
|
|
195
|
+
? { ...fields, _tag: tag }
|
|
196
|
+
: { _tag: tag }
|
|
197
|
+
return makeEventConstruction(schema, tag, input)
|
|
198
|
+
},
|
|
199
|
+
enumerable: true
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return constructors
|
|
204
|
+
}
|
|
205
|
+
|
|
114
206
|
export const decodeBoundary = <A>(
|
|
115
207
|
machine: Machine.Any,
|
|
116
208
|
schema: Schema.Top,
|
|
@@ -195,6 +287,54 @@ export const makeEvent = <Schema extends Machine.TaggedSchema>(
|
|
|
195
287
|
return event
|
|
196
288
|
}
|
|
197
289
|
|
|
290
|
+
const eventConstructionProtocolError = (
|
|
291
|
+
machine: Machine.Any,
|
|
292
|
+
construction: EventConstruction
|
|
293
|
+
): MachineSchemaDecodeError =>
|
|
294
|
+
new MachineSchemaDecodeError({
|
|
295
|
+
machineId: machine.id,
|
|
296
|
+
boundary: "event",
|
|
297
|
+
event: String(construction._tag),
|
|
298
|
+
cause: Cause.die(new Error("Constructed event schema does not belong to the machine event protocol"))
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const decodeEventConstruction = (
|
|
302
|
+
machine: Machine.Any,
|
|
303
|
+
protocol: MachineProtocolSchemas,
|
|
304
|
+
construction: EventConstruction
|
|
305
|
+
): Effect.Effect<unknown, MachineSchemaDecodeError> => {
|
|
306
|
+
if (!protocol.eventConstructors.has(construction.schema as object)) {
|
|
307
|
+
return Effect.fail(eventConstructionProtocolError(machine, construction))
|
|
308
|
+
}
|
|
309
|
+
return construction.schema.makeEffect(construction.input as never).pipe(
|
|
310
|
+
Effect.mapError((cause) =>
|
|
311
|
+
new MachineSchemaDecodeError({
|
|
312
|
+
machineId: machine.id,
|
|
313
|
+
boundary: "event",
|
|
314
|
+
event: String(construction._tag),
|
|
315
|
+
cause: new Schema.SchemaError(cause)
|
|
316
|
+
})
|
|
317
|
+
),
|
|
318
|
+
Effect.tap((event) => Effect.sync(() => protocol.trustedEvents.add(event as object)))
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const decodeEventConstructionSync = (
|
|
323
|
+
machine: Machine.Any,
|
|
324
|
+
protocol: MachineProtocolSchemas,
|
|
325
|
+
construction: EventConstruction
|
|
326
|
+
): unknown => {
|
|
327
|
+
if (!protocol.eventConstructors.has(construction.schema as object)) {
|
|
328
|
+
throw eventConstructionProtocolError(machine, construction)
|
|
329
|
+
}
|
|
330
|
+
const event = makeBoundarySync(machine, construction.schema, construction.input, {
|
|
331
|
+
boundary: "event",
|
|
332
|
+
event: String(construction._tag)
|
|
333
|
+
})
|
|
334
|
+
protocol.trustedEvents.add(event as object)
|
|
335
|
+
return event
|
|
336
|
+
}
|
|
337
|
+
|
|
198
338
|
const isTrustedEvent = (protocol: MachineProtocolSchemas, event: unknown): boolean =>
|
|
199
339
|
typeof event === "object" && event !== null && protocol.trustedEvents.has(event)
|
|
200
340
|
|
|
@@ -210,6 +350,12 @@ export const decodeEvent = <const Events extends ReadonlyArray<Machine.TaggedSch
|
|
|
210
350
|
event: unknown
|
|
211
351
|
): Effect.Effect<Machine.EventOf<Events>, MachineSchemaDecodeError> => {
|
|
212
352
|
const protocol = getProtocolSchemas(machine)
|
|
353
|
+
if (isEventConstruction(event)) {
|
|
354
|
+
return decodeEventConstruction(machine, protocol, event) as Effect.Effect<
|
|
355
|
+
Machine.EventOf<Events>,
|
|
356
|
+
MachineSchemaDecodeError
|
|
357
|
+
>
|
|
358
|
+
}
|
|
213
359
|
if (isTrustedEvent(protocol, event)) {
|
|
214
360
|
return Effect.succeed(event as Machine.EventOf<Events>)
|
|
215
361
|
}
|
|
@@ -227,6 +373,9 @@ export const decodeEventSync = <const Events extends ReadonlyArray<Machine.Tagge
|
|
|
227
373
|
event: unknown
|
|
228
374
|
): Machine.EventOf<Events> => {
|
|
229
375
|
const protocol = getProtocolSchemas(machine)
|
|
376
|
+
if (isEventConstruction(event)) {
|
|
377
|
+
return decodeEventConstructionSync(machine, protocol, event) as Machine.EventOf<Events>
|
|
378
|
+
}
|
|
230
379
|
if (isTrustedEvent(protocol, event)) {
|
|
231
380
|
return event as Machine.EventOf<Events>
|
|
232
381
|
}
|
|
@@ -31,7 +31,7 @@ const EncodedSnapshotSchema = Schema.Struct({
|
|
|
31
31
|
_tag: Schema.Literal("MachineSnapshot"),
|
|
32
32
|
active: Schema.Array(Schema.Struct({
|
|
33
33
|
path: Schema.String,
|
|
34
|
-
value: Schema.Unknown
|
|
34
|
+
value: Schema.optional(Schema.Unknown)
|
|
35
35
|
})),
|
|
36
36
|
completed: Schema.optional(Schema.Array(Schema.Struct({
|
|
37
37
|
path: Schema.String,
|
|
@@ -220,13 +220,17 @@ export const encodeSnapshot = (
|
|
|
220
220
|
const path of Array.from(configuration.active).sort((left, right) => compareDocumentOrder(machine, left, right))
|
|
221
221
|
) {
|
|
222
222
|
const node = getNode(machine, path)
|
|
223
|
-
|
|
224
|
-
path
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
if (node.schema === undefined) {
|
|
224
|
+
active.push({ path })
|
|
225
|
+
} else {
|
|
226
|
+
active.push({
|
|
227
|
+
path,
|
|
228
|
+
value: yield* encodeBoundary(machine, getStateNodeSchema(node), getActiveValue(configuration, path), {
|
|
229
|
+
boundary: "state",
|
|
230
|
+
state: path
|
|
231
|
+
})
|
|
228
232
|
})
|
|
229
|
-
}
|
|
233
|
+
}
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
const completed: Array<Machine.EncodedSnapshotCompletion> = []
|
|
@@ -290,7 +294,6 @@ export const encodeSnapshot = (
|
|
|
290
294
|
const stateNode = machine.stateNodes.byPath.get(path)
|
|
291
295
|
if (
|
|
292
296
|
stateNode === undefined || stateNode.type === "history" || stateNode.type === "choice" ||
|
|
293
|
-
!record.values.has(path) ||
|
|
294
297
|
!(isPathInSubtree(path, record.parent) || getPathToRoot(machine, record.parent).includes(path))
|
|
295
298
|
) {
|
|
296
299
|
return yield* Effect.fail(
|
|
@@ -302,14 +305,23 @@ export const encodeSnapshot = (
|
|
|
302
305
|
})
|
|
303
306
|
)
|
|
304
307
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
if (stateNode.schema === undefined) {
|
|
309
|
+
if (record.values.has(path)) {
|
|
310
|
+
throw new Error(`Machine history record contains a value for structural state "${path}"`)
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
if (!record.values.has(path)) {
|
|
314
|
+
throw new Error(`Machine history record omits value for "${path}"`)
|
|
315
|
+
}
|
|
316
|
+
encodedValues[path] = yield* encodeBoundary(
|
|
317
|
+
machine,
|
|
318
|
+
getStateNodeSchema(stateNode),
|
|
319
|
+
record.values.get(path),
|
|
320
|
+
{ boundary: "history", state: path }
|
|
321
|
+
)
|
|
322
|
+
}
|
|
311
323
|
}
|
|
312
|
-
if (
|
|
324
|
+
if (Object.keys(encodedValues).length !== record.values.size) {
|
|
313
325
|
return yield* Effect.fail(
|
|
314
326
|
new MachineSchemaEncodeError({
|
|
315
327
|
machineId: machine.id,
|
|
@@ -356,13 +368,19 @@ export const decodeSnapshot = (
|
|
|
356
368
|
}
|
|
357
369
|
const node = getNode(machine, entry.path)
|
|
358
370
|
active.add(entry.path)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
371
|
+
const hasValue = Object.prototype.hasOwnProperty.call(entry, "value")
|
|
372
|
+
if (node.schema === undefined) {
|
|
373
|
+
if (hasValue) throw new Error(`Machine encoded snapshot contains a value for structural state "${entry.path}"`)
|
|
374
|
+
} else {
|
|
375
|
+
if (!hasValue) throw new Error(`Machine encoded snapshot omits value for state "${entry.path}"`)
|
|
376
|
+
values.set(
|
|
377
|
+
entry.path,
|
|
378
|
+
yield* decodeEncodedBoundary(machine, getStateNodeSchema(node), entry.value, {
|
|
379
|
+
boundary: "state",
|
|
380
|
+
state: entry.path
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
}
|
|
366
384
|
}
|
|
367
385
|
|
|
368
386
|
const history = new Map<string, HistoryRecord>()
|
|
@@ -397,7 +415,6 @@ export const decodeSnapshot = (
|
|
|
397
415
|
const stateNode = machine.stateNodes.byPath.get(path)
|
|
398
416
|
if (
|
|
399
417
|
stateNode === undefined || stateNode.type === "history" || stateNode.type === "choice" ||
|
|
400
|
-
!Object.prototype.hasOwnProperty.call(encodedRecord.values, path) ||
|
|
401
418
|
!(isPathInSubtree(path, historyNode.parent) || getPathToRoot(machine, historyNode.parent).includes(path))
|
|
402
419
|
) {
|
|
403
420
|
return yield* Effect.fail(
|
|
@@ -410,15 +427,23 @@ export const decodeSnapshot = (
|
|
|
410
427
|
)
|
|
411
428
|
}
|
|
412
429
|
rememberedActive.add(path)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
430
|
+
const hasValue = Object.prototype.hasOwnProperty.call(encodedRecord.values, path)
|
|
431
|
+
if (stateNode.schema === undefined) {
|
|
432
|
+
if (hasValue) {
|
|
433
|
+
throw new Error(`Machine encoded history contains a value for structural state "${path}"`)
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
if (!hasValue) throw new Error(`Machine encoded history omits value for state "${path}"`)
|
|
437
|
+
rememberedValues.set(
|
|
438
|
+
path,
|
|
439
|
+
yield* decodeEncodedBoundary(machine, getStateNodeSchema(stateNode), encodedRecord.values[path], {
|
|
440
|
+
boundary: "history",
|
|
441
|
+
state: path
|
|
442
|
+
})
|
|
443
|
+
)
|
|
444
|
+
}
|
|
420
445
|
}
|
|
421
|
-
if (Object.keys(encodedRecord.values).length !==
|
|
446
|
+
if (Object.keys(encodedRecord.values).length !== rememberedValues.size) {
|
|
422
447
|
return yield* Effect.fail(
|
|
423
448
|
new MachineSchemaDecodeError({
|
|
424
449
|
machineId: machine.id,
|
|
@@ -2,10 +2,10 @@ import * as Schema from "effect/Schema"
|
|
|
2
2
|
import * as SchemaAST from "effect/SchemaAST"
|
|
3
3
|
|
|
4
4
|
export const StateNodePropertyPolicy = {
|
|
5
|
-
atomic: ["schema", "type"],
|
|
6
|
-
final: ["schema", "type", "output"],
|
|
7
|
-
compound: ["schema", "type", "initial", "states"],
|
|
8
|
-
parallel: ["schema", "type", "output", "states"],
|
|
5
|
+
atomic: ["schema", "type", "annotations"],
|
|
6
|
+
final: ["schema", "type", "output", "annotations"],
|
|
7
|
+
compound: ["schema", "type", "initial", "states", "annotations"],
|
|
8
|
+
parallel: ["schema", "type", "output", "states", "annotations"],
|
|
9
9
|
history: ["type", "history", "annotations"],
|
|
10
10
|
choice: ["type", "annotations"]
|
|
11
11
|
} as const
|
|
@@ -152,18 +152,22 @@ const assertAllowedProperties = (
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
const
|
|
155
|
+
const validateSchemaLessAnnotations = (
|
|
156
156
|
boundary: StateDefinitionBoundary,
|
|
157
157
|
path: string,
|
|
158
158
|
annotations: unknown
|
|
159
159
|
): void => {
|
|
160
|
-
assertPlainRecord(boundary, `${path}.annotations`, annotations, "
|
|
160
|
+
assertPlainRecord(boundary, `${path}.annotations`, annotations, "schema-less state annotations")
|
|
161
161
|
for (const property of Reflect.ownKeys(annotations)) {
|
|
162
162
|
if (!(PseudoStateAnnotationProperties as ReadonlyArray<PropertyKey>).includes(property)) {
|
|
163
|
-
fail(
|
|
163
|
+
fail(
|
|
164
|
+
boundary,
|
|
165
|
+
`${path}.annotations`,
|
|
166
|
+
`schema-less state annotations cannot declare property "${String(property)}"`
|
|
167
|
+
)
|
|
164
168
|
}
|
|
165
169
|
if (typeof annotations[property] !== "string") {
|
|
166
|
-
fail(boundary, `${path}.annotations.${String(property)}`, "
|
|
170
|
+
fail(boundary, `${path}.annotations.${String(property)}`, "schema-less state annotation values must be strings")
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
}
|
|
@@ -211,7 +215,7 @@ const validateStateTree = (
|
|
|
211
215
|
fail(boundary, path, `${kind} states must be declared below an active parent state`)
|
|
212
216
|
}
|
|
213
217
|
if (hasOwn(node, "annotations")) {
|
|
214
|
-
|
|
218
|
+
validateSchemaLessAnnotations(boundary, path, node.annotations)
|
|
215
219
|
}
|
|
216
220
|
if (kind === "history" && hasOwn(node, "history") && node.history !== "shallow" && node.history !== "deep") {
|
|
217
221
|
fail(boundary, `${path}.history`, "history must be \"shallow\" or \"deep\"")
|
|
@@ -219,8 +223,15 @@ const validateStateTree = (
|
|
|
219
223
|
continue
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
const valued = hasOwn(node, "schema")
|
|
227
|
+
if (valued && !isTaggedSchema(node.schema)) {
|
|
228
|
+
fail(boundary, `${path}.schema`, "state schemas must decode to an object with a required PropertyKey _tag")
|
|
229
|
+
}
|
|
230
|
+
if (valued && hasOwn(node, "annotations")) {
|
|
231
|
+
fail(boundary, `${path}.annotations`, "schema-backed states must declare annotations on their schema")
|
|
232
|
+
}
|
|
233
|
+
if (!valued && hasOwn(node, "annotations")) {
|
|
234
|
+
validateSchemaLessAnnotations(boundary, path, node.annotations)
|
|
224
235
|
}
|
|
225
236
|
if (kind === "atomic" && hasOwn(node, "type") && node.type !== "active") {
|
|
226
237
|
fail(boundary, `${path}.type`, "atomic state type must be \"active\"")
|