@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.
Files changed (77) hide show
  1. package/README.md +85 -24
  2. package/dist/Machine.d.ts +545 -444
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +69 -172
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +5 -12
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +47 -17
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/atom.d.ts +10 -4
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js.map +1 -1
  13. package/dist/internal/machine/configuration.d.ts.map +1 -1
  14. package/dist/internal/machine/configuration.js +103 -20
  15. package/dist/internal/machine/configuration.js.map +1 -1
  16. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  17. package/dist/internal/machine/executionPlan.js +31 -0
  18. package/dist/internal/machine/executionPlan.js.map +1 -1
  19. package/dist/internal/machine/invocation.d.ts +10 -2
  20. package/dist/internal/machine/invocation.d.ts.map +1 -1
  21. package/dist/internal/machine/invocation.js +93 -34
  22. package/dist/internal/machine/invocation.js.map +1 -1
  23. package/dist/internal/machine/invocationEvent.d.ts +39 -0
  24. package/dist/internal/machine/invocationEvent.d.ts.map +1 -0
  25. package/dist/internal/machine/invocationEvent.js +43 -0
  26. package/dist/internal/machine/invocationEvent.js.map +1 -0
  27. package/dist/internal/machine/machine.d.ts +6 -50
  28. package/dist/internal/machine/machine.d.ts.map +1 -1
  29. package/dist/internal/machine/machine.js +30 -81
  30. package/dist/internal/machine/machine.js.map +1 -1
  31. package/dist/internal/machine/planner.d.ts +12 -1
  32. package/dist/internal/machine/planner.d.ts.map +1 -1
  33. package/dist/internal/machine/planner.js +90 -37
  34. package/dist/internal/machine/planner.js.map +1 -1
  35. package/dist/internal/machine/protocol.d.ts +9 -0
  36. package/dist/internal/machine/protocol.d.ts.map +1 -1
  37. package/dist/internal/machine/protocol.js +114 -0
  38. package/dist/internal/machine/protocol.js.map +1 -1
  39. package/dist/internal/machine/serialization.d.ts.map +1 -1
  40. package/dist/internal/machine/serialization.js +53 -21
  41. package/dist/internal/machine/serialization.js.map +1 -1
  42. package/dist/internal/machine/stateDefinition.d.ts +4 -4
  43. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  44. package/dist/internal/machine/stateDefinition.js +18 -11
  45. package/dist/internal/machine/stateDefinition.js.map +1 -1
  46. package/dist/internal/machine/symbols.d.ts +2 -0
  47. package/dist/internal/machine/symbols.d.ts.map +1 -1
  48. package/dist/internal/machine/symbols.js +2 -0
  49. package/dist/internal/machine/symbols.js.map +1 -1
  50. package/dist/internal/machine/topology.d.ts +5 -5
  51. package/dist/internal/machine/topology.d.ts.map +1 -1
  52. package/dist/internal/machine/topology.js +39 -13
  53. package/dist/internal/machine/topology.js.map +1 -1
  54. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  55. package/dist/internal/testing/machine/verification.js +10 -3
  56. package/dist/internal/testing/machine/verification.js.map +1 -1
  57. package/dist/unstable/reactivity/AtomMachine.d.ts +11 -5
  58. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  59. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  60. package/docs/agent-guide.md +174 -121
  61. package/package.json +3 -3
  62. package/src/Machine.ts +1336 -872
  63. package/src/internal/machine/activities.ts +48 -33
  64. package/src/internal/machine/atom.ts +13 -6
  65. package/src/internal/machine/configuration.ts +102 -23
  66. package/src/internal/machine/executionPlan.ts +35 -0
  67. package/src/internal/machine/invocation.ts +180 -49
  68. package/src/internal/machine/invocationEvent.ts +72 -0
  69. package/src/internal/machine/machine.ts +105 -403
  70. package/src/internal/machine/planner.ts +110 -48
  71. package/src/internal/machine/protocol.ts +149 -0
  72. package/src/internal/machine/serialization.ts +56 -31
  73. package/src/internal/machine/stateDefinition.ts +22 -11
  74. package/src/internal/machine/symbols.ts +3 -0
  75. package/src/internal/machine/topology.ts +44 -18
  76. package/src/internal/testing/machine/verification.ts +13 -3
  77. 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 initializer = machine.handlers[path]?.initial
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
- values.set(child.path, decodeStateValueSync(machine, child, initialized.value))
223
- commands.push(...initialized.commands)
224
- raisedEvents.push(...initialized.raisedEvents)
225
- emittedEvents.push(...initialized.emittedEvents)
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 initializer = machine.handlers[path]?.initial
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 initialized = collectStateInitializer(machine, initializer, {
242
- state: getActiveValue(current, path),
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
- values.set(
257
- child.path,
258
- decodeStateValueSync(machine, child, (initialized.value as Record<string, unknown>)[child.key])
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: getActiveValue(configuration, path) as Machine.StateByIdentifier<States, StateId>,
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: getActiveValue(configuration, path) as Machine.StateByIdentifier<States, StateId>,
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: getActiveValue(configuration, path) as Machine.StateByIdentifier<States, StateId>,
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: getActiveValue(configuration, path) as Machine.StateByIdentifier<
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
- values[String(current.path)] = current.value
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 = selectEventTransitions<States, Events, Emits, E, R>(
1824
- machine,
1825
- configuration,
1826
- decodedEvent as Machine.EventByTag<Events, Machine.TagOf<Events[number]>>
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
- active.push({
224
- path,
225
- value: yield* encodeBoundary(machine, getStateNodeSchema(node), getActiveValue(configuration, path), {
226
- boundary: "state",
227
- state: path
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
- encodedValues[path] = yield* encodeBoundary(
306
- machine,
307
- getStateNodeSchema(stateNode),
308
- record.values.get(path),
309
- { boundary: "history", state: path }
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 (record.values.size !== record.active.size) {
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
- values.set(
360
- entry.path,
361
- yield* decodeEncodedBoundary(machine, getStateNodeSchema(node), entry.value, {
362
- boundary: "state",
363
- state: entry.path
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
- rememberedValues.set(
414
- path,
415
- yield* decodeEncodedBoundary(machine, getStateNodeSchema(stateNode), encodedRecord.values[path], {
416
- boundary: "history",
417
- state: path
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 !== rememberedActive.size) {
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 validatePseudoAnnotations = (
155
+ const validateSchemaLessAnnotations = (
156
156
  boundary: StateDefinitionBoundary,
157
157
  path: string,
158
158
  annotations: unknown
159
159
  ): void => {
160
- assertPlainRecord(boundary, `${path}.annotations`, annotations, "pseudo-state 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(boundary, `${path}.annotations`, `pseudo-state annotations cannot declare property "${String(property)}"`)
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)}`, "pseudo-state annotation values must be strings")
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
- validatePseudoAnnotations(boundary, path, node.annotations)
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
- if (!hasOwn(node, "schema") || !isTaggedSchema(node.schema)) {
223
- fail(boundary, `${path}.schema`, "active states must declare an Effect Schema with a required PropertyKey _tag")
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\"")