@typeonce/effect-machine 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -80
- package/dist/Machine.d.ts +434 -297
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +13 -55
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/cluster.d.ts +2 -2
- package/dist/internal/machine/cluster.d.ts.map +1 -1
- package/dist/internal/machine/cluster.js +2 -1
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +1 -1
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +48 -10
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts.map +1 -1
- package/dist/internal/machine/serialization.js +75 -18
- package/dist/internal/machine/serialization.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +9 -6
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +13 -10
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +5 -3
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +22 -2
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +4 -0
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/docs/agent-guide.md +189 -83
- package/package.json +4 -4
- package/src/Machine.ts +845 -1051
- package/src/internal/machine/cluster.ts +4 -1
- package/src/internal/machine/invocation.ts +1 -1
- package/src/internal/machine/machine.ts +64 -8
- package/src/internal/machine/serialization.ts +100 -25
- package/src/internal/testing/machine/exploration.ts +1 -1
- package/src/internal/testing/machine/trace.ts +1 -1
- package/src/internal/testing/machine/verification.ts +16 -11
- package/src/testing/MachineTest.ts +13 -10
- package/src/unstable/cluster/ClusterMachine.ts +51 -1
- package/src/unstable/reactivity/AtomMachine.ts +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Cluster and are exposed only through explicit integration boundaries.
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
pnpm add @typeonce/effect-machine effect@4.0.0-rc.
|
|
37
|
+
pnpm add @typeonce/effect-machine effect@4.0.0-rc.110
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
`effect` is an exact peer dependency. Install the version above and upgrade it
|
|
@@ -49,11 +49,14 @@ import { Machine } from "@typeonce/effect-machine"
|
|
|
49
49
|
import { Effect, Schema, Stream } from "effect"
|
|
50
50
|
|
|
51
51
|
const State = Schema.TaggedUnion({
|
|
52
|
-
Idle: {},
|
|
53
52
|
Running: { count: Schema.Number }
|
|
54
53
|
})
|
|
55
54
|
|
|
56
|
-
const States = Machine.states(
|
|
55
|
+
const States = Machine.states({
|
|
56
|
+
Idle: {},
|
|
57
|
+
Running: State.cases.Running
|
|
58
|
+
})
|
|
59
|
+
|
|
57
60
|
const CounterEvent = Machine.events(
|
|
58
61
|
Schema.TaggedUnion({
|
|
59
62
|
Start: {},
|
|
@@ -66,7 +69,7 @@ const CounterDefinition = Machine.make({
|
|
|
66
69
|
id: "Counter",
|
|
67
70
|
states: States.states,
|
|
68
71
|
events: CounterEvent,
|
|
69
|
-
initial: (to) => to.Idle()
|
|
72
|
+
initial: (to) => to.Idle()
|
|
70
73
|
})
|
|
71
74
|
|
|
72
75
|
const Counter = CounterDefinition.handle({
|
|
@@ -78,7 +81,7 @@ const Counter = CounterDefinition.handle({
|
|
|
78
81
|
Running: {
|
|
79
82
|
on: {
|
|
80
83
|
Increment: (to) => to.full.Running().resolve(({ state, target }) => target.from({ count: state.count + 1 })),
|
|
81
|
-
Stop: (to) => to.full.Idle()
|
|
84
|
+
Stop: (to) => to.full.Idle()
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
})
|
|
@@ -122,7 +125,27 @@ Keep one-off topology inline in `Machine.states`. Use `Machine.state` only when
|
|
|
122
125
|
the same active state definition is mounted more than once; tagged schemas are
|
|
123
126
|
already reusable without it. For repeated finite regions, derive names with
|
|
124
127
|
`States.path(...)` so every literal in the path family is checked against the
|
|
125
|
-
complete tree. Type full-snapshot helpers as `Machine.Snapshot<typeof States
|
|
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>`.
|
|
132
|
+
|
|
133
|
+
### Make invalid states unrepresentable
|
|
134
|
+
|
|
135
|
+
Treat topology as a domain contract, not as file organization. A parallel state
|
|
136
|
+
declares the full Cartesian product of its regions, so use it only when every
|
|
137
|
+
combination has a coherent meaning. If one region must inspect another before
|
|
138
|
+
entering a state safely, prefer a compound hierarchy that makes the forbidden
|
|
139
|
+
combination impossible. `matches` remains useful for views, tests, and genuine
|
|
140
|
+
coordination between independent regions; it should not repair an invalid
|
|
141
|
+
state product.
|
|
142
|
+
|
|
143
|
+
Keep state-scoped Effects beneath the state that guarantees their resources,
|
|
144
|
+
and enforce command availability in the machine rather than only by disabling
|
|
145
|
+
UI controls. When entering an inactive compound or parallel state's declared
|
|
146
|
+
default, select `.initial`; explicitly construct descendants only for a
|
|
147
|
+
non-default configuration or a complete replacement of an already-active
|
|
148
|
+
parallel root.
|
|
126
149
|
|
|
127
150
|
### Construct state through builders
|
|
128
151
|
|
|
@@ -150,7 +173,8 @@ const handlers = {
|
|
|
150
173
|
}
|
|
151
174
|
```
|
|
152
175
|
|
|
153
|
-
Omit `schema` when a state represents control flow but owns no data
|
|
176
|
+
Omit `schema` when a state represents control flow but owns no data. Use `{}`
|
|
177
|
+
instead of defining an empty tagged schema:
|
|
154
178
|
|
|
155
179
|
```ts
|
|
156
180
|
const States = Machine.states({
|
|
@@ -175,6 +199,11 @@ Schema-less states remain active, targetable, matchable, and visible through
|
|
|
175
199
|
their handler `state` is `undefined`, and `get` / `getWithParents` accept only
|
|
176
200
|
schema-backed paths. Add a schema later if the state starts owning data.
|
|
177
201
|
|
|
202
|
+
Keep data-bearing state schemas together in a named `Schema.TaggedUnion` and
|
|
203
|
+
reference its cases from the topology. For a standalone state schema whose
|
|
204
|
+
class identity is useful, declare a named `Schema.TaggedClass`. Do not bury
|
|
205
|
+
one-off tagged schema declarations inside `Machine.states`.
|
|
206
|
+
|
|
178
207
|
Put data on the narrowest state where it is valid. If sibling phases share
|
|
179
208
|
data, put it on their compound parent.
|
|
180
209
|
|
|
@@ -206,7 +235,7 @@ const definition = Machine.make({
|
|
|
206
235
|
events: CommandEvent,
|
|
207
236
|
internalEvents: InternalEvent,
|
|
208
237
|
emittedEvents: Emissions,
|
|
209
|
-
initial: (to) => to.Idle()
|
|
238
|
+
initial: (to) => to.Idle()
|
|
210
239
|
})
|
|
211
240
|
```
|
|
212
241
|
|
|
@@ -318,7 +347,7 @@ const child = Machine.make({
|
|
|
318
347
|
states: ChildStates.states,
|
|
319
348
|
events: ChildEvents,
|
|
320
349
|
parent: Machine.parent(ParentEvents),
|
|
321
|
-
initial: (to) => to.Working()
|
|
350
|
+
initial: (to) => to.Working()
|
|
322
351
|
}).handle({
|
|
323
352
|
Working: {
|
|
324
353
|
on: {
|
|
@@ -369,9 +398,13 @@ paths. `parent` always means the owning machine target.
|
|
|
369
398
|
| `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
|
|
370
399
|
|
|
371
400
|
Every required transition handler selects a target from its inline `to`
|
|
372
|
-
builder.
|
|
373
|
-
|
|
374
|
-
|
|
401
|
+
builder. Return a bare selection such as `to.full.Idle()` when the selected
|
|
402
|
+
builder supports zero-argument construction; the machine applies the same
|
|
403
|
+
default construction as `target.from()`. This includes empty schemas and
|
|
404
|
+
schemas whose constructor fields are all optional or defaulted. TypeScript
|
|
405
|
+
rejects the bare form when state data or nested configuration is required.
|
|
406
|
+
Call `.resolve(...)` when construction depends on handler context or the
|
|
407
|
+
transition needs to enqueue commands. An absent handler ignores the trigger; `to.none` handles
|
|
375
408
|
it and retains queued commands, raised events, and emitted events without
|
|
376
409
|
selecting a destination. Concrete destinations stay narrowed inside their
|
|
377
410
|
resolver, and `to.branches({...})` gives the resolver only the declared named
|
|
@@ -437,37 +470,35 @@ arbitrary asynchronous Effects do not run inside planning.
|
|
|
437
470
|
State-scoped work starts on entry and is interrupted on exit:
|
|
438
471
|
|
|
439
472
|
```ts
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
onDone: (to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" }))
|
|
454
|
-
})
|
|
455
|
-
}
|
|
473
|
+
machine.handle({
|
|
474
|
+
Loading: {
|
|
475
|
+
invoke: (from) =>
|
|
476
|
+
from.effect("save-document", () => saveDocument)
|
|
477
|
+
.onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ id: output.id })))
|
|
478
|
+
.onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) })))
|
|
479
|
+
},
|
|
480
|
+
Waiting: {
|
|
481
|
+
invoke: (from) =>
|
|
482
|
+
from.timer("save-timeout", "3 seconds")
|
|
483
|
+
.onDone((to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" })))
|
|
484
|
+
}
|
|
485
|
+
})
|
|
456
486
|
```
|
|
457
487
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
for state-dependent Effects:
|
|
488
|
+
The state-local `from` selector starts an `effect`, `stream`, `timer`, reusable
|
|
489
|
+
`logic`, or complete `child` statechart. The selected source determines which
|
|
490
|
+
lifecycle methods the chain requires and which methods are available. For
|
|
491
|
+
example, an Effect with non-`never` output and error channels must handle both;
|
|
492
|
+
the completed chain is the value returned by the callback:
|
|
464
493
|
|
|
465
494
|
```ts
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
495
|
+
machine.handle({
|
|
496
|
+
Loading: {
|
|
497
|
+
invoke: (from) =>
|
|
498
|
+
from.effect("load-document", ({ state }) => loadDocument(state.documentId))
|
|
499
|
+
.onDone((to) => to.full.Ready().resolve(({ output, target }) => target.from({ document: output })))
|
|
500
|
+
.onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: error.message })))
|
|
501
|
+
}
|
|
471
502
|
})
|
|
472
503
|
```
|
|
473
504
|
|
|
@@ -476,15 +507,18 @@ is mapped by `onElement`, and the next element is not pulled until that parent
|
|
|
476
507
|
macrostep commits:
|
|
477
508
|
|
|
478
509
|
```ts
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
510
|
+
machine.handle({
|
|
511
|
+
Listening: {
|
|
512
|
+
invoke: (from) =>
|
|
513
|
+
from.stream("channel", () => channelMessages)
|
|
514
|
+
.onElement((to) =>
|
|
515
|
+
to.none.resolve(({ element }, enqueue) => {
|
|
516
|
+
enqueue.raise(Events.MessageReceived({ message: element }))
|
|
517
|
+
})
|
|
518
|
+
)
|
|
519
|
+
.onDone((to) => to.none)
|
|
520
|
+
.onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ error })))
|
|
521
|
+
}
|
|
488
522
|
})
|
|
489
523
|
```
|
|
490
524
|
|
|
@@ -493,10 +527,10 @@ current configuration, or call `.resolve(...)` when the transition only needs
|
|
|
493
527
|
to enqueue commands. A block resolver may omit its return because it is
|
|
494
528
|
contextually typed to return `undefined`.
|
|
495
529
|
|
|
496
|
-
Inside `.handle(...)`, `
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
530
|
+
Inside `.handle(...)`, `from` receives the owning machine's public input and
|
|
531
|
+
declared parent protocol contextually. Source and lifecycle callbacks can send
|
|
532
|
+
through `self` and `parent` while retaining the invoked Effect's output and
|
|
533
|
+
error inference:
|
|
500
534
|
|
|
501
535
|
```ts
|
|
502
536
|
const machine = Machine.make({
|
|
@@ -506,36 +540,47 @@ const machine = Machine.make({
|
|
|
506
540
|
// ...
|
|
507
541
|
}).handle({
|
|
508
542
|
Saving: {
|
|
509
|
-
invoke:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
})
|
|
543
|
+
invoke: (from) =>
|
|
544
|
+
from.effect("notify-parent", () => saveDocument)
|
|
545
|
+
.onDone((to) =>
|
|
546
|
+
to.none.resolve(({ parent, self }, enqueue) => {
|
|
547
|
+
enqueue.sendTo(self, Commands.Save())
|
|
548
|
+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
549
|
+
})
|
|
550
|
+
)
|
|
551
|
+
.onFailure((to) => to.none)
|
|
519
552
|
}
|
|
520
553
|
})
|
|
521
554
|
```
|
|
522
555
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
556
|
+
Return an array of completed chains to compose multiple state-owned activities.
|
|
557
|
+
The source computation itself, process logic, or `Machine.child(id, machine)`
|
|
558
|
+
descriptor can be named and reused; the invocation chain stays local so its
|
|
559
|
+
transitions retain the exact owning state and machine protocols.
|
|
526
560
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
561
|
+
```ts
|
|
562
|
+
const refreshCache = Cache.refresh
|
|
563
|
+
|
|
564
|
+
machine.handle({
|
|
565
|
+
Active: {
|
|
566
|
+
invoke: (from) => [
|
|
567
|
+
from.effect("refresh-cache", () => refreshCache).onDone((to) => to.none).onFailure((to) => to.none),
|
|
568
|
+
from.timer("expire-session", "5 minutes").onDone((to) => to.full.Expired())
|
|
569
|
+
]
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
```
|
|
531
573
|
|
|
532
574
|
`onDone` is required for a non-`never` output, and `onFailure` is required for a
|
|
533
|
-
non-`never` typed error
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
575
|
+
non-`never` typed error. Streams additionally require `onElement` when their
|
|
576
|
+
element channel is non-`never` and always require `onDone`; logic and child
|
|
577
|
+
chains optionally expose `onSnapshot`. A handled method disappears from the
|
|
578
|
+
next builder step, so every reachable lifecycle channel is handled exactly
|
|
579
|
+
once. Defects, interruption, and source-construction failures terminate the
|
|
580
|
+
owning runtime. Effect sources are factories evaluated when their state is
|
|
581
|
+
entered. Use an Effect containing `Effect.sleep(...)` for generic work, while
|
|
582
|
+
`from.timer(...)` keeps timer intent explicit and makes static durations visible
|
|
583
|
+
through activity inspection.
|
|
539
584
|
|
|
540
585
|
## Reactivity
|
|
541
586
|
|
|
@@ -579,6 +624,14 @@ const decoded = yield * Machine.decodeSnapshot(machine, encoded)
|
|
|
579
624
|
const ref = yield * Machine.resume(machine, decoded)
|
|
580
625
|
```
|
|
581
626
|
|
|
627
|
+
Decoded snapshots are local runtime values and may contain class instances or
|
|
628
|
+
other process-local data. `encodeSnapshot` is the persistence boundary: it uses
|
|
629
|
+
each declared schema's canonical JSON codec and succeeds only when every active
|
|
630
|
+
state value, completion output, and history value is JSON. Rich values such as
|
|
631
|
+
dates and bigints use their schema-defined JSON representation; cyclic or
|
|
632
|
+
non-JSON values fail with `MachineSchemaEncodeError` instead of escaping to a
|
|
633
|
+
later `JSON.stringify` crash.
|
|
634
|
+
|
|
582
635
|
Resumption restores logical state, values, completion, and history metadata.
|
|
583
636
|
It creates a fresh runtime: active invokes restart, timers restart at their
|
|
584
637
|
full duration, and prior fibers, subscriptions, queues, and child runtimes are
|
|
@@ -631,11 +684,11 @@ Each ESM entrypoint is independent and tree-shakeable.
|
|
|
631
684
|
Every package directly under [`examples/`](./examples) has its own lockfile and
|
|
632
685
|
`check` script.
|
|
633
686
|
|
|
634
|
-
| Example | What it demonstrates
|
|
635
|
-
| ----------------------------------- |
|
|
636
|
-
| [Playground](./examples/playground) | Five focused React examples: atomic turnstile commands, state-scoped traffic-light timers, microwave safety
|
|
637
|
-
| [Pokémon](./examples/pokemon) | Compound
|
|
638
|
-
| [Platformer](./examples/platformer) | Nested parallel statecharts, typed deep history, raised events, state-scoped timers, deterministic model tests, and a playable SVG adapter
|
|
687
|
+
| Example | What it demonstrates |
|
|
688
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
689
|
+
| [Playground](./examples/playground) | Five focused React examples: atomic turnstile commands, state-scoped traffic-light timers, hierarchical microwave safety, a resource-owned media player, and a worker-hosted machine synchronized across tabs |
|
|
690
|
+
| [Pokémon](./examples/pokemon) | Compound workflow states, invoked child machines, typed emissions, Atom reactivity, and a live Effect service |
|
|
691
|
+
| [Platformer](./examples/platformer) | Nested parallel statecharts, typed deep history, raised events, state-scoped timers, deterministic model tests, and a playable SVG adapter |
|
|
639
692
|
|
|
640
693
|
The playground is the shortest path from one concept to working code. The
|
|
641
694
|
standalone examples show larger composition and ownership boundaries.
|