@typeonce/effect-machine 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +164 -792
  2. package/docs/agent-guide.md +13 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,52 +2,40 @@
2
2
 
3
3
  Schema-first state machines and statecharts for Effect.
4
4
 
5
- > Early-release software: APIs may change, and releases are coupled to an exact
6
- > Effect beta.
5
+ State, event, input, output, and persistence boundaries are described with
6
+ Effect Schema. The same definition can be planned synchronously, run as a
7
+ managed machine, mounted as an Atom, tested as a model, or hosted by the
8
+ cluster adapter.
7
9
 
8
- ## Installation
10
+ > This is early-release software. Its API may change, and each release targets
11
+ > one exact Effect beta.
12
+
13
+ ## Install
9
14
 
10
15
  ```sh
11
16
  pnpm add @typeonce/effect-machine effect@4.0.0-beta.107
12
17
  ```
13
18
 
14
- `effect` is an exact peer dependency, not a bundled runtime dependency.
15
- Consumers must install `effect@4.0.0-beta.107`. Upgrading this package may
16
- require upgrading Effect in lockstep; do not override the peer to another beta.
19
+ `effect` is an exact peer dependency. Install the version above and upgrade it
20
+ in lockstep with this package.
17
21
 
18
- ## Entrypoints
22
+ ## Quick start
19
23
 
20
- ```ts
21
- import { Machine } from "@typeonce/effect-machine"
22
- import { ClusterMachine } from "@typeonce/effect-machine/cluster"
23
- import { AtomMachine } from "@typeonce/effect-machine/reactivity"
24
- import { MachineTest } from "@typeonce/effect-machine/testing"
25
- ```
26
-
27
- Each ESM entrypoint is independent and tree-shakeable. Importing the root does
28
- not load the reactivity, cluster, or testing modules.
29
-
30
- ## First machine
31
-
32
- Schemas provide runtime decoders and the types used by handlers, targets,
33
- inputs, outputs, and running references. The public/internal event distinction
34
- has an additional boundary described below.
35
-
36
- Effect's `Schema.TaggedUnion` is a compact way to declare cases. Its `cases`
37
- property contains the individual tagged schemas, and each case has a typed
38
- `make` constructor.
24
+ Define schemas first, derive the state topology, then add behavior:
39
25
 
40
26
  ```ts
41
27
  import { Machine } from "@typeonce/effect-machine"
42
- import { Schema } from "effect"
28
+ import { Effect, Schema } from "effect"
43
29
 
44
30
  const State = Schema.TaggedUnion({
45
31
  Idle: {},
46
- Running: {}
32
+ Running: { count: Schema.Number }
47
33
  })
48
34
 
49
35
  const Event = Schema.TaggedUnion({
50
- Start: {}
36
+ Start: {},
37
+ Increment: {},
38
+ Stop: {}
51
39
  })
52
40
 
53
41
  const States = Machine.defineStates(State.cases)
@@ -55,575 +43,174 @@ const States = Machine.defineStates(State.cases)
55
43
  const Counter = Machine.make({
56
44
  id: "Counter",
57
45
  states: States.states,
58
- events: [Event.cases.Start],
46
+ events: [Event],
59
47
  initial: () => States.initial.Idle.from()
60
48
  }).handle({
61
49
  Idle: {
62
50
  on: {
63
- Start: ({ target }) => target.full.Running.from()
51
+ Start: ({ target }) => target.full.Running.from({ count: 0 })
64
52
  }
65
53
  },
66
- Running: {}
54
+ Running: {
55
+ on: {
56
+ Increment: ({ state, target }) => target.full.Running.from({ count: state.count + 1 }),
57
+ Stop: ({ target }) => target.full.Idle.from()
58
+ }
59
+ }
60
+ })
61
+
62
+ const program = Effect.gen(function*() {
63
+ const ref = yield* Machine.start(Counter)
64
+ yield* ref.send(Machine.event(Counter, Event.cases.Start))
65
+ yield* ref.send(Machine.event(Counter, Event.cases.Increment))
67
66
  })
68
67
  ```
69
68
 
70
- Handler objects mirror the state definition recursively. `effect-machine` does
71
- not impose a fixed handler-tree depth; inference continues until TypeScript's
72
- normal, shape-dependent compiler limits.
69
+ `Machine.start` returns a `MachineRef` with `send`, `state`, `snapshot`,
70
+ `changes`, `join`, and `stop`. Sending enqueues an event; observe `changes` or
71
+ use the testing probe when work must be causally acknowledged.
73
72
 
74
- `initial` is always a function. For a machine with an input schema, the
75
- initializer receives the decoded input.
73
+ ## Modeling workflow
76
74
 
77
- Builder methods accept an already constructed state value directly, or expose
78
- `.from` for constructing one safely from the state schema's make input:
75
+ Use this order to preserve inference and keep boundaries explicit:
79
76
 
80
- ```ts
81
- target.local.Running(decodedRunning)
82
- target.local.Running.from({ startedAt: event.at })
83
- ```
77
+ 1. Define domain, state, public-event, internal-event, and emitted-event schemas.
78
+ 2. Declare topology with `Machine.defineStates`.
79
+ 3. Create the protocol and initializer with `Machine.make`.
80
+ 4. Implement every active state with `.handle(...)`.
81
+ 5. Add runtime, Atom, testing, or cluster adapters at the application boundary.
84
82
 
85
- Use the direct call when a decoded value already exists. Use `.from` when
86
- entering a state from fields. Construction runs through the schema's
87
- `makeEffect` while the machine plans the configuration, so constructor
88
- defaults and tagged-class identity are preserved and failed refinements become
89
- `MachineSchemaDecodeError` failures instead of synchronous throws. The same
90
- form is available on initial, local, branch, full, compound, parallel, and
91
- final builders. A `.from` builder result is therefore a machine construction
92
- instruction; it becomes a validated public snapshot when planning succeeds.
83
+ ### Construct state through builders
93
84
 
94
- When `{}` is valid constructor input, omit it. Required state fields remain
95
- required, while compound and parallel states still require their active-child
96
- callback:
85
+ Use `.from(...)` when constructing a new state from fields:
97
86
 
98
87
  ```ts
99
- States.initial.Idle.from()
88
+ target.local.Saving.from({ draft: event.draft })
100
89
  States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
101
- States.initial.Flow.from((flow) => flow.Idle.from())
102
90
  ```
103
91
 
104
- Tagged classes are equally valid when cases need class methods or nominal
105
- identity:
92
+ The machine runs these inputs through the state schema while planning. Schema
93
+ defaults, refinements, and tagged-class identity are therefore preserved, and
94
+ decode failures remain typed machine failures. Pass a value directly only when
95
+ it is already decoded, such as a value returned by `Machine.retag`.
106
96
 
107
- ```ts
108
- class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
109
- ```
97
+ Put data on the narrowest state where it is valid. If sibling phases share
98
+ data, put it on their compound parent.
110
99
 
111
- ## Public and internal events
100
+ ### Separate public and internal events
112
101
 
113
- Declare commands that callers may send in `events`. Declare machine-local
114
- deliveries, such as invoke results and child emissions, in `internalEvents`:
102
+ `events` is the public command protocol. Invoke results, timer deliveries,
103
+ raised events, and child emissions belong in `internalEvents`:
115
104
 
116
105
  ```ts
117
- const Command = Schema.TaggedUnion({
118
- Save: {}
119
- })
120
-
121
- const InternalEvent = Schema.TaggedUnion({
106
+ const Command = Schema.TaggedUnion({ Save: {} })
107
+ const Internal = Schema.TaggedUnion({
122
108
  Saved: { id: Schema.String },
123
109
  SaveFailed: { message: Schema.String }
124
110
  })
125
111
 
126
112
  const machine = Machine.make({
127
113
  states: States.states,
128
- events: [Command.cases.Save],
129
- internalEvents: [InternalEvent.cases.Saved, InternalEvent.cases.SaveFailed],
114
+ events: [Command],
115
+ internalEvents: [Internal],
130
116
  initial: () => States.initial.Idle.from()
131
117
  })
132
118
  ```
133
119
 
134
- Construct reusable events with `Machine.event` when the schema is owned by the
135
- machine protocol:
136
-
137
- ```ts
138
- const save = Machine.event(machine, Command.cases.Save)
139
- yield * ref.send(save)
140
- ```
141
-
142
- The schema constructor runs once and the decoded value is trusted by that
143
- machine and definitions derived from it with `handle`. This avoids decoding a
144
- known event again on every delivery. A configured `Schema.TaggedUnion` can use
145
- either the union schema itself or one of its `cases`. Treat the returned event
146
- as immutable; sending it to an unrelated machine goes through that machine's
147
- normal decoder.
148
-
149
- Ordinary values remain valid and are decoded at every boundary:
150
-
151
- ```ts
152
- yield * ref.send({ _tag: "Save" })
153
- ```
154
-
155
- Handlers and machine logic see the complete union. Local public APIs such as
156
- `MachineRef.send`, `machineAtom.send`, and `Machine.plan` expose only `events`
157
- in TypeScript. The local planner and runtime still share the complete event
158
- decoder so machine-local deliveries can flow through the same execution
159
- protocol; bypassing the types with JavaScript or `any` is therefore not a
160
- runtime authorization boundary. Cluster RPC delivery additionally validates
161
- incoming payloads against the public `events` schemas.
162
-
163
- The utility types make the distinction available to application code:
164
-
165
- ```ts
166
- type PublicCommand = Machine.Machine.InputEvent<typeof machine>
167
- type HandledEvent = Machine.Machine.Event<typeof machine>
168
- ```
169
-
170
- Tags must be unique within each list, and public and internal tags must be
171
- disjoint. Reusing a tag is a type error, so a command cannot accidentally
172
- masquerade as an internal result.
173
-
174
- ## Statechart structure
175
-
176
- `Machine.defineStates` accepts atomic, compound, parallel, final, choice, and history
177
- nodes:
178
-
179
- ```ts
180
- const State = Schema.TaggedUnion({
181
- Form: { draft: Schema.String },
182
- Editing: {},
183
- Saving: {},
184
- Done: {}
185
- })
186
-
187
- const States = Machine.defineStates({
188
- Form: {
189
- schema: State.cases.Form,
190
- initial: "Editing",
191
- states: {
192
- Editing: State.cases.Editing,
193
- Saving: State.cases.Saving,
194
- Done: {
195
- schema: State.cases.Done,
196
- type: "final",
197
- output: Schema.String
198
- }
199
- }
200
- }
201
- })
202
- ```
203
-
204
- Compound states have one active child and declare its initial key. Parallel
205
- states use `type: "parallel"` and have one active state in every direct region.
206
- Finality is topology, so declare `type: "final"` only in the state definition.
207
- Handlers implement behavior and output computation without repeating it:
208
-
209
- ```ts
210
- const machine = Machine.make({
211
- states: States.states,
212
- events: [],
213
- initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
214
- }).handle({
215
- Form: {
216
- states: {
217
- Done: {
218
- output: () => "saved"
219
- }
220
- }
221
- }
222
- })
223
- ```
224
-
225
- Every declared output schema must have a matching handler implementation before
226
- the machine can be planned, started, invoked, or adapted to Atom/Cluster.
227
- Final children complete their parent; put `onDone` on that compound or parallel
228
- parent, not on the final leaf.
120
+ Handlers see both protocols. Typed `send` and `Machine.plan` accept only public
121
+ events. Event tags must be unique and public/internal tags must be disjoint.
229
122
 
230
- Put data on the narrowest state where it is valid. If several sibling phases
231
- share data, prefer storing it on their compound parent instead of copying it
232
- into every child state.
123
+ Use `Machine.event(machine, schema, fields?)` for reusable machine-owned event
124
+ values. Ordinary objects and schema-constructed values are also accepted and
125
+ decoded at the machine boundary.
233
126
 
234
- ### Choice states
127
+ ### Choose the target by scope
235
128
 
236
- A choice is a transient, targetable decision point. Declare it with only
237
- `type: "choice"`; it has no schema, value, children, lifecycle actions, invoke,
238
- or event handlers. A choice is therefore absent from `StateIdentifier`, stable
239
- snapshots, configurations, and encoded snapshots.
129
+ | Builder | Use when | Preserves |
130
+ | ---------------- | ---------------------------------------- | ------------------------------------------------- |
131
+ | `target.local` | Moving inside the nearest compound scope | Ancestors and unrelated parallel regions |
132
+ | `target.branch` | Moving elsewhere under the active root | Omitted active ancestors and parallel regions |
133
+ | `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
134
+ | `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
240
135
 
241
- ```ts
242
- const States = Machine.defineStates({
243
- Flow: {
244
- schema: State.cases.Flow,
245
- initial: "Routing",
246
- states: {
247
- Routing: { type: "choice" },
248
- Approved: State.cases.Approved,
249
- Rejected: State.cases.Rejected
250
- }
251
- }
252
- })
136
+ Builders describe the next logical configuration. Shared states exit and enter
137
+ only when paths change; use `{ reenter: true, transition }` when the source must
138
+ restart even if its path is unchanged.
253
139
 
254
- const machine = Machine.make({
255
- states: States.states,
256
- events: [Event],
257
- initial: () =>
258
- States.initial.Flow(
259
- State.cases.Flow.make({ score: 80 }),
260
- (flow) => flow.Routing()
261
- )
262
- }).handle({
263
- Flow: {
264
- states: {
265
- Routing: {
266
- choice: {
267
- targets: ["Flow.Approved", "Flow.Rejected"],
268
- transition: ({ parent, target }) =>
269
- parent.score >= 70
270
- ? target.local.Approved(State.cases.Approved.make({}))
271
- : target.local.Rejected(State.cases.Rejected.make({}))
272
- }
273
- }
274
- }
275
- }
276
- })
277
- ```
140
+ ## Statechart capabilities
278
141
 
279
- The resolver is ordinary TypeScript or an `Effect`. It receives the triggering
280
- lifecycle event, typed parent values, target builders, and the normal planning
281
- capabilities, but no `state` because the choice itself has no state value. Its
282
- declared `targets` are both a compile-time bound and inspectable graph edges.
283
- The resolver must return one of them; missing, malformed, or undeclared targets
284
- fail planning. Choice implementations are required before execution APIs are
285
- available.
142
+ `Machine.defineStates` supports:
286
143
 
287
- Initial, event, completion, always, history-default, and other choice
288
- transitions settle in the same macrostep. Chained choices use the normal
289
- infinite-transition limit. Choice nodes never run entry or exit actions and
290
- never become active while their resolution remains visible in traces and
291
- coverage as a `choice` transition trigger.
144
+ - atomic states;
145
+ - compound states with one active child;
146
+ - parallel states with one active state in every region;
147
+ - final states and typed outputs;
148
+ - transient choice states;
149
+ - shallow and deep history states.
292
150
 
293
- ### History states
151
+ Declare topology—including finality, output schemas, choices, and history—only
152
+ in `defineStates`. Handlers implement behavior and output computation without
153
+ repeating structural metadata. Final children complete their parent, so
154
+ `onDone` belongs on that compound or parallel parent.
294
155
 
295
- A history pseudo-state remembers the last active configuration of its parent.
296
- It has no value schema and never appears in an active snapshot. History is
297
- shallow by default; use `history: "deep"` to retain the complete descendant
298
- configuration and its validated values:
156
+ Transition, entry, exit, choice, initial, and history callbacks are
157
+ synchronous. Conditions use ordinary TypeScript control flow. Callbacks may
158
+ select state and enqueue explicit `raise`, `emit`, `sendTo`, or `stop` commands;
159
+ arbitrary asynchronous Effects do not run inside planning.
299
160
 
300
- ```ts
301
- const States = Machine.defineStates({
302
- checkout: {
303
- schema: Checkout,
304
- initial: "shipping",
305
- states: {
306
- shipping: Shipping,
307
- payment: {
308
- schema: Payment,
309
- initial: "cardEntry",
310
- states: {
311
- cardEntry: CardEntry,
312
- verifying: Verifying
313
- }
314
- },
315
- resume: { type: "history", history: "deep" }
316
- }
317
- },
318
- support: Support
319
- })
320
- ```
161
+ ## Effects, timers, and child machines
321
162
 
322
- Implement a typed default for the first transition before any configuration
323
- has been remembered, then target history without supplying a state value:
163
+ State-scoped work starts on entry and is interrupted on exit:
324
164
 
325
165
  ```ts
326
- machine.handle({
327
- checkout: {
328
- history: {
329
- resume: {
330
- default: () => initialCheckoutSnapshot
331
- }
332
- }
333
- },
334
- support: {
335
- on: {
336
- Resume: ({ target }) => target.history.checkout.resume()
337
- }
338
- }
339
- })
340
- ```
341
-
342
- A history default is source-independent. It must construct a complete root
343
- configuration containing the history owner, including every inactive ancestor
344
- above a nested owner and every required region of a parallel ancestor. For a
345
- top-level owner, its owner snapshot is already a complete root snapshot.
346
-
347
- For example, a history node owned by `App.Workspace` can be targeted from an
348
- unrelated `Closed` root and supplies the complete `App` configuration on first
349
- use:
350
-
351
- ```ts
352
- Workspace: {
353
- history: {
354
- resume: {
355
- default: ({ target }) =>
356
- target.App(
357
- State.cases.App.make({ workspaceId: "default" }),
358
- (app) =>
359
- app.Workspace(
360
- State.cases.Workspace.make({}),
361
- (workspace) =>
362
- workspace.Editing(State.cases.Editing.make({}))
363
- )
364
- )
365
- }
366
- }
166
+ Loading: {
167
+ invoke: Machine.invokeEffect({
168
+ id: "save-document",
169
+ effect: saveDocument,
170
+ onSuccess: (entry) => Internal.cases.Saved.make({ id: entry.id }),
171
+ onFailure: (error) => Internal.cases.SaveFailed.make({ message: String(error) })
172
+ })
367
173
  }
368
- ```
369
-
370
- The containing branch is enforced statically: unrelated roots, sibling
371
- compound branches that omit the owner, owner-only nested snapshots, and
372
- incomplete parallel configurations are rejected.
373
-
374
- Deep history restores every remembered descendant value. Shallow history
375
- restores the parent and direct-child values, then follows normal initial paths.
376
- Only compound or parallel states that shallow restoration can enter implicitly
377
- need an `initial` handler to construct those new child values:
378
-
379
- ```ts
380
- payment: {
381
- initial: ;
382
- ;(({ state }) => new CardEntry({ attempt: state.attempt, cardNumber: "" }))
383
- }
384
- ```
385
-
386
- Execution APIs remain unavailable until required history defaults and shallow
387
- initializers have been implemented. History records are part of logical
388
- snapshots and are schema-validated by `encodeSnapshot` and `decodeSnapshot`.
389
-
390
- Transition between structurally related tagged states with `Machine.retag`.
391
- The source `_tag` is discarded, compatible fields are reused, and missing or
392
- incompatible required fields must be supplied:
393
-
394
- ```ts
395
- const saving = Machine.retag(State.cases.Saving, editing)
396
- ```
397
174
 
398
- ## Choosing a target builder
399
-
400
- Transition contexts expose four typed target builders:
401
-
402
- | Builder | Destination | Configuration behavior |
403
- | ---------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
404
- | `target.local` | Inside the source's nearest compound scope | Keeps the compound value, active ancestors, and unrelated parallel regions |
405
- | `target.branch` | Anywhere under the source's active top-level root | Replaces the selected branch while keeping omitted active ancestor values and parallel regions |
406
- | `target.full` | Any top-level root | Builds a complete active snapshot for the selected root |
407
- | `target.history` | A declared history pseudo-state | Restores its parent's remembered configuration or runs its typed default |
408
-
409
- When `target.local` or `target.branch` enters an inactive nested parallel
410
- state, its callback must select every region, just like `initial` and
411
- `target.full`. When that parallel state is already active, `target.branch`
412
- can still update one region directly and preserves the other active regions.
413
-
414
- The builder controls how the next configuration is assembled; it does not by
415
- itself decide which invokes restart. The runtime derives exit and entry paths
416
- from the previous and next active paths. Shared active ancestors remain entered,
417
- even when `target.full` supplies their values again. Use an event transition
418
- with `reenter: true` when the source state should explicitly exit and enter
419
- again:
420
-
421
- ```ts
422
- Refresh: {
423
- reenter: true,
424
- transition: ({ state, target }) =>
425
- target.full.Ready(new Ready({ value: state.value }))
426
- }
427
- ```
428
-
429
- `States.get`, `States.getWithParents`, `States.getSnapshot`, and
430
- `States.matches` accept typed dotted paths. Handler `parents` values are also
431
- keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
432
- provides the immediate parent directly and is `undefined` at a root state.
433
-
434
- Event, eventless, and completion transition contexts also expose `snapshot`, a
435
- read-only view of the complete logical configuration captured at the beginning
436
- of that transition microstep. This lets one parallel region inspect a sibling
437
- without copying active-state facts into parent values:
438
-
439
- ```ts
440
- BufferReady: ;
441
- ;(({ snapshot, target }) =>
442
- States.matches(snapshot, "Player.Network.Online")
443
- ? target.local.Playing(State.cases.Playing.make({}))
444
- : undefined)
445
- ```
446
-
447
- All non-conflicting handlers selected together observe the same captured
448
- snapshot. Handlers are synchronous and cannot read mutable live runtime state
449
- later. `snapshot` is intentionally absent from entry, exit,
450
- invoke, and choice contexts. In particular, startup and chained choices may run
451
- before a complete stable snapshot containing their pseudo-source exists.
452
-
453
- Effect Schema annotations are the metadata source for active states. Annotate
454
- the schema itself; `Machine.stateNodes` exposes the resolved annotation map:
455
-
456
- ```ts
457
- const Saving = State.cases.Saving.annotate({
458
- title: "Saving document",
459
- description: "Persisting local changes to the server"
460
- })
461
- ```
462
-
463
- Schema-less choice and history nodes accept only descriptive `title`,
464
- `description`, and `documentation` annotations. Titles may be used as display
465
- labels, but structural paths remain the only identity and targeting mechanism.
466
-
467
- ## Synchronous transitions and actor commands
468
-
469
- Transition, entry, exit, choice, initial, and history callbacks are synchronous.
470
- They select state and may enqueue only explicit statechart or actor operations:
471
- raise an internal event, emit to the parent, send to an invoked child, or stop a
472
- child. Arbitrary Effects are not accepted at this boundary.
473
-
474
- ```ts
475
- const handlers = {
476
- Save: ({ target }, enqueue) => {
477
- enqueue.emit(new SaveRequested({}))
478
- return target.local.Saving.from()
479
- }
480
- }
481
- ```
482
-
483
- Use `Machine.invokeEffect`, `Machine.invoke`, or an invoked child machine for
484
- asynchronous work. Their results return to the parent as typed events, keeping
485
- the transition core deterministic and synchronous.
486
-
487
- `Machine.plan` and `Machine.planInitial` return a `done` discriminator. When
488
- `done` is `true`, `output` is the schema-derived structural terminal union;
489
- while the machine remains active, it is `undefined`. A started machine's
490
- `join` uses the same terminal union. Output-less structural terminal paths
491
- contribute `undefined`, while active atomic roots do not.
492
-
493
- This union is intentionally conservative with respect to handler behavior. For
494
- example, a root `onDone` transition may make one structurally terminal result
495
- unreachable even though its schema remains in `Machine.TerminalOutput`.
496
-
497
- ## State-scoped invokes
498
-
499
- `Machine.invoke` runs child logic while its owning state is active. Leaving the
500
- state interrupts the child. For a one-shot Effect, `Machine.invokeEffect` maps
501
- typed success and failure values directly to internal events:
502
-
503
- ```ts
504
- const loading = {
505
- invoke: ({ state }) =>
506
- Machine.invokeEffect({
507
- id: "save",
508
- effect: save(state),
509
- onSuccess: (entry) => InternalEvent.cases.Saved.make({ id: entry.id }),
510
- onFailure: (error) =>
511
- InternalEvent.cases.SaveFailed.make({
512
- message: String(error)
513
- })
514
- })
175
+ Waiting: {
176
+ invoke: Machine.after(
177
+ "3 seconds",
178
+ Internal.cases.SaveFailed.make({ message: "Timed out" })
179
+ )
515
180
  }
516
181
  ```
517
182
 
518
- Omit `onFailure` when the Effect cannot fail. Defects and interruption remain
519
- failures rather than being mapped.
520
-
521
- `Machine.after` creates a cancellable, state-scoped delayed event with the same
522
- lifetime:
183
+ Use `Machine.invokeEffect` for one Effect, `Machine.after` for a cancellable
184
+ delay, and lower-level `Machine.invoke` only for custom process behavior or
185
+ snapshot mapping. Use one exported `Machine.child(id, machine)` descriptor for
186
+ `invokeMachine`, `sendTo`, and child lookup.
523
187
 
524
- ```ts
525
- invoke: Machine.after("3 seconds", InternalEvent.cases.SaveFailed.make({ message: "Timed out" }), {
526
- id: "save-timeout"
527
- })
528
- ```
529
-
530
- Provide an explicit id when more than one active timer could deliver the same
531
- event tag.
532
-
533
- Use lower-level `Machine.invoke` with `Machine.effect` for custom child logic or
534
- snapshot mapping. Its `id` is only the state-local lifecycle key. If the parent
535
- must send events to that invocation, create a typed low-level address with
536
- `Machine.childAddress<Event>("worker")` and pass it through the explicit
537
- `address` option; the address protocol is checked against the child logic.
538
- Lifecycle ids must be unique among simultaneously active invokes owned by the
539
- same state.
540
-
541
- Invoke outputs, invoke snapshot events, and invoked-child emissions belong in
542
- `internalEvents`. They are available to typed handlers but are not accepted by
543
- the typed public input APIs. Include a child machine's emitted protocol with
544
- `internalEvents: [...ChildMachine.emits]` when those emissions should be handled
545
- by the parent.
546
-
547
- For a child statechart, create one descriptor for `invokeMachine`, `sendTo`,
548
- and child lookup:
549
-
550
- ```ts
551
- const Editor = Machine.child("editor", EditorMachine)
552
- ```
553
-
554
- `Machine.child(id, machine)` is the complete statechart descriptor;
555
- `Machine.childAddress<Event>(id)` is the lower-level event-only address.
556
- Descriptors are matched by id and machine identity, so independently created
557
- descriptors for the same pair address the same child without a global cache.
558
- Exporting one descriptor remains the clearest module boundary.
559
-
560
- `Machine.activityDefinitions(machine)` inspects state-owned work without
561
- executing it. Static descriptors report their source path, lifecycle id, and
562
- kind. Timers also report normalized duration and emitted event tag;
563
- `invokeEffect` mappings are described as dynamic; invoked machines expose only
564
- safe child identity. A function-valued `invoke` factory is reported as dynamic
565
- and is never evaluated during inspection. The result is serializable and does
566
- not contain Effects, closures, services, or child runtimes.
188
+ Expected failures should become internal events. An unrecovered invoke or child
189
+ failure terminates the owning runtime.
567
190
 
568
191
  ## Reactivity
569
192
 
570
- `AtomMachine.make` creates a lazy bridge backed by one running machine per
571
- `AtomRegistry`. Mounting or reading one of its atoms starts the machine;
572
- disposing the registry-owned reference stops it.
193
+ `AtomMachine` runs one lazy machine instance per `AtomRegistry`:
573
194
 
574
195
  ```ts
575
196
  import { AtomMachine } from "@typeonce/effect-machine/reactivity"
576
197
  import { Atom } from "effect/unstable/reactivity"
577
198
 
578
199
  const runtime = Atom.runtime(AppLayer)
579
- const machines = AtomMachine.bind(runtime)
580
- const machineAtom = machines.make(Counter)
200
+ const counterAtom = AtomMachine.bind(runtime).make(Counter)
581
201
  ```
582
202
 
583
- For applications with a shared runtime, treat
584
- `AtomMachine.bind(runtime).make(...)` as the canonical form. It keeps runtime
585
- ownership at the composition boundary so it does not need to be passed through
586
- every feature. Service-free machines may use `AtomMachine.make(machine)`
587
- directly.
588
-
589
- The bridge exposes:
590
-
591
- - `ref`: the running `MachineRef`
592
- - `result`: fail-aware logical state, combining startup and post-start runtime
593
- failures
594
- - `snapshot`: authoritative runtime lifecycle, including `active`, `done`,
595
- `error`, and `stopped`
596
- - `state`: the last logical state, including the retained state after a runtime
597
- failure
598
- - `send` and `stop`: writable command atoms
599
- - `child(descriptor)`: a reactive bridge for a directly owned child
600
-
601
- Use `AtomMachine.select` and `AtomMachine.matches` for equality-aware root
602
- derivations. Use `selectChild` and `matchesChild` for child bridges. Selector
603
- paths and selected value types are inferred directly from the bridge snapshot,
604
- so these combinators do not need the `DefinedStates` object. They follow normal
605
- Atom identity semantics and return a new atom on each call, so retain or memoize
606
- them when constructing them in a component. The `child` method uses Effect's
607
- `Atom.family` to reuse a live bridge for the same descriptor without maintaining
608
- a package-level cache.
609
- `AtomMachine.ChildMachineAtom<typeof Child>` uses `unknown` as its startup-error
610
- default for general component props.
611
- `AtomMachine.ChildOf<typeof parentAtom, typeof Child>` preserves the exact
612
- parent startup-error channel.
613
-
614
- Child state and snapshot atoms contain `Option.none()` while that child is
615
- inactive. React applications using `@effect/atom-react` need a
616
- `RegistryProvider`; see the [Pokémon example](./examples/pokemon).
617
-
618
- ## Snapshots and persistence
619
-
620
- `Machine.encodeSnapshot` and `Machine.decodeSnapshot` validate logical
621
- statechart data for storage or transport. The encoded representation does not
622
- contain the machine definition, machine version, services, subscriptions, or
623
- running child processes. Store machine identity and migration/version metadata
624
- alongside it.
625
-
626
- Resume a decoded logical snapshot explicitly:
203
+ Binding a shared runtime once is the canonical form for service-backed
204
+ applications. Service-free machines can use `AtomMachine.make(Counter)`.
205
+
206
+ The bridge exposes `ref`, `snapshot`, `state`, fail-aware `result`, writable
207
+ `send` and `stop` atoms, and `child(descriptor)`. Use `AtomMachine.select` and
208
+ `AtomMachine.matches` for typed, equality-aware derivations. React applications
209
+ using `@effect/atom-react` need a `RegistryProvider`.
210
+
211
+ ## Persistence
212
+
213
+ Logical snapshots can be validated for storage or transport:
627
214
 
628
215
  ```ts
629
216
  const encoded = yield * Machine.encodeSnapshot(machine, snapshot)
@@ -631,249 +218,65 @@ const decoded = yield * Machine.decodeSnapshot(machine, encoded)
631
218
  const ref = yield * Machine.resume(machine, decoded)
632
219
  ```
633
220
 
634
- `resume` does not call `initial`, require machine input, or replay entry,
635
- transition, completion, eventless, raised-event, or emitted-event work that
636
- produced the snapshot. The decoded snapshot is the first published logical
637
- state. A final snapshot immediately yields a completed ref with its output.
638
-
639
- Resumption creates a fresh runtime. Invokes owned by active states start once in
640
- normal ancestor/document order and receive `Machine.InitialEvent` as their
641
- lifecycle event. `invokeEffect` runs again, invoked machines start from their
642
- own initial state, and `Machine.after` timers restart from their full declared
643
- duration. Spawned children, queued events, subscriptions, fibers, scopes,
644
- elapsed timer time, child snapshots, and prior `RuntimeSnapshot` status/errors
645
- are not restored. Completion and history metadata remain logical state and are
646
- not replayed. A changed machine definition does not cause `resume` itself to
647
- evaluate newly enabled `always` or `onDone` transitions.
648
-
649
- Reactive applications use `AtomMachine.resume(machine, decoded)` for a
650
- service-free machine or `AtomMachine.bind(runtime).resume(machine, decoded)`
651
- for a service-backed machine. These bridges have the same lazy one-runtime-per-
652
- registry ownership and disposal behavior as `AtomMachine.make`.
653
-
654
- `ClusterMachine` provides a separate persisted entity adapter. Its process-local
655
- restrictions, checkpoint planning, and delivery guarantees are documented on
656
- that API. `Machine.resume` is logical resumption, not durable process or cluster
657
- restoration.
658
-
659
- ## Property-based semantic invariants
660
-
661
- `MachineTest.verify` checks statechart structure and planner lifecycle laws.
662
- Application semantics belong in invariants that can be reused across generated
663
- scenarios and, in future, bounded exploration:
221
+ Resumption restores logical state, values, completion, and history metadata.
222
+ It creates a fresh runtime: active invokes restart, timers restart at their
223
+ full duration, and prior fibers, subscriptions, queues, and child runtimes are
224
+ not restored. Store machine identity and migration/version metadata beside the
225
+ encoded snapshot.
664
226
 
665
- ```ts
666
- import { MachineTest } from "@typeonce/effect-machine/testing"
667
- import { Effect } from "effect"
668
-
669
- const invariant = MachineTest.invariants(accountMachine)
670
- const laws = [
671
- invariant.state(
672
- "balance is never negative",
673
- ({ snapshot }) =>
674
- snapshot.value.balance >= 0 ||
675
- `negative balance: ${snapshot.value.balance}`
676
- ),
677
- invariant.step(
678
- "withdrawal removes exactly its amount",
679
- ({ before, event, after }) =>
680
- event._tag !== "Withdraw" ||
681
- after.value.balance === before.value.balance - event.amount
682
- )
683
- ]
684
-
685
- const generated = MachineTest.scenarios(accountMachine, {
686
- minEvents: 0,
687
- maxEvents: 30
688
- })
689
-
690
- it.effect.prop(
691
- "preserves account laws",
692
- { scenario: generated.arbitrary },
693
- ({ scenario }) =>
694
- MachineTest.run(accountMachine, scenario).pipe(
695
- Effect.tap((trace) => MachineTest.verify(accountMachine, trace)),
696
- Effect.flatMap((trace) => MachineTest.assertInvariants(accountMachine, trace, laws))
697
- )
698
- )
699
- ```
227
+ ## Testing
700
228
 
701
- State invariants observe settled startup and public-event states by default.
702
- Set `observe` to `"microsteps"`, `"all"`, or `"final"` for a different scope.
703
- Use `when` for conditional laws. A condition with no matches is reported as
704
- `untested`; add `require: { minObservations: 1 }` when a particular trace must
705
- exercise it. `checkInvariants` returns this report, while `assertInvariants`
706
- returns `void` for direct use in property tests. Failures retain the complete
707
- shrunk trace and precise event, microstep, configuration, and observation
708
- location.
229
+ The testing entrypoint provides complementary layers:
709
230
 
710
- These APIs inspect planner evidence. Staged action effects, invokes, timing,
711
- and process scheduling require the runtime command-model APIs instead.
712
-
713
- Use bounded exploration when random scenarios should be complemented by a
714
- systematic search over concrete event representatives:
231
+ - `MachineTest.run` and `verify` inspect pure planner traces;
232
+ - invariants and generated scenarios check application laws;
233
+ - `explore` performs bounded breadth-first state-space exploration;
234
+ - `probe` causally acknowledges live runtime commands;
235
+ - runtime command models cover timers, invokes, bursts, and scheduling.
715
236
 
716
237
  ```ts
717
- const explored = yield * MachineTest.explore(accountMachine, {
718
- events: ({ snapshot }) => [
719
- new Deposit({ amount: 1 }),
720
- new Withdraw({ amount: snapshot.value.balance }),
721
- new Withdraw({ amount: snapshot.value.balance + 1 })
722
- ],
723
- stateKey: ({ snapshot }) => `${snapshot.value._tag}:${snapshot.value.balance}`,
724
- limits: {
725
- maxDepth: 20,
726
- maxStates: 1_000,
727
- maxTransitions: 10_000
728
- },
729
- invariants: laws
730
- })
731
-
732
- const rejected = yield * MachineTest.assertReachable(
733
- explored,
734
- "insufficient funds rejection",
735
- ({ configuration }) => configuration.includes("Rejected")
736
- )
737
-
738
- console.log(rejected.trace.scenario.events) // shortest witness
739
- ```
740
-
741
- Exploration is breadth-first, so each retained node owns its shortest trace.
742
- It is exhaustive only for the concrete events returned by `events` and the
743
- equivalence relation defined by `stateKey`. Equal keys intentionally collapse
744
- snapshots and only the first representative is expanded. Results distinguish
745
- `Complete` from `Truncated` and retain the depth, state, or transition frontier
746
- that hit a limit. An unreachability assertion succeeds only for a complete
747
- result; otherwise it fails as inconclusive. Cycles are retained as graph edges,
748
- but exploration does not enumerate every cyclic path. Invariants are checked
749
- on startup and on each planned edge extending a node's shortest trace.
750
-
751
- ## Causal runtime probes
752
-
753
- Pure traces do not execute invokes or the managed runtime. When a test needs to
754
- prove that one live event has actually left the mailbox, attach a testing-only
755
- probe to a statechart reference:
756
-
757
- ```ts
758
- const ref = yield * Machine.start(machine)
759
- const probe = yield * MachineTest.probe(machine, ref)
760
-
761
- const step = yield * probe.sendAndAwait(new CancelRequested({}))
762
-
763
- assert.strictEqual(step.handled, false)
764
- assert.deepStrictEqual(step.before, step.after)
765
- ```
766
-
767
- `sendAndAwait` completes after that event's synchronous macrostep and managed
768
- commit work. It also completes for ignored events, which publish no snapshot
769
- and therefore cannot be synchronized by waiting for `ref.changes`.
770
-
771
- The step retains the exact runtime plan, before/after logical snapshots, and
772
- whether the event was handled or changed/reentered the active configuration.
773
- It does not wait for timers or invoked processes to finish. Production code
774
- continues to use enqueue-only `ref.send`; probes are exported only from the
775
- separate testing entry point.
238
+ import { MachineTest } from "@typeonce/effect-machine/testing"
776
239
 
777
- For command-model and property tests, choose the delivery semantics explicitly.
778
- `runCausalCommands` requires a probe and completes every accepted send before
779
- checking it or starting the next command:
240
+ const trace = yield* MachineTest.run(Counter, {
241
+ events: [Event.cases.Start.make({}), Event.cases.Increment.make({})]
242
+ })
780
243
 
781
- ```ts
782
- const transcript = yield * MachineTest.runCausalCommands(
783
- probe,
784
- commands,
785
- {
786
- initialModel,
787
- transition: (model, command) =>
788
- Effect.succeed({
789
- model: updateModel(model, command),
790
- expected: expectedResult(model, command)
791
- }),
792
- assert: ({ actual, expected }) =>
793
- Effect.sync(() => {
794
- if (actual.result._tag === "SendProcessed") {
795
- assert.deepStrictEqual(actual.result.step.after, expected.snapshot)
796
- assert.strictEqual(actual.result.step.handled, expected.handled)
797
- }
798
- })
799
- }
800
- )
244
+ yield* MachineTest.verify(Counter, trace)
801
245
  ```
802
246
 
803
- A causal model step needs no synchronization policy. Use the probe-bound
804
- `probe.await.until(...)` only for later asynchronous work such as a timer,
805
- invoke result, or child delivery. The predicate sees the exact runtime snapshot
806
- type. `actual.awaited` retains every snapshot tested by that explicit wait.
807
-
808
- Use `runEnqueuedCommands(ref, ...)` when the property intentionally submits
809
- bursts or retains outstanding mailbox work. Its model steps continue to use
810
- `RuntimeSynchronization`. The old `runRuntimeCommands` and
811
- `formatRuntimeTranscript` names are deprecated aliases for the enqueue-oriented
812
- runner and formatter because their delivery semantics were not visible.
813
-
814
- ### Runtime invariants and planner agreement
247
+ Pure planner tests do not execute invokes or time. Use a started machine and a
248
+ probe when those semantics matter.
815
249
 
816
- Planner invariants and runtime invariants are deliberately separate. Runtime
817
- laws inspect causal command evidence, explicit asynchronous observations, and
818
- runtime status without requiring a duplicate reference model:
819
-
820
- ```ts
821
- const invariant = MachineTest.runtimeInvariants(machine)
822
- const laws = [
823
- invariant.snapshot("count never becomes negative", ({ snapshot }) => snapshot.state.value.count >= 0),
824
- invariant.command(
825
- "every accepted add is processed",
826
- ({ command, result }) =>
827
- command._tag !== "Send" || command.event._tag !== "Add" ||
828
- result._tag === "SendProcessed"
829
- )
830
- ]
831
-
832
- const transcript = yield * MachineTest.verifyCausalCommands(
833
- probe,
834
- commands,
835
- { invariants: laws }
836
- )
837
- ```
838
-
839
- Use the existing `runCausalCommands` when a simplified application model
840
- provides exact expected results. Its returned transcript implements the same
841
- model-independent evidence interface, so reusable runtime laws compose with
842
- it directly:
250
+ ## Entrypoints
843
251
 
844
252
  ```ts
845
- const transcript = yield * MachineTest.runCausalCommands(probe, commands, model)
846
-
847
- yield * MachineTest.assertRuntimeInvariants(machine, transcript, laws)
848
- yield * MachineTest.assertPlannerRuntimeAgreement(machine, transcript)
253
+ import { Machine } from "@typeonce/effect-machine"
254
+ import { ClusterMachine } from "@typeonce/effect-machine/cluster"
255
+ import { AtomMachine } from "@typeonce/effect-machine/reactivity"
256
+ import { MachineTest } from "@typeonce/effect-machine/testing"
849
257
  ```
850
258
 
851
- `checkRuntimeInvariants` returns an aggregate report; `assertRuntimeInvariants`
852
- fails with every predicate and non-vacuity violation. Snapshot laws observe the
853
- initial and post-command snapshots by default. Select `"awaited"`, `"all"`, or
854
- `"final"` explicitly when a law targets observations retained by
855
- `probe.await.until` or only the final runtime snapshot.
259
+ Each ESM entrypoint is independent and tree-shakeable.
856
260
 
857
- `assertPlannerRuntimeAgreement` is an explicit consistency check, not an
858
- application oracle. For each processed send it freshly plans from the receipt's
859
- `before` snapshot and compares handled/change flags, the public next snapshots,
860
- completion, command counts, emitted events, and public microstep evidence. It
861
- does not prove that the planner implements the intended business rules; use a
862
- reference model and runtime invariants for that.
261
+ ## Examples
863
262
 
864
- ## Current limits
263
+ Every package directly under [`examples/`](./examples) has its own lockfile and
264
+ `check` script.
865
265
 
866
- Declarative first-class guards are not part of the current API. Ordinary
867
- TypeScript conditions implement guards. Use `Machine.after` for a cancellable
868
- state-scoped delayed event.
266
+ | Example | What it demonstrates |
267
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
268
+ | [Playground](./examples/playground) | Five focused React examples: atomic turnstile commands, state-scoped traffic-light timers, microwave safety across parallel regions, a service-backed media player, and a worker-hosted machine synchronized across tabs |
269
+ | [Pokémon](./examples/pokemon) | Compound and parallel states, invoked child machines, typed emissions, Atom reactivity, and a live Effect service |
270
+ | [Platformer](./examples/platformer) | Nested parallel statecharts, typed deep history, raised events, state-scoped timers, deterministic model tests, and a playable SVG adapter |
869
271
 
870
- ## Guidance for agents and contributors
272
+ The playground is the shortest path from one concept to working code. The
273
+ standalone examples show larger composition and ownership boundaries.
871
274
 
872
- The shipped [agent guide](./docs/agent-guide.md) contains the recommended
873
- definition order, modeling rules, lifecycle invariants, React recipe, common
874
- compiler errors, and unsupported features.
275
+ ## Reference and development
875
276
 
876
- ## Development and validation
277
+ - [API reference](https://effect-machine.typeonce.dev)
278
+ - [Agent and implementation guide](./docs/agent-guide.md)
279
+ - [Contributing guide](./CONTRIBUTING.md)
877
280
 
878
281
  Use pnpm 10 and Node.js 20 or newer:
879
282
 
@@ -882,40 +285,9 @@ pnpm install --frozen-lockfile
882
285
  pnpm check
883
286
  ```
884
287
 
885
- Individual commands are available for `build`, `test`, `test:types`,
886
- `typecheck`, `format:check`, `test:consumer`, and `pack:check`. Runtime tests use
887
- `@effect/vitest`; type tests use TSTyche and TypeScript 6.0.3. The consumer check
888
- packs the package, imports all public entrypoints, and compiles a strict
889
- TypeScript consumer with `skipLibCheck: false`.
890
-
891
- Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. Pull
892
- requests receive an automated base-versus-head type-instantiation report.
893
-
894
- ## Examples
895
-
896
- The [platformer statechart example](./examples/platformer) is a playable SVG
897
- demo centered on a schema-first character machine. It demonstrates nested
898
- compound locomotion, parallel airborne motion and air-jump regions, independent
899
- facing and wall-contact regions, a pause/resume flow backed by typed deep
900
- history, typed protocol events, state-scoped timers, and state-driven SVG
901
- transforms.
902
-
903
- The [Pokémon statechart example](./examples/pokemon) is a standalone React and
904
- Vite project demonstrating compound and parallel states, state-scoped invokes,
905
- invoked child statecharts, typed emissions, and Atom reactivity. It uses a local
906
- `file:` dependency on this package while retaining an isolated dependency graph,
907
- lockfile, build, and CI job.
908
-
909
- The [playground](./examples/playground) collects focused interactive examples
910
- for traffic lights, turnstiles, media players, microwaves, and worker-backed
911
- machines. CI discovers every direct package under `examples/` and runs its
912
- `check` script automatically.
913
-
914
- ## Releases
915
-
916
- Add a changeset with `pnpm changeset`. CI validates frozen installation and the
917
- complete check suite. The release workflow opens version PRs and publishes with
918
- npm provenance through GitHub Actions.
288
+ Declarative first-class guards are not currently part of the API; use ordinary
289
+ TypeScript conditions. Pull requests that change `src/` or `package.json` need
290
+ a changeset and the performance checks described in `AGENTS.md`.
919
291
 
920
292
  When equivalent Machine modules ship in Effect, this package is intended to
921
- become a thin compatibility re-export package before eventual retirement.
293
+ become a compatibility re-export before eventual retirement.
@@ -51,9 +51,11 @@ const InternalEvent = Schema.TaggedUnion({
51
51
  const States = Machine.defineStates(State.cases)
52
52
  ```
53
53
 
54
- Construct values with `State.cases.Idle.make({})` or
55
- `Event.cases.Save.make({})`. Use `Schema.TaggedClass` instead when a case needs
56
- class methods or nominal class identity.
54
+ Construct event values with `Event.cases.Save.make({})`. Construct new state
55
+ values through the target or initial builder's `.from(...)` method so schema
56
+ construction runs inside planning. Pass a state directly only when it is
57
+ already decoded. Use `Schema.TaggedClass` when a case needs class methods or
58
+ nominal class identity; `.from(...)` preserves that identity.
57
59
 
58
60
  ## Hard invariants
59
61
 
@@ -190,7 +192,7 @@ const States = Machine.defineStates({
190
192
  const machine = Machine.make({
191
193
  states: States.states,
192
194
  events: [],
193
- initial: () => States.initial.Done(State.cases.Done.make({}))
195
+ initial: () => States.initial.Done.from()
194
196
  }).handle({
195
197
  Done: {
196
198
  output: () => "done"
@@ -268,15 +270,8 @@ Workspace: {
268
270
  history: {
269
271
  resume: {
270
272
  default: ({ target }) =>
271
- target.App(
272
- State.cases.App.make({ workspaceId: "default" }),
273
- (app) =>
274
- app.Workspace(
275
- State.cases.Workspace.make({}),
276
- (workspace) =>
277
- workspace.Editing(State.cases.Editing.make({}))
278
- )
279
- )
273
+ target.App.from({ workspaceId: "default" }, (app) =>
274
+ app.Workspace.from((workspace) => workspace.Editing.from()))
280
275
  }
281
276
  }
282
277
  }
@@ -315,7 +310,7 @@ exiting shared states. To force the source to exit and enter again:
315
310
  Refresh: {
316
311
  reenter: true,
317
312
  transition: ({ state, target }) =>
318
- target.full.Ready(new Ready({ value: state.value }))
313
+ target.full.Ready.from({ value: state.value })
319
314
  }
320
315
  ```
321
316
 
@@ -382,7 +377,7 @@ microstep, before any selected transition is applied:
382
377
  ```ts
383
378
  BufferReady: ({ snapshot, target }) =>
384
379
  States.matches(snapshot, "Player.Network.Online")
385
- ? target.local.Playing(new Playing({}))
380
+ ? target.local.Playing.from()
386
381
  : undefined
387
382
  ```
388
383
 
@@ -426,7 +421,7 @@ A transition returns a target synchronously:
426
421
 
427
422
  ```ts
428
423
  Submit: ({ state, target }) =>
429
- state.valid ? target.local.Saving(new Saving({ draft: state.draft })) : undefined
424
+ state.valid ? target.local.Saving.from({ draft: state.draft }) : undefined
430
425
  ```
431
426
 
432
427
  Closed statechart and actor operations use `enqueue`:
@@ -434,7 +429,7 @@ Closed statechart and actor operations use `enqueue`:
434
429
  ```ts
435
430
  Submit: ({ target }, enqueue) => {
436
431
  enqueue.emit(new SaveRequested({}))
437
- return target.local.Saving(new Saving({}))
432
+ return target.local.Saving.from()
438
433
  }
439
434
  ```
440
435
 
@@ -878,7 +873,7 @@ reference model when correctness of the expected behavior matters.
878
873
  Wrap the initial builder result:
879
874
 
880
875
  ```ts
881
- initial: () => States.initial.Idle(new Idle({}))
876
+ initial: () => States.initial.Idle.from()
882
877
  ```
883
878
 
884
879
  ### Invoked child output must be a machine event
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {