@typeonce/effect-machine 0.31.2 → 0.33.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 (151) hide show
  1. package/README.md +168 -162
  2. package/dist/Machine.d.ts +495 -284
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +91 -111
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +3 -4
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +0 -2
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/childRegistry.d.ts +52 -0
  11. package/dist/internal/machine/childRegistry.d.ts.map +1 -0
  12. package/dist/internal/machine/childRegistry.js +74 -0
  13. package/dist/internal/machine/childRegistry.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +2 -3
  15. package/dist/internal/machine/cluster.d.ts.map +1 -1
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/configuration.d.ts.map +1 -1
  18. package/dist/internal/machine/configuration.js +14 -4
  19. package/dist/internal/machine/configuration.js.map +1 -1
  20. package/dist/internal/machine/executionPlan.d.ts +1 -0
  21. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  22. package/dist/internal/machine/executionPlan.js +97 -53
  23. package/dist/internal/machine/executionPlan.js.map +1 -1
  24. package/dist/internal/machine/invocation.d.ts.map +1 -1
  25. package/dist/internal/machine/invocation.js +4 -2
  26. package/dist/internal/machine/invocation.js.map +1 -1
  27. package/dist/internal/machine/invocationDefinition.d.ts +35 -0
  28. package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
  29. package/dist/internal/machine/invocationDefinition.js +21 -0
  30. package/dist/internal/machine/invocationDefinition.js.map +1 -0
  31. package/dist/internal/machine/invocationEvent.d.ts +0 -2
  32. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  33. package/dist/internal/machine/invocationEvent.js +0 -8
  34. package/dist/internal/machine/invocationEvent.js.map +1 -1
  35. package/dist/internal/machine/machine.d.ts +21 -43
  36. package/dist/internal/machine/machine.d.ts.map +1 -1
  37. package/dist/internal/machine/machine.js +134 -403
  38. package/dist/internal/machine/machine.js.map +1 -1
  39. package/dist/internal/machine/planner.d.ts +13 -19
  40. package/dist/internal/machine/planner.d.ts.map +1 -1
  41. package/dist/internal/machine/planner.js +63 -35
  42. package/dist/internal/machine/planner.js.map +1 -1
  43. package/dist/internal/machine/process.d.ts +6 -8
  44. package/dist/internal/machine/process.d.ts.map +1 -1
  45. package/dist/internal/machine/process.js +1 -0
  46. package/dist/internal/machine/process.js.map +1 -1
  47. package/dist/internal/machine/protocol.d.ts +2 -2
  48. package/dist/internal/machine/protocol.d.ts.map +1 -1
  49. package/dist/internal/machine/protocol.js +4 -18
  50. package/dist/internal/machine/protocol.js.map +1 -1
  51. package/dist/internal/machine/readiness.d.ts +1 -1
  52. package/dist/internal/machine/readiness.d.ts.map +1 -1
  53. package/dist/internal/machine/requirements.d.ts +4 -0
  54. package/dist/internal/machine/requirements.d.ts.map +1 -0
  55. package/dist/internal/machine/requirements.js +2 -0
  56. package/dist/internal/machine/requirements.js.map +1 -0
  57. package/dist/internal/machine/runtime.d.ts +4 -58
  58. package/dist/internal/machine/runtime.d.ts.map +1 -1
  59. package/dist/internal/machine/runtime.js +1 -69
  60. package/dist/internal/machine/runtime.js.map +1 -1
  61. package/dist/internal/machine/serialization.d.ts.map +1 -1
  62. package/dist/internal/machine/serialization.js +16 -0
  63. package/dist/internal/machine/serialization.js.map +1 -1
  64. package/dist/internal/machine/stateDefinition.d.ts +3 -1
  65. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  66. package/dist/internal/machine/stateDefinition.js +36 -2
  67. package/dist/internal/machine/stateDefinition.js.map +1 -1
  68. package/dist/internal/machine/targetBuilder.d.ts +16 -0
  69. package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
  70. package/dist/internal/machine/targetBuilder.js +333 -0
  71. package/dist/internal/machine/targetBuilder.js.map +1 -0
  72. package/dist/internal/machine/topology.js +3 -3
  73. package/dist/internal/machine/topology.js.map +1 -1
  74. package/dist/internal/machine/transition.d.ts +16 -0
  75. package/dist/internal/machine/transition.d.ts.map +1 -0
  76. package/dist/internal/machine/transition.js +2 -0
  77. package/dist/internal/machine/transition.js.map +1 -0
  78. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  79. package/dist/internal/testing/machine/finiteModel.js +11 -13
  80. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  81. package/dist/internal/testing/machine/format.d.ts +7 -0
  82. package/dist/internal/testing/machine/format.d.ts.map +1 -0
  83. package/dist/internal/testing/machine/format.js +104 -0
  84. package/dist/internal/testing/machine/format.js.map +1 -0
  85. package/dist/internal/testing/machine/probe.d.ts +1 -1
  86. package/dist/internal/testing/machine/probe.d.ts.map +1 -1
  87. package/dist/internal/testing/machine/probe.js +1 -1
  88. package/dist/internal/testing/machine/probe.js.map +1 -1
  89. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
  90. package/dist/internal/testing/machine/referenceModel.js +15 -19
  91. package/dist/internal/testing/machine/referenceModel.js.map +1 -1
  92. package/dist/internal/testing/machine/trace.d.ts +1 -1
  93. package/dist/internal/testing/machine/trace.d.ts.map +1 -1
  94. package/dist/internal/testing/machine/trace.js +3 -3
  95. package/dist/internal/testing/machine/trace.js.map +1 -1
  96. package/dist/internal/testing/machine/value.d.ts +8 -0
  97. package/dist/internal/testing/machine/value.d.ts.map +1 -0
  98. package/dist/internal/testing/machine/value.js +82 -0
  99. package/dist/internal/testing/machine/value.js.map +1 -0
  100. package/dist/internal/testing/machine/verification.d.ts +2 -13
  101. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  102. package/dist/internal/testing/machine/verification.js +21 -113
  103. package/dist/internal/testing/machine/verification.js.map +1 -1
  104. package/dist/testing/MachineTest.d.ts +89 -104
  105. package/dist/testing/MachineTest.d.ts.map +1 -1
  106. package/dist/testing/MachineTest.js +70 -84
  107. package/dist/testing/MachineTest.js.map +1 -1
  108. package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
  109. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  110. package/dist/unstable/cluster/ClusterMachine.js +5 -8
  111. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  112. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
  113. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  114. package/dist/unstable/reactivity/AtomMachine.js +17 -26
  115. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  116. package/docs/agent-guide.md +45 -45
  117. package/docs/effect-atom-react.md +35 -63
  118. package/docs/machine-review.md +7 -7
  119. package/docs/root-api.md +259 -0
  120. package/package.json +1 -1
  121. package/src/Machine.ts +1053 -538
  122. package/src/internal/machine/atom.ts +3 -13
  123. package/src/internal/machine/childRegistry.ts +141 -0
  124. package/src/internal/machine/cluster.ts +5 -14
  125. package/src/internal/machine/configuration.ts +20 -4
  126. package/src/internal/machine/executionPlan.ts +109 -52
  127. package/src/internal/machine/invocation.ts +10 -6
  128. package/src/internal/machine/invocationDefinition.ts +52 -0
  129. package/src/internal/machine/invocationEvent.ts +0 -8
  130. package/src/internal/machine/machine.ts +222 -716
  131. package/src/internal/machine/planner.ts +74 -62
  132. package/src/internal/machine/process.ts +14 -21
  133. package/src/internal/machine/protocol.ts +9 -32
  134. package/src/internal/machine/readiness.ts +1 -0
  135. package/src/internal/machine/requirements.ts +11 -0
  136. package/src/internal/machine/runtime.ts +17 -186
  137. package/src/internal/machine/serialization.ts +22 -0
  138. package/src/internal/machine/stateDefinition.ts +36 -3
  139. package/src/internal/machine/targetBuilder.ts +525 -0
  140. package/src/internal/machine/topology.ts +3 -3
  141. package/src/internal/machine/transition.ts +26 -0
  142. package/src/internal/testing/machine/finiteModel.ts +23 -24
  143. package/src/internal/testing/machine/format.ts +120 -0
  144. package/src/internal/testing/machine/probe.ts +4 -4
  145. package/src/internal/testing/machine/referenceModel.ts +25 -17
  146. package/src/internal/testing/machine/trace.ts +4 -4
  147. package/src/internal/testing/machine/value.ts +77 -0
  148. package/src/internal/testing/machine/verification.ts +904 -1109
  149. package/src/testing/MachineTest.ts +103 -114
  150. package/src/unstable/cluster/ClusterMachine.ts +10 -20
  151. package/src/unstable/reactivity/AtomMachine.ts +42 -79
package/README.md CHANGED
@@ -42,57 +42,42 @@ in lockstep with this package.
42
42
 
43
43
  ## Quick start
44
44
 
45
- Define schemas first, derive the state topology, then add behavior:
45
+ Start with events and root-owned data. A data-only machine needs no child states:
46
46
 
47
47
  ```ts
48
48
  import { Machine } from "@typeonce/effect-machine"
49
- import { Effect, Schema, Stream } from "effect"
50
-
51
- const State = Schema.TaggedUnion({
52
- Running: { count: Schema.Number }
53
- })
54
-
55
- const States = Machine.states({
56
- Idle: {},
57
- Running: State.cases.Running
58
- })
59
-
60
- const CounterEvent = Machine.events(
61
- Schema.TaggedUnion({
62
- Start: {},
63
- Increment: {},
64
- Stop: {}
65
- })
66
- )
49
+ import { Effect, Schema } from "effect"
67
50
 
51
+ const Root = Machine.state({ fields: { count: Schema.Number } })
52
+ const Events = Machine.events({ Increment: { by: Schema.Number } })
68
53
  const CounterDefinition = Machine.make({
69
- id: "Counter",
70
- states: States.states,
71
- events: CounterEvent,
72
- initial: (to) => to.Idle()
54
+ root: Root,
55
+ events: Events,
56
+ initial: (root) => root.from(() => ({ count: 0 }))
73
57
  })
74
-
75
58
  const Counter = CounterDefinition.handle({
76
- Idle: {
77
- on: {
78
- Start: (to) => to.full.Running().resolve(({ target }) => target.from({ count: 0 }))
79
- }
80
- },
81
- Running: {
82
- on: {
83
- Increment: (to) => to.full.Running().resolve(({ state, target }) => target.from({ count: state.count + 1 })),
84
- Stop: (to) => to.full.Idle()
85
- }
59
+ on: {
60
+ Increment: (to) =>
61
+ to.self.update.from(({ current, event }) => ({
62
+ count: current.count + event.by
63
+ }))
86
64
  }
87
65
  })
88
66
 
89
- const program = Effect.gen(function*() {
67
+ const program = Effect.scoped(Effect.gen(function*() {
90
68
  const ref = yield* Machine.start(Counter)
91
- yield* ref.send(CounterEvent.Start())
92
- yield* ref.send(CounterEvent.Increment())
93
- })
69
+ yield* ref.send(Events.Increment({ by: 1 }))
70
+ }))
94
71
  ```
95
72
 
73
+ `self.update` replaces the owner's complete value without exiting and reentering
74
+ it. Its children and scoped work remain active. Add `initial` and `states` to
75
+ the same root descriptor when the workflow needs distinct modes; the root data
76
+ survives transitions between those children.
77
+
78
+ See [the root API guide](./docs/root-api.md) for topology-only machines,
79
+ initialization, reusable schemas, guards, migration, and observation contracts.
80
+
96
81
  `handle` creates a complete implementation boundary. Its result does not
97
82
  expose `handle`, so all behavior for one machine belongs in the same handler
98
83
  tree. Reuse the definition when multiple independent implementations are
@@ -109,26 +94,25 @@ const TestingCounter = CounterDefinition.handle(testingHandlers)
109
94
 
110
95
  ## Modeling workflow
111
96
 
112
- Use this order to preserve inference and keep boundaries explicit:
113
-
114
- 1. Define domain schemas used by state and by shared event fields.
115
- 2. Declare topology with `Machine.states`, naming a tagged state union
116
- when its `.cases` are reused.
117
- 3. Create event descriptors with `Machine.events`, `Machine.internalEvents`,
118
- and `Machine.emittedEvents`, passing tagged unions or tagged classes directly.
119
- 4. Create the machine and implement every active state with
120
- `Machine.make({...}).handle({...})`.
121
- 5. Add child descriptors, then runtime, Atom, testing, or cluster adapters at
122
- the application boundary.
123
-
124
- Keep one-off topology inline in `Machine.states`. Use `Machine.state` only when
125
- the same active state definition is mounted more than once; tagged schemas are
126
- already reusable without it. For repeated finite regions, derive names with
127
- `States.path(...)` so every literal in the path family is checked against the
128
- complete tree. Type full-snapshot helpers as `Machine.Snapshot<typeof States>`
129
- or `Machine.Snapshot<typeof machine>`, schema-backed state payloads as
130
- `Machine.Value<typeof States, Path>`, and path-rooted snapshots as
131
- `Machine.SnapshotAt<typeof States, Path>`.
97
+ 1. Define reusable domain schemas where they express a domain boundary.
98
+ 2. Declare one root with `Machine.state`. Use inline children or mount reusable
99
+ descriptors under `states`. `fields` builds a tagged schema; `schema` keeps
100
+ an existing schema and its construction behavior.
101
+ 3. Declare field records with `Machine.events`, `Machine.internalEvents`, and
102
+ `Machine.emittedEvents`. Import existing tagged unions or tagged classes with
103
+ the corresponding `eventsFromSchemas`, `internalEventsFromSchemas`, or
104
+ `emittedEventsFromSchemas` constructor.
105
+ 4. Pass the root descriptor to `Machine.make({ root, events })`. Supply root
106
+ values with `initial`; declared child defaults determine startup topology.
107
+ 5. Implement root behavior directly in `handle`, with child behavior in its
108
+ `states` property. Add runtime, Atom, testing, or Cluster adapters at the
109
+ application boundary.
110
+
111
+ The root has path `""`; children retain paths such as `Editing` and
112
+ `Editing.Form`. Use `Root.path(...)` to validate path literals. Type full
113
+ snapshots as `Machine.Snapshot<typeof Root>` or `Machine.Snapshot<typeof machine>`,
114
+ state values as `Machine.Value<typeof Root, Path>`, and selected snapshots as
115
+ `Machine.SnapshotAt<typeof Root, Path>`.
132
116
 
133
117
  ### Make invalid states unrepresentable
134
118
 
@@ -187,20 +171,24 @@ Omit `schema` when a state represents control flow but owns no data. Use `{}`
187
171
  instead of defining an empty tagged schema:
188
172
 
189
173
  ```ts
190
- const States = Machine.states({
191
- Form: {
192
- initial: "Editing",
193
- states: {
194
- Editing: {},
195
- Saving
174
+ const States = Machine.state({
175
+ initial: "Form",
176
+ states: {
177
+ Form: {
178
+ initial: "Editing",
179
+ states: {
180
+ Editing: {},
181
+ Saving
182
+ }
196
183
  }
197
184
  }
198
185
  })
199
186
 
200
187
  const definition = Machine.make({
201
- states: States.states,
202
- events: Machine.events(),
203
- initial: (to) => to.Form.initial.resolve(({ target }) => target.from((form) => form.Editing.from()))
188
+ root: States,
189
+ events: Machine.eventsFromSchemas(),
190
+ initialConfiguration: (root) =>
191
+ root.resolve(({ target }) => target.from((to) => to.Form.from((form) => form.Editing.from())))
204
192
  })
205
193
  ```
206
194
 
@@ -212,7 +200,7 @@ schema-backed paths. Add a schema later if the state starts owning data.
212
200
  Keep data-bearing state schemas together in a named `Schema.TaggedUnion` and
213
201
  reference its cases from the topology. For a standalone state schema whose
214
202
  class identity is useful, declare a named `Schema.TaggedClass`. Do not bury
215
- one-off tagged schema declarations inside `Machine.states`.
203
+ inline `fields` in a `Machine.state` descriptor.
216
204
 
217
205
  Put data on the narrowest state where it is valid. If sibling phases share
218
206
  data, put it on their compound parent.
@@ -224,24 +212,24 @@ belong in `internalEvents`. Ephemeral outward notifications have their own
224
212
  `emittedEvents` protocol:
225
213
 
226
214
  ```ts
227
- export const CommandEvent = Machine.events(
215
+ export const CommandEvent = Machine.eventsFromSchemas(
228
216
  Schema.TaggedUnion({ Save: {} })
229
217
  )
230
218
  export type PublicCommandEvent = Machine.EventOf<typeof CommandEvent>
231
- const InternalEvent = Machine.internalEvents(
219
+ const InternalEvent = Machine.internalEventsFromSchemas(
232
220
  Schema.TaggedUnion({
233
221
  Saved: { id: Schema.String },
234
222
  SaveFailed: { message: Schema.String }
235
223
  })
236
224
  )
237
- const Emissions = Machine.emittedEvents(
225
+ const Emissions = Machine.emittedEventsFromSchemas(
238
226
  Schema.TaggedUnion({
239
227
  SaveObserved: { id: Schema.String }
240
228
  })
241
229
  )
242
230
 
243
231
  const definition = Machine.make({
244
- states: States.states,
232
+ root: States,
245
233
  events: CommandEvent,
246
234
  internalEvents: InternalEvent,
247
235
  emittedEvents: Emissions,
@@ -352,28 +340,30 @@ machine mailbox and is processed later. A machine that requires an owner
352
340
  declares the subset of parent inputs it may send with `Machine.parent`:
353
341
 
354
342
  ```ts
355
- const ParentEvents = Machine.events(ChildFinished)
343
+ const ParentEvents = Machine.eventsFromSchemas(ChildFinished)
356
344
 
357
345
  const child = Machine.make({
358
- states: ChildStates.states,
346
+ root: ChildStates,
359
347
  events: ChildEvents,
360
348
  parent: Machine.parent(ParentEvents),
361
349
  initial: (to) => to.Working()
362
350
  }).handle({
363
- Working: {
364
- on: {
365
- Finish: (to) =>
366
- to.full.Done().resolve(({ parent, target }, enqueue) => {
367
- enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
368
- return target.from()
369
- })
370
- }
371
- },
372
- Done: {}
351
+ states: {
352
+ Working: {
353
+ on: {
354
+ Finish: (to) =>
355
+ to.branch.Done().resolve(({ parent, target }, enqueue) => {
356
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
357
+ return target.from()
358
+ })
359
+ }
360
+ },
361
+ Done: {}
362
+ }
373
363
  })
374
364
 
375
365
  const Child = Machine.child("worker", child)
376
- const ParentInputs = Machine.events(Start, ParentEvents)
366
+ const ParentInputs = Machine.eventsFromSchemas(Start, ParentEvents)
377
367
  ```
378
368
 
379
369
  `parent` is statically present in every child callback, and root APIs such as
@@ -409,7 +399,7 @@ paths. `parent` always means the owning machine target.
409
399
  | `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
410
400
 
411
401
  Every required transition handler selects a target from its inline `to`
412
- builder. Return a bare selection such as `to.full.Idle()` when the selected
402
+ builder. Return a bare selection such as `to.local.Idle()` when the selected
413
403
  builder supports zero-argument construction; the machine applies the same
414
404
  default construction as `target.from()`. This includes empty schemas and
415
405
  schemas whose constructor fields are all optional or defaulted. TypeScript
@@ -421,13 +411,14 @@ selecting a destination. Concrete destinations stay narrowed inside their
421
411
  resolver, and `to.branches({...})` gives the resolver only the declared named
422
412
  `select` builders. Builders describe the
423
413
  next logical configuration. Shared states exit and enter only when paths
424
- change; call `.reenter()` for resolver-free reentry or pass `{ reenter: true }`
425
- to `.resolve(...)` when the source must restart. With `to.none`, reentry
414
+ change; call `.reenter()` before `.from(...)`, `.decoded(...)`, or `.resolve(...)`
415
+ when the source must restart. Default-constructible targets can finish at
416
+ `.reenter()`. With `to.none`, reentry
426
417
  restarts the source while retaining its configuration.
427
418
 
428
419
  Topology-only definition instructions are values: `to.none`, declared
429
420
  `.initial` and history selections, and `to.local.with`. Concrete state and
430
- choice destinations remain calls such as `to.full.Running()`. Runtime named
421
+ choice destinations remain calls such as `to.local.Running()`. Runtime named
431
422
  branch builders remain callable, including `select.unchanged()`, because their
432
423
  result carries the selected branch evidence.
433
424
 
@@ -440,7 +431,7 @@ handler source:
440
431
 
441
432
  ```ts
442
433
  const handlers = {
443
- Increment: (to) => to.branch.root.session.update(({ current, owner }) => owner.from({ count: current.count + 1 }))
434
+ Increment: (to) => to.branch.root.session.update.from(({ current }) => ({ count: current.count + 1 }))
444
435
  }
445
436
  ```
446
437
 
@@ -473,30 +464,28 @@ const handlers = {
473
464
  CreatePlan: (to) =>
474
465
  to.local.SavingPlan()
475
466
  .updating(to.branch.Ready)
476
- .resolve(({ current, event, owner, target }) =>
477
- target.from({
478
- request: { _tag: "Create", input: event.input }
479
- }).update(
480
- owner.decoded(new Ready({ ...current, notice: null }))
481
- )
482
- )
467
+ .from(({ current, event }) => ({
468
+ target: { request: { _tag: "Create", input: event.input } },
469
+ update: { ...current, notice: null }
470
+ }))
483
471
  }
484
472
  ```
485
473
 
486
474
  `to.local.SavingPlan()` selects topology. `.updating(to.branch.Ready)` names
487
- the retained valued owner and makes its replacement mandatory: the resolver
488
- does not type-check unless destination construction finishes with
489
- `.update(...)`. `current` is that owner's decoded value from the
490
- pre-transition snapshot. `target` constructs the destination; `owner`
491
- constructs the complete replacement owner value.
475
+ the retained valued owner and makes its replacement mandatory. `.from(...)`
476
+ returns `{ target, update }` with constructor inputs for both values;
477
+ `.decoded(...)` returns already decoded values for both. `current` is that
478
+ owner's decoded value from the pre-transition snapshot. Use `.resolve(...)`
479
+ when constructing explicit children, mixing construction methods, or queuing
480
+ commands; its `target` and `owner` builders construct the two values.
492
481
 
493
482
  The topology change and owner replacement apply atomically in one microstep.
494
483
  The owner does not exit or reenter, its work is not restarted, and destination
495
484
  entry actions observe the new owner value. Eventless stabilization follows.
496
485
  Only one retained owner may be replaced by a combined target. A `full` target,
497
486
  or any target that exits the selected owner, does not expose `.updating`.
498
- Combined updates use a direct resolver in this release; named branches continue
499
- to support value-only updates.
487
+ Named branches support value-only updates; a combined update declares its
488
+ destination directly.
500
489
 
501
490
  For a schema-less destination, construction remains explicit:
502
491
 
@@ -521,11 +510,13 @@ before lifecycle actions run. Competing transitions that write the same owner
521
510
  conflict; document order and hierarchy select one writer rather than applying
522
511
  last-write-wins behavior.
523
512
 
524
- The resolver must return `target.decoded(value)` or `target.from(input)`. It
525
- may return `decline()` only with `{ declinable: true }`. Pass `{ reenter: true }`
526
- on event or invocation transitions when the handler source should exit and
527
- enter again. Reentry applies to that source, not to the ancestor whose value
528
- changed.
513
+ Use `.guard(predicate)` before construction to decline an update without
514
+ constructing values or queuing commands. It is available on standalone and
515
+ combined updates. A false guard allows ancestor fallback. A resolver may also
516
+ return `decline()` with `{ declinable: true }` for decisions during resolution.
517
+ Call `.reenter()` before construction on event or invocation transitions when
518
+ the handler source should exit and enter again. Reentry applies to that source,
519
+ not to the retained ancestor whose value changed.
529
520
 
530
521
  The selector omits `update` for schema-less scopes, atomic and final states,
531
522
  inactive branches, parallel sibling regions, and choice resolvers. Updating a
@@ -574,7 +565,7 @@ lifecycle do not run.
574
565
 
575
566
  ## Statechart capabilities
576
567
 
577
- `Machine.states` supports:
568
+ `Machine.state` supports:
578
569
 
579
570
  - atomic states;
580
571
  - compound states with one active child;
@@ -599,16 +590,18 @@ State-scoped work starts on entry and is interrupted on exit:
599
590
 
600
591
  ```ts
601
592
  machine.handle({
602
- Loading: {
603
- invoke: (from) =>
604
- from.effect("save-document", () => saveDocument)
605
- .onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ id: output.id })))
606
- .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) })))
607
- },
608
- Waiting: {
609
- invoke: (from) =>
610
- from.timer("save-timeout", "3 seconds")
611
- .onDone((to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" })))
593
+ states: {
594
+ Loading: {
595
+ invoke: (from) =>
596
+ from.effect("save-document", () => saveDocument)
597
+ .onDone((to) => to.branch.Saved().resolve(({ output, target }) => target.from({ id: output.id })))
598
+ .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ message: String(error) })))
599
+ },
600
+ Waiting: {
601
+ invoke: (from) =>
602
+ from.timer("save-timeout", "3 seconds")
603
+ .onDone((to) => to.branch.Failed().resolve(({ target }) => target.from({ message: "Timed out" })))
604
+ }
612
605
  }
613
606
  })
614
607
  ```
@@ -621,11 +614,13 @@ the completed chain is the value returned by the callback:
621
614
 
622
615
  ```ts
623
616
  machine.handle({
624
- Loading: {
625
- invoke: (from) =>
626
- from.effect("load-document", ({ state }) => loadDocument(state.documentId))
627
- .onDone((to) => to.full.Ready().resolve(({ output, target }) => target.from({ document: output })))
628
- .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: error.message })))
617
+ states: {
618
+ Loading: {
619
+ invoke: (from) =>
620
+ from.effect("load-document", ({ state }) => loadDocument(state.documentId))
621
+ .onDone((to) => to.branch.Ready().resolve(({ output, target }) => target.from({ document: output })))
622
+ .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ message: error.message })))
623
+ }
629
624
  }
630
625
  })
631
626
  ```
@@ -636,16 +631,18 @@ macrostep commits:
636
631
 
637
632
  ```ts
638
633
  machine.handle({
639
- Listening: {
640
- invoke: (from) =>
641
- from.stream("channel", () => channelMessages)
642
- .onElement((to) =>
643
- to.none.resolve(({ element }, enqueue) => {
644
- enqueue.raise(Events.MessageReceived({ message: element }))
645
- })
646
- )
647
- .onDone((to) => to.none)
648
- .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ error })))
634
+ states: {
635
+ Listening: {
636
+ invoke: (from) =>
637
+ from.stream("channel", () => channelMessages)
638
+ .onElement((to) =>
639
+ to.none.resolve(({ element }, enqueue) => {
640
+ enqueue.raise(Events.MessageReceived({ message: element }))
641
+ })
642
+ )
643
+ .onDone((to) => to.none)
644
+ .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ error })))
645
+ }
649
646
  }
650
647
  })
651
648
  ```
@@ -667,16 +664,18 @@ const machine = Machine.make({
667
664
  parent: Machine.parent(ParentEvents)
668
665
  // ...
669
666
  }).handle({
670
- Saving: {
671
- invoke: (from) =>
672
- from.effect("notify-parent", () => saveDocument)
673
- .onDone((to) =>
674
- to.none.resolve(({ parent, self }, enqueue) => {
675
- enqueue.sendTo(self, Commands.Save())
676
- enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
677
- })
678
- )
679
- .onFailure((to) => to.none)
667
+ states: {
668
+ Saving: {
669
+ invoke: (from) =>
670
+ from.effect("notify-parent", () => saveDocument)
671
+ .onDone((to) =>
672
+ to.none.resolve(({ parent, self }, enqueue) => {
673
+ enqueue.sendTo(self, Commands.Save())
674
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
675
+ })
676
+ )
677
+ .onFailure((to) => to.none)
678
+ }
680
679
  }
681
680
  })
682
681
  ```
@@ -690,11 +689,13 @@ transitions retain the exact owning state and machine protocols.
690
689
  const refreshCache = Cache.refresh
691
690
 
692
691
  machine.handle({
693
- Active: {
694
- invoke: (from) => [
695
- from.effect("refresh-cache", () => refreshCache).onDone((to) => to.none).onFailure((to) => to.none),
696
- from.timer("expire-session", "5 minutes").onDone((to) => to.full.Expired())
697
- ]
692
+ states: {
693
+ Active: {
694
+ invoke: (from) => [
695
+ from.effect("refresh-cache", () => refreshCache).onDone((to) => to.none).onFailure((to) => to.none),
696
+ from.timer("expire-session", "5 minutes").onDone((to) => to.branch.Expired())
697
+ ]
698
+ }
698
699
  }
699
700
  })
700
701
  ```
@@ -720,19 +721,21 @@ open set of children that must survive state changes:
720
721
  const Plant = Machine.childFamily(plantMachine)
721
722
 
722
723
  const central = Machine.make({
723
- events: Machine.events(ResourcesOffered, PlantBroken)
724
+ events: Machine.eventsFromSchemas(ResourcesOffered, PlantBroken)
724
725
  // ...
725
726
  }).handle({
726
- Commissioning: {
727
- invoke: (from) =>
728
- from.effect("commission-wave", ({ children, state }) =>
729
- Effect.forEach(
730
- state.plants,
731
- (input) => children.spawn(Plant(input.id), { input }),
732
- { discard: true }
733
- ))
734
- .onDone((to) => to.full.Operating())
735
- .onFailure((to) => to.full.CommissioningFailed())
727
+ states: {
728
+ Commissioning: {
729
+ invoke: (from) =>
730
+ from.effect("commission-wave", ({ children, state }) =>
731
+ Effect.forEach(
732
+ state.plants,
733
+ (input) => children.spawn(Plant(input.id), { input }),
734
+ { discard: true }
735
+ ))
736
+ .onDone((to) => to.branch.Operating())
737
+ .onFailure((to) => to.branch.CommissioningFailed())
738
+ }
736
739
  }
737
740
  })
738
741
  ```
@@ -857,7 +860,10 @@ or child instance.
857
860
 
858
861
  ## Persistence
859
862
 
860
- Logical snapshots can be validated for storage or transport:
863
+ Logical snapshots use codec version 2, including the root at path `""`. Earlier
864
+ encoded snapshots are rejected; migrate persisted data explicitly before decoding.
865
+
866
+ Snapshots can be validated for storage or transport:
861
867
 
862
868
  ```ts
863
869
  const encoded = yield * Machine.encodeSnapshot(machine, snapshot)