@typeonce/effect-machine 0.16.0 → 0.17.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 +98 -106
- package/dist/Machine.d.ts +324 -367
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +61 -96
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +7 -7
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.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 +6 -5
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +10 -3
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +8 -6
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +191 -25
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +19 -4
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +56 -20
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
- package/dist/internal/machine/stateDefinition.js +14 -0
- package/dist/internal/machine/stateDefinition.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +9 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +16 -0
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +15 -20
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
- package/dist/internal/testing/machine/transitionCoverage.js +2 -0
- package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +11 -11
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +5 -8
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +2 -2
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +12 -12
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +144 -147
- package/package.json +1 -1
- package/src/Machine.ts +1118 -1178
- package/src/internal/machine/atom.ts +20 -15
- package/src/internal/machine/cluster.ts +14 -9
- package/src/internal/machine/executionPlan.ts +8 -3
- package/src/internal/machine/machine.ts +301 -40
- package/src/internal/machine/planner.ts +89 -27
- package/src/internal/machine/stateDefinition.ts +39 -0
- package/src/internal/machine/topology.ts +28 -0
- package/src/internal/testing/machine/finiteModel.ts +18 -21
- package/src/internal/testing/machine/transitionCoverage.ts +2 -0
- package/src/testing/MachineTest.ts +14 -11
- package/src/unstable/cluster/ClusterMachine.ts +8 -4
- package/src/unstable/reactivity/AtomMachine.ts +19 -12
package/README.md
CHANGED
|
@@ -66,31 +66,19 @@ const CounterDefinition = Machine.make({
|
|
|
66
66
|
id: "Counter",
|
|
67
67
|
states: States.states,
|
|
68
68
|
events: CounterEvent,
|
|
69
|
-
initial: {
|
|
70
|
-
target: (to) => to.Idle(),
|
|
71
|
-
resolve: ({ target }) => target.from()
|
|
72
|
-
}
|
|
69
|
+
initial: (to) => to.Idle().resolve(({ target }) => target.from())
|
|
73
70
|
})
|
|
74
71
|
|
|
75
72
|
const Counter = CounterDefinition.handle({
|
|
76
73
|
Idle: {
|
|
77
74
|
on: {
|
|
78
|
-
Start:
|
|
79
|
-
target: (to) => to.full.Running(),
|
|
80
|
-
resolve: ({ target }) => target.from({ count: 0 })
|
|
81
|
-
})
|
|
75
|
+
Start: (to) => to.full.Running().resolve(({ target }) => target.from({ count: 0 }))
|
|
82
76
|
}
|
|
83
77
|
},
|
|
84
78
|
Running: {
|
|
85
79
|
on: {
|
|
86
|
-
Increment:
|
|
87
|
-
|
|
88
|
-
resolve: ({ state, target }) => target.from({ count: state.count + 1 })
|
|
89
|
-
}),
|
|
90
|
-
Stop: Machine.transition({
|
|
91
|
-
target: (to) => to.full.Idle(),
|
|
92
|
-
resolve: ({ target }) => target.from()
|
|
93
|
-
})
|
|
80
|
+
Increment: (to) => to.full.Running().resolve(({ state, target }) => target.from({ count: state.count + 1 })),
|
|
81
|
+
Stop: (to) => to.full.Idle().resolve(({ target }) => target.from())
|
|
94
82
|
}
|
|
95
83
|
}
|
|
96
84
|
})
|
|
@@ -153,13 +141,13 @@ When sibling states share fields, remove the source discriminator and pass the
|
|
|
153
141
|
remaining fields through the target schema:
|
|
154
142
|
|
|
155
143
|
```ts
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
144
|
+
const handlers = {
|
|
145
|
+
Submit: (to) =>
|
|
146
|
+
to.local.Saving().resolve(({ state, target }) => {
|
|
147
|
+
const { _tag: _, ...fields } = state
|
|
148
|
+
return target.from({ ...fields, attempt: 1 })
|
|
149
|
+
})
|
|
150
|
+
}
|
|
163
151
|
```
|
|
164
152
|
|
|
165
153
|
Omit `schema` when a state represents control flow but owns no data:
|
|
@@ -175,10 +163,11 @@ const States = Machine.states({
|
|
|
175
163
|
}
|
|
176
164
|
})
|
|
177
165
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
166
|
+
const definition = Machine.make({
|
|
167
|
+
states: States.states,
|
|
168
|
+
events: Machine.events(),
|
|
169
|
+
initial: (to) => to.Form.initial.resolve(({ target }) => target((form) => form.Editing.from()))
|
|
170
|
+
})
|
|
182
171
|
```
|
|
183
172
|
|
|
184
173
|
Schema-less states remain active, targetable, matchable, and visible through
|
|
@@ -217,10 +206,7 @@ const definition = Machine.make({
|
|
|
217
206
|
events: CommandEvent,
|
|
218
207
|
internalEvents: InternalEvent,
|
|
219
208
|
emittedEvents: Emissions,
|
|
220
|
-
initial: {
|
|
221
|
-
target: (to) => to.Idle(),
|
|
222
|
-
resolve: ({ target }) => target.from()
|
|
223
|
-
}
|
|
209
|
+
initial: (to) => to.Idle().resolve(({ target }) => target.from())
|
|
224
210
|
})
|
|
225
211
|
```
|
|
226
212
|
|
|
@@ -322,8 +308,8 @@ Invalid event and emission constructions fail the machine with a typed
|
|
|
322
308
|
### Send explicitly between machines
|
|
323
309
|
|
|
324
310
|
`raise` targets the current machine in the same macrostep. `sendTo` targets a
|
|
325
|
-
machine mailbox and is processed later. A
|
|
326
|
-
inputs it may send with `
|
|
311
|
+
machine mailbox and is processed later. A machine that requires an owner
|
|
312
|
+
declares the subset of parent inputs it may send with `Machine.parent`:
|
|
327
313
|
|
|
328
314
|
```ts
|
|
329
315
|
const ParentEvents = Machine.events(ChildFinished)
|
|
@@ -331,23 +317,16 @@ const ParentEvents = Machine.events(ChildFinished)
|
|
|
331
317
|
const child = Machine.make({
|
|
332
318
|
states: ChildStates.states,
|
|
333
319
|
events: ChildEvents,
|
|
334
|
-
|
|
335
|
-
initial: {
|
|
336
|
-
target: (to) => to.Working(),
|
|
337
|
-
resolve: ({ target }) => target.from()
|
|
338
|
-
}
|
|
320
|
+
parent: Machine.parent(ParentEvents),
|
|
321
|
+
initial: (to) => to.Working().resolve(({ target }) => target.from())
|
|
339
322
|
}).handle({
|
|
340
323
|
Working: {
|
|
341
324
|
on: {
|
|
342
|
-
Finish:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (parent !== undefined) {
|
|
346
|
-
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
347
|
-
}
|
|
325
|
+
Finish: (to) =>
|
|
326
|
+
to.full.Done().resolve(({ parent, target }, enqueue) => {
|
|
327
|
+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
348
328
|
return target.from()
|
|
349
|
-
}
|
|
350
|
-
})
|
|
329
|
+
})
|
|
351
330
|
}
|
|
352
331
|
},
|
|
353
332
|
Done: {}
|
|
@@ -357,10 +336,16 @@ const Child = Machine.child("worker", child)
|
|
|
357
336
|
const ParentInputs = Machine.events(Start, ParentEvents)
|
|
358
337
|
```
|
|
359
338
|
|
|
360
|
-
|
|
361
|
-
`
|
|
362
|
-
|
|
339
|
+
`parent` is statically present in every child callback, and root APIs such as
|
|
340
|
+
`Machine.start`, `Machine.planInitial`, Atom machines, and Cluster machines
|
|
341
|
+
reject this machine. When `Child` is invoked, the parent definition must accept
|
|
342
|
+
every declared parent event; otherwise `.handle(...)` is a compile-time error.
|
|
363
343
|
Inside the child, the parent target accepts only those declared events.
|
|
344
|
+
|
|
345
|
+
Use `parent: Machine.optionalParent(ParentEvents)` when the same machine is
|
|
346
|
+
intentionally valid both as a root and as a child. In that case `parent` is
|
|
347
|
+
`MachineTarget<...> | undefined` and must be narrowed before sending. When no
|
|
348
|
+
parent declaration is present, callbacks do not expose a `parent` property.
|
|
364
349
|
`emit` never sends to the parent: it only publishes on the emitting machine's
|
|
365
350
|
`emissions` stream.
|
|
366
351
|
|
|
@@ -383,14 +368,48 @@ paths. `parent` always means the owning machine target.
|
|
|
383
368
|
| `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
|
|
384
369
|
| `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
|
|
385
370
|
|
|
386
|
-
Every
|
|
387
|
-
|
|
371
|
+
Every required transition handler selects a target from its inline `to`
|
|
372
|
+
builder. A bare selection uses the target schema's default construction; call
|
|
373
|
+
`.resolve(...)` when construction depends on handler context. An absent handler
|
|
374
|
+
ignores the trigger; `to.none` handles
|
|
388
375
|
it and retains queued commands, raised events, and emitted events without
|
|
389
|
-
selecting a destination.
|
|
390
|
-
|
|
376
|
+
selecting a destination. Concrete destinations stay narrowed inside their
|
|
377
|
+
resolver, and `to.branches({...})` gives the resolver only the declared named
|
|
378
|
+
`select` builders. Builders describe the
|
|
391
379
|
next logical configuration. Shared states exit and enter only when paths
|
|
392
|
-
change;
|
|
393
|
-
|
|
380
|
+
change; call `.reenter()` for resolver-free reentry or pass `{ reenter: true }`
|
|
381
|
+
to `.resolve(...)` when the source must restart. With `to.none`, reentry
|
|
382
|
+
restarts the source while retaining its configuration.
|
|
383
|
+
|
|
384
|
+
Topology-only definition instructions are values: `to.none`, declared
|
|
385
|
+
`.initial` and history selections, and `to.local.with`. Concrete state and
|
|
386
|
+
choice destinations remain calls such as `to.full.Running()`. Runtime named
|
|
387
|
+
branch builders remain callable, including `select.unchanged()`, because their
|
|
388
|
+
result carries the selected branch evidence.
|
|
389
|
+
|
|
390
|
+
Use `declinable: true` when a resolver may decide that its transition is not
|
|
391
|
+
enabled. Only that resolver receives `decline()`, and its return type expands to
|
|
392
|
+
accept the opaque declined result:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
const handlers = {
|
|
396
|
+
Submit: (to) =>
|
|
397
|
+
to.local.Saving().resolve(
|
|
398
|
+
({ event, target, decline }) => accepts(event) ? target.from({ draft: event.draft }) : decline(),
|
|
399
|
+
{ declinable: true }
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Declining discards work enqueued by that resolver. Event and eventless dispatch
|
|
405
|
+
continues with the next eligible ancestor; if no candidate accepts, no
|
|
406
|
+
transition is selected. This differs from `target.none()`, which consumes the
|
|
407
|
+
trigger and prevents an ancestor from handling it. `transitionDefinitions`
|
|
408
|
+
reports each handler's `acceptance` as `"required"` or `"declinable"` while
|
|
409
|
+
preserving the exact declared target branches. Choices and initial routing must
|
|
410
|
+
remain total and cannot use declinable transitions. Completion and invocation
|
|
411
|
+
outcomes have no ancestor candidate: declining one ignores that lifecycle
|
|
412
|
+
occurrence and leaves the current configuration active.
|
|
394
413
|
|
|
395
414
|
## Statechart capabilities
|
|
396
415
|
|
|
@@ -422,14 +441,8 @@ Loading: {
|
|
|
422
441
|
invoke: Machine.invoke({
|
|
423
442
|
id: "save-document",
|
|
424
443
|
effect: () => saveDocument,
|
|
425
|
-
onDone:
|
|
426
|
-
|
|
427
|
-
resolve: ({ output, target }) => target.from({ id: output.id })
|
|
428
|
-
}),
|
|
429
|
-
onFailure: Machine.transition({
|
|
430
|
-
target: (to) => to.full.Failed(),
|
|
431
|
-
resolve: ({ error, target }) => target.from({ message: String(error) })
|
|
432
|
-
})
|
|
444
|
+
onDone: (to) => to.full.Saved().resolve(({ output, target }) => target.from({ id: output.id })),
|
|
445
|
+
onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) }))
|
|
433
446
|
})
|
|
434
447
|
}
|
|
435
448
|
|
|
@@ -437,10 +450,7 @@ Waiting: {
|
|
|
437
450
|
invoke: Machine.invoke({
|
|
438
451
|
id: "save-timeout",
|
|
439
452
|
after: "3 seconds",
|
|
440
|
-
onDone:
|
|
441
|
-
target: (to) => to.full.Failed(),
|
|
442
|
-
resolve: ({ target }) => target.from({ message: "Timed out" })
|
|
443
|
-
})
|
|
453
|
+
onDone: (to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" }))
|
|
444
454
|
})
|
|
445
455
|
}
|
|
446
456
|
```
|
|
@@ -456,14 +466,8 @@ for state-dependent Effects:
|
|
|
456
466
|
invoke: Machine.invoke({
|
|
457
467
|
id: "load-document",
|
|
458
468
|
effect: ({ state }) => loadDocument(state.documentId),
|
|
459
|
-
onDone:
|
|
460
|
-
|
|
461
|
-
resolve: ({ output, target }) => target.from({ document: output })
|
|
462
|
-
}),
|
|
463
|
-
onFailure: Machine.transition({
|
|
464
|
-
target: (to) => to.full.Failed(),
|
|
465
|
-
resolve: ({ error, target }) => target.from({ message: error.message })
|
|
466
|
-
})
|
|
469
|
+
onDone: (to) => to.full.Ready().resolve(({ output, target }) => target.from({ document: output })),
|
|
470
|
+
onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: error.message }))
|
|
467
471
|
})
|
|
468
472
|
```
|
|
469
473
|
|
|
@@ -475,27 +479,22 @@ macrostep commits:
|
|
|
475
479
|
invoke: Machine.invoke({
|
|
476
480
|
id: "channel",
|
|
477
481
|
stream: () => channelMessages,
|
|
478
|
-
onElement:
|
|
479
|
-
|
|
480
|
-
resolve: ({ element }, enqueue) => {
|
|
482
|
+
onElement: (to) =>
|
|
483
|
+
to.none.resolve(({ element }, enqueue) => {
|
|
481
484
|
enqueue.raise(Events.MessageReceived({ message: element }))
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
onFailure: Machine.transition({
|
|
486
|
-
target: (to) => to.full.Failed(),
|
|
487
|
-
resolve: ({ error, target }) => target.from({ error })
|
|
488
|
-
})
|
|
485
|
+
}),
|
|
486
|
+
onDone: (to) => to.none,
|
|
487
|
+
onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ error }))
|
|
489
488
|
})
|
|
490
489
|
```
|
|
491
490
|
|
|
492
|
-
`
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
491
|
+
`to.none` is the targetless transition value. Return it directly to keep the
|
|
492
|
+
current configuration, or call `.resolve(...)` when the transition only needs
|
|
493
|
+
to enqueue commands. A block resolver may omit its return because it is
|
|
494
|
+
contextually typed to return `undefined`.
|
|
496
495
|
|
|
497
496
|
Inside `.handle(...)`, `Machine.invoke(...)` receives the owning machine's
|
|
498
|
-
public input and
|
|
497
|
+
public input and declared parent protocol contextually. Its source and lifecycle
|
|
499
498
|
callbacks can send through `self` and `parent` while retaining the invoked
|
|
500
499
|
Effect's output and error inference:
|
|
501
500
|
|
|
@@ -503,34 +502,27 @@ Effect's output and error inference:
|
|
|
503
502
|
const machine = Machine.make({
|
|
504
503
|
events: Commands,
|
|
505
504
|
internalEvents: InternalEvents,
|
|
506
|
-
|
|
505
|
+
parent: Machine.parent(ParentEvents)
|
|
507
506
|
// ...
|
|
508
507
|
}).handle({
|
|
509
508
|
Saving: {
|
|
510
509
|
invoke: Machine.invoke({
|
|
511
510
|
id: "notify-parent",
|
|
512
511
|
effect: () => saveDocument,
|
|
513
|
-
onDone:
|
|
514
|
-
|
|
515
|
-
resolve: ({ parent, self }, enqueue) => {
|
|
512
|
+
onDone: (to) =>
|
|
513
|
+
to.none.resolve(({ parent, self }, enqueue) => {
|
|
516
514
|
enqueue.sendTo(self, Commands.Save())
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return undefined
|
|
521
|
-
}
|
|
522
|
-
}),
|
|
523
|
-
onFailure: Machine.transition({
|
|
524
|
-
target: (to) => to.none(),
|
|
525
|
-
resolve: () => undefined
|
|
526
|
-
})
|
|
515
|
+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
516
|
+
}),
|
|
517
|
+
onFailure: (to) => to.none
|
|
527
518
|
})
|
|
528
519
|
}
|
|
529
520
|
})
|
|
530
521
|
```
|
|
531
522
|
|
|
532
|
-
The
|
|
533
|
-
|
|
523
|
+
The standard `Machine.invoke(...)` form retains exact `self` and `parent`
|
|
524
|
+
typing even when the definition is named separately; no intermediate
|
|
525
|
+
definition method is required.
|
|
534
526
|
|
|
535
527
|
A direct `invoke: { ... }` object is also supported when its lifecycle handlers
|
|
536
528
|
do not need source-derived context. Reuse one exported
|