@typeonce/effect-machine 0.15.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 +115 -95
- package/dist/Machine.d.ts +464 -337
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +72 -107
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +2 -0
- package/dist/internal/machine/activities.d.ts.map +1 -1
- package/dist/internal/machine/activities.js +4 -0
- package/dist/internal/machine/activities.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 +16 -4
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +25 -3
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/invocationEvent.d.ts +8 -0
- package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
- package/dist/internal/machine/invocationEvent.js +8 -0
- package/dist/internal/machine/invocationEvent.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 +308 -71
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +26 -4
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +76 -31
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/runtime.d.ts +1 -0
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +25 -16
- package/dist/internal/machine/runtime.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 +20 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +33 -6
- 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 +8 -2
- package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +6 -0
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +13 -12
- 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 +182 -159
- package/package.json +1 -1
- package/src/Machine.ts +1073 -700
- package/src/internal/machine/activities.ts +7 -0
- package/src/internal/machine/atom.ts +20 -15
- package/src/internal/machine/cluster.ts +14 -9
- package/src/internal/machine/executionPlan.ts +14 -4
- package/src/internal/machine/invocation.ts +39 -4
- package/src/internal/machine/invocationEvent.ts +16 -0
- package/src/internal/machine/machine.ts +460 -86
- package/src/internal/machine/planner.ts +106 -30
- package/src/internal/machine/runtime.ts +61 -25
- package/src/internal/machine/stateDefinition.ts +39 -0
- package/src/internal/machine/topology.ts +59 -2
- package/src/internal/testing/machine/finiteModel.ts +18 -21
- package/src/internal/testing/machine/transitionCoverage.ts +8 -2
- package/src/internal/testing/machine/verification.ts +11 -0
- package/src/testing/MachineTest.ts +16 -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
|
|
|
@@ -413,7 +432,7 @@ synchronous. Conditions use ordinary TypeScript control flow. Callbacks may
|
|
|
413
432
|
select state and enqueue explicit `raise`, `emit`, `sendTo`, or `stop` commands;
|
|
414
433
|
arbitrary asynchronous Effects do not run inside planning.
|
|
415
434
|
|
|
416
|
-
## Effects, timers, and child machines
|
|
435
|
+
## Effects, Streams, timers, and child machines
|
|
417
436
|
|
|
418
437
|
State-scoped work starts on entry and is interrupted on exit:
|
|
419
438
|
|
|
@@ -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,16 +450,14 @@ 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
|
```
|
|
447
457
|
|
|
448
|
-
Use `effect` for one Effect, `
|
|
449
|
-
|
|
458
|
+
Use `effect` for one Effect, `stream` for a sequence of externally produced
|
|
459
|
+
values, `after` for a cancellable delay, `logic` for a reusable process, and
|
|
460
|
+
`child` for a complete child statechart—all through
|
|
450
461
|
`Machine.invoke({...})`. The helper is an identity at runtime and preserves
|
|
451
462
|
owner-context and source-channel inference across lifecycle handlers, including
|
|
452
463
|
for state-dependent Effects:
|
|
@@ -455,19 +466,35 @@ for state-dependent Effects:
|
|
|
455
466
|
invoke: Machine.invoke({
|
|
456
467
|
id: "load-document",
|
|
457
468
|
effect: ({ state }) => loadDocument(state.documentId),
|
|
458
|
-
onDone:
|
|
459
|
-
|
|
460
|
-
resolve: ({ output, target }) => target.from({ document: output })
|
|
461
|
-
}),
|
|
462
|
-
onFailure: Machine.transition({
|
|
463
|
-
target: (to) => to.full.Failed(),
|
|
464
|
-
resolve: ({ error, target }) => target.from({ message: error.message })
|
|
465
|
-
})
|
|
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 }))
|
|
466
471
|
})
|
|
467
472
|
```
|
|
468
473
|
|
|
474
|
+
A Stream source remains independent of the parent event protocol. Each element
|
|
475
|
+
is mapped by `onElement`, and the next element is not pulled until that parent
|
|
476
|
+
macrostep commits:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
invoke: Machine.invoke({
|
|
480
|
+
id: "channel",
|
|
481
|
+
stream: () => channelMessages,
|
|
482
|
+
onElement: (to) =>
|
|
483
|
+
to.none.resolve(({ element }, enqueue) => {
|
|
484
|
+
enqueue.raise(Events.MessageReceived({ message: element }))
|
|
485
|
+
}),
|
|
486
|
+
onDone: (to) => to.none,
|
|
487
|
+
onFailure: (to) => to.full.Failed().resolve(({ error, target }) => target.from({ error }))
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
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`.
|
|
495
|
+
|
|
469
496
|
Inside `.handle(...)`, `Machine.invoke(...)` receives the owning machine's
|
|
470
|
-
public input and
|
|
497
|
+
public input and declared parent protocol contextually. Its source and lifecycle
|
|
471
498
|
callbacks can send through `self` and `parent` while retaining the invoked
|
|
472
499
|
Effect's output and error inference:
|
|
473
500
|
|
|
@@ -475,34 +502,27 @@ Effect's output and error inference:
|
|
|
475
502
|
const machine = Machine.make({
|
|
476
503
|
events: Commands,
|
|
477
504
|
internalEvents: InternalEvents,
|
|
478
|
-
|
|
505
|
+
parent: Machine.parent(ParentEvents)
|
|
479
506
|
// ...
|
|
480
507
|
}).handle({
|
|
481
508
|
Saving: {
|
|
482
509
|
invoke: Machine.invoke({
|
|
483
510
|
id: "notify-parent",
|
|
484
511
|
effect: () => saveDocument,
|
|
485
|
-
onDone:
|
|
486
|
-
|
|
487
|
-
resolve: ({ parent, self }, enqueue) => {
|
|
512
|
+
onDone: (to) =>
|
|
513
|
+
to.none.resolve(({ parent, self }, enqueue) => {
|
|
488
514
|
enqueue.sendTo(self, Commands.Save())
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
return undefined
|
|
493
|
-
}
|
|
494
|
-
}),
|
|
495
|
-
onFailure: Machine.transition({
|
|
496
|
-
target: (to) => to.none(),
|
|
497
|
-
resolve: () => undefined
|
|
498
|
-
})
|
|
515
|
+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
516
|
+
}),
|
|
517
|
+
onFailure: (to) => to.none
|
|
499
518
|
})
|
|
500
519
|
}
|
|
501
520
|
})
|
|
502
521
|
```
|
|
503
522
|
|
|
504
|
-
The
|
|
505
|
-
|
|
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.
|
|
506
526
|
|
|
507
527
|
A direct `invoke: { ... }` object is also supported when its lifecycle handlers
|
|
508
528
|
do not need source-derived context. Reuse one exported
|