@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.
Files changed (85) hide show
  1. package/README.md +115 -95
  2. package/dist/Machine.d.ts +464 -337
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +72 -107
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +2 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +4 -0
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/atom.d.ts +7 -7
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js.map +1 -1
  13. package/dist/internal/machine/cluster.d.ts +2 -2
  14. package/dist/internal/machine/cluster.d.ts.map +1 -1
  15. package/dist/internal/machine/cluster.js +6 -5
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  18. package/dist/internal/machine/executionPlan.js +16 -4
  19. package/dist/internal/machine/executionPlan.js.map +1 -1
  20. package/dist/internal/machine/invocation.d.ts +1 -1
  21. package/dist/internal/machine/invocation.d.ts.map +1 -1
  22. package/dist/internal/machine/invocation.js +25 -3
  23. package/dist/internal/machine/invocation.js.map +1 -1
  24. package/dist/internal/machine/invocationEvent.d.ts +8 -0
  25. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  26. package/dist/internal/machine/invocationEvent.js +8 -0
  27. package/dist/internal/machine/invocationEvent.js.map +1 -1
  28. package/dist/internal/machine/machine.d.ts +8 -6
  29. package/dist/internal/machine/machine.d.ts.map +1 -1
  30. package/dist/internal/machine/machine.js +308 -71
  31. package/dist/internal/machine/machine.js.map +1 -1
  32. package/dist/internal/machine/planner.d.ts +26 -4
  33. package/dist/internal/machine/planner.d.ts.map +1 -1
  34. package/dist/internal/machine/planner.js +76 -31
  35. package/dist/internal/machine/planner.js.map +1 -1
  36. package/dist/internal/machine/runtime.d.ts +1 -0
  37. package/dist/internal/machine/runtime.d.ts.map +1 -1
  38. package/dist/internal/machine/runtime.js +25 -16
  39. package/dist/internal/machine/runtime.js.map +1 -1
  40. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  41. package/dist/internal/machine/stateDefinition.js +14 -0
  42. package/dist/internal/machine/stateDefinition.js.map +1 -1
  43. package/dist/internal/machine/topology.d.ts +20 -0
  44. package/dist/internal/machine/topology.d.ts.map +1 -1
  45. package/dist/internal/machine/topology.js +33 -6
  46. package/dist/internal/machine/topology.js.map +1 -1
  47. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  48. package/dist/internal/testing/machine/finiteModel.js +15 -20
  49. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  50. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  51. package/dist/internal/testing/machine/transitionCoverage.js +8 -2
  52. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  53. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  54. package/dist/internal/testing/machine/verification.js +6 -0
  55. package/dist/internal/testing/machine/verification.js.map +1 -1
  56. package/dist/testing/MachineTest.d.ts +13 -12
  57. package/dist/testing/MachineTest.d.ts.map +1 -1
  58. package/dist/testing/MachineTest.js +5 -8
  59. package/dist/testing/MachineTest.js.map +1 -1
  60. package/dist/unstable/cluster/ClusterMachine.d.ts +2 -2
  61. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  62. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  63. package/dist/unstable/reactivity/AtomMachine.d.ts +12 -12
  64. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  65. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  66. package/docs/agent-guide.md +182 -159
  67. package/package.json +1 -1
  68. package/src/Machine.ts +1073 -700
  69. package/src/internal/machine/activities.ts +7 -0
  70. package/src/internal/machine/atom.ts +20 -15
  71. package/src/internal/machine/cluster.ts +14 -9
  72. package/src/internal/machine/executionPlan.ts +14 -4
  73. package/src/internal/machine/invocation.ts +39 -4
  74. package/src/internal/machine/invocationEvent.ts +16 -0
  75. package/src/internal/machine/machine.ts +460 -86
  76. package/src/internal/machine/planner.ts +106 -30
  77. package/src/internal/machine/runtime.ts +61 -25
  78. package/src/internal/machine/stateDefinition.ts +39 -0
  79. package/src/internal/machine/topology.ts +59 -2
  80. package/src/internal/testing/machine/finiteModel.ts +18 -21
  81. package/src/internal/testing/machine/transitionCoverage.ts +8 -2
  82. package/src/internal/testing/machine/verification.ts +11 -0
  83. package/src/testing/MachineTest.ts +16 -11
  84. package/src/unstable/cluster/ClusterMachine.ts +8 -4
  85. 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: Machine.transition({
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: Machine.transition({
87
- target: (to) => to.full.Running(),
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
- Submit: Machine.transition({
157
- target: (to) => to.local.Saving(),
158
- resolve: ({ state, target }) => {
159
- const { _tag: _, ...fields } = state
160
- return target.from({ ...fields, attempt: 1 })
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
- initial: {
179
- target: (to) => to.Form.initial(),
180
- resolve: ({ target }) => target((form) => form.Editing.from())
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 child declares the subset of parent
326
- inputs it may send with `parentEvents`:
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
- parentEvents: ParentEvents,
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: Machine.transition({
343
- target: (to) => to.full.Done(),
344
- resolve: ({ parent, target }, enqueue) => {
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
- The same child remains isolated and may be started as a root, where `parent` is
361
- `undefined`. When `Child` is invoked, the parent definition must accept every
362
- event in `parentEvents`; otherwise `.handle(...)` is a compile-time error.
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 installed transition handler returns either a concrete target or
387
- `target.none()`. An absent handler ignores the trigger; `target.none()` handles
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. Declared `targets` constrain only concrete
390
- destinations, so `target.none()` is always permitted. Builders describe the
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; use `{ reenter: true, transition }` when the source must restart. With
393
- `target.none()`, reentry restarts the source while retaining its configuration.
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: Machine.transition({
426
- target: (to) => to.full.Saved(),
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: Machine.transition({
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, `after` for a cancellable delay, `logic` for a
449
- reusable process, and `child` for a complete child statechart—all through
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: Machine.transition({
459
- target: (to) => to.full.Ready(),
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 `parentEvents` protocols contextually. Its source and lifecycle
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
- parentEvents: ParentEvents
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: Machine.transition({
486
- target: (to) => to.none(),
487
- resolve: ({ parent, self }, enqueue) => {
512
+ onDone: (to) =>
513
+ to.none.resolve(({ parent, self }, enqueue) => {
488
514
  enqueue.sendTo(self, Commands.Save())
489
- if (parent !== undefined) {
490
- enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
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 machine-bound `definition.invoke(...)` form remains equivalent when a
505
- definition is already named; it is not required for `self` or `parent` typing.
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