@typeonce/effect-machine 0.17.0 → 0.18.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 CHANGED
@@ -122,7 +122,10 @@ Keep one-off topology inline in `Machine.states`. Use `Machine.state` only when
122
122
  the same active state definition is mounted more than once; tagged schemas are
123
123
  already reusable without it. For repeated finite regions, derive names with
124
124
  `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>`.
125
+ complete tree. Type full-snapshot helpers as `Machine.Snapshot<typeof States>`
126
+ or `Machine.Snapshot<typeof machine>`, schema-backed state payloads as
127
+ `Machine.Value<typeof States, Path>`, and path-rooted snapshots as
128
+ `Machine.SnapshotAt<typeof States, Path>`.
126
129
 
127
130
  ### Construct state through builders
128
131
 
@@ -437,37 +440,35 @@ arbitrary asynchronous Effects do not run inside planning.
437
440
  State-scoped work starts on entry and is interrupted on exit:
438
441
 
439
442
  ```ts
440
- Loading: {
441
- invoke: Machine.invoke({
442
- id: "save-document",
443
- effect: () => saveDocument,
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) }))
446
- })
447
- }
448
-
449
- Waiting: {
450
- invoke: Machine.invoke({
451
- id: "save-timeout",
452
- after: "3 seconds",
453
- onDone: (to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" }))
454
- })
455
- }
443
+ machine.handle({
444
+ Loading: {
445
+ invoke: (from) =>
446
+ from.effect("save-document", () => saveDocument)
447
+ .onDone((to) => to.full.Saved().resolve(({ output, target }) => target.from({ id: output.id })))
448
+ .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) })))
449
+ },
450
+ Waiting: {
451
+ invoke: (from) =>
452
+ from.timer("save-timeout", "3 seconds")
453
+ .onDone((to) => to.full.Failed().resolve(({ target }) => target.from({ message: "Timed out" })))
454
+ }
455
+ })
456
456
  ```
457
457
 
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
461
- `Machine.invoke({...})`. The helper is an identity at runtime and preserves
462
- owner-context and source-channel inference across lifecycle handlers, including
463
- for state-dependent Effects:
458
+ The state-local `from` selector starts an `effect`, `stream`, `timer`, reusable
459
+ `logic`, or complete `child` statechart. The selected source determines which
460
+ lifecycle methods the chain requires and which methods are available. For
461
+ example, an Effect with non-`never` output and error channels must handle both;
462
+ the completed chain is the value returned by the callback:
464
463
 
465
464
  ```ts
466
- invoke: Machine.invoke({
467
- id: "load-document",
468
- effect: ({ state }) => loadDocument(state.documentId),
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 }))
465
+ machine.handle({
466
+ Loading: {
467
+ invoke: (from) =>
468
+ from.effect("load-document", ({ state }) => loadDocument(state.documentId))
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 })))
471
+ }
471
472
  })
472
473
  ```
473
474
 
@@ -476,15 +477,18 @@ is mapped by `onElement`, and the next element is not pulled until that parent
476
477
  macrostep commits:
477
478
 
478
479
  ```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 }))
480
+ machine.handle({
481
+ Listening: {
482
+ invoke: (from) =>
483
+ from.stream("channel", () => channelMessages)
484
+ .onElement((to) =>
485
+ to.none.resolve(({ element }, enqueue) => {
486
+ enqueue.raise(Events.MessageReceived({ message: element }))
487
+ })
488
+ )
489
+ .onDone((to) => to.none)
490
+ .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ error })))
491
+ }
488
492
  })
489
493
  ```
490
494
 
@@ -493,10 +497,10 @@ current configuration, or call `.resolve(...)` when the transition only needs
493
497
  to enqueue commands. A block resolver may omit its return because it is
494
498
  contextually typed to return `undefined`.
495
499
 
496
- Inside `.handle(...)`, `Machine.invoke(...)` receives the owning machine's
497
- public input and declared parent protocol contextually. Its source and lifecycle
498
- callbacks can send through `self` and `parent` while retaining the invoked
499
- Effect's output and error inference:
500
+ Inside `.handle(...)`, `from` receives the owning machine's public input and
501
+ declared parent protocol contextually. Source and lifecycle callbacks can send
502
+ through `self` and `parent` while retaining the invoked Effect's output and
503
+ error inference:
500
504
 
501
505
  ```ts
502
506
  const machine = Machine.make({
@@ -506,36 +510,47 @@ const machine = Machine.make({
506
510
  // ...
507
511
  }).handle({
508
512
  Saving: {
509
- invoke: Machine.invoke({
510
- id: "notify-parent",
511
- effect: () => saveDocument,
512
- onDone: (to) =>
513
- to.none.resolve(({ parent, self }, enqueue) => {
514
- enqueue.sendTo(self, Commands.Save())
515
- enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
516
- }),
517
- onFailure: (to) => to.none
518
- })
513
+ invoke: (from) =>
514
+ from.effect("notify-parent", () => saveDocument)
515
+ .onDone((to) =>
516
+ to.none.resolve(({ parent, self }, enqueue) => {
517
+ enqueue.sendTo(self, Commands.Save())
518
+ enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
519
+ })
520
+ )
521
+ .onFailure((to) => to.none)
519
522
  }
520
523
  })
521
524
  ```
522
525
 
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.
526
+ Return an array of completed chains to compose multiple state-owned activities.
527
+ The source computation itself, process logic, or `Machine.child(id, machine)`
528
+ descriptor can be named and reused; the invocation chain stays local so its
529
+ transitions retain the exact owning state and machine protocols.
526
530
 
527
- A direct `invoke: { ... }` object is also supported when its lifecycle handlers
528
- do not need source-derived context. Reuse one exported
529
- `Machine.child(id, machine)` descriptor for invocation, `sendTo`, and child
530
- lookup.
531
+ ```ts
532
+ const refreshCache = Cache.refresh
533
+
534
+ machine.handle({
535
+ Active: {
536
+ invoke: (from) => [
537
+ from.effect("refresh-cache", () => refreshCache).onDone((to) => to.none).onFailure((to) => to.none),
538
+ from.timer("expire-session", "5 minutes").onDone((to) => to.full.Expired())
539
+ ]
540
+ }
541
+ })
542
+ ```
531
543
 
532
544
  `onDone` is required for a non-`never` output, and `onFailure` is required for a
533
- non-`never` typed error; each handler is omitted when its channel is `never`.
534
- Defects, interruption, and source-construction failures terminate the owning
535
- runtime. Effect sources are always factories evaluated when their state is
536
- entered. Use `effect: () => Effect.sleep(...)` for a generic Effect, while
537
- `after` keeps timers explicit and makes static durations visible through
538
- activity inspection.
545
+ non-`never` typed error. Streams additionally require `onElement` when their
546
+ element channel is non-`never` and always require `onDone`; logic and child
547
+ chains optionally expose `onSnapshot`. A handled method disappears from the
548
+ next builder step, so every reachable lifecycle channel is handled exactly
549
+ once. Defects, interruption, and source-construction failures terminate the
550
+ owning runtime. Effect sources are factories evaluated when their state is
551
+ entered. Use an Effect containing `Effect.sleep(...)` for generic work, while
552
+ `from.timer(...)` keeps timer intent explicit and makes static durations visible
553
+ through activity inspection.
539
554
 
540
555
  ## Reactivity
541
556