@typeonce/effect-machine 0.16.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 +157 -150
- package/dist/Machine.d.ts +416 -592
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +55 -136
- 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/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 +8 -6
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +239 -35
- 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 +15 -15
- 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 +13 -13
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +229 -193
- package/package.json +1 -1
- package/src/Machine.ts +1686 -2234
- 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/invocation.ts +1 -1
- package/src/internal/machine/machine.ts +365 -48
- 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/exploration.ts +1 -1
- package/src/internal/testing/machine/finiteModel.ts +18 -21
- package/src/internal/testing/machine/trace.ts +1 -1
- package/src/internal/testing/machine/transitionCoverage.ts +2 -0
- package/src/internal/testing/machine/verification.ts +1 -1
- package/src/testing/MachineTest.ts +18 -15
- package/src/unstable/cluster/ClusterMachine.ts +8 -4
- package/src/unstable/reactivity/AtomMachine.ts +20 -13
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
|
})
|
|
@@ -134,7 +122,10 @@ Keep one-off topology inline in `Machine.states`. Use `Machine.state` only when
|
|
|
134
122
|
the same active state definition is mounted more than once; tagged schemas are
|
|
135
123
|
already reusable without it. For repeated finite regions, derive names with
|
|
136
124
|
`States.path(...)` so every literal in the path family is checked against the
|
|
137
|
-
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>`.
|
|
138
129
|
|
|
139
130
|
### Construct state through builders
|
|
140
131
|
|
|
@@ -153,13 +144,13 @@ When sibling states share fields, remove the source discriminator and pass the
|
|
|
153
144
|
remaining fields through the target schema:
|
|
154
145
|
|
|
155
146
|
```ts
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
147
|
+
const handlers = {
|
|
148
|
+
Submit: (to) =>
|
|
149
|
+
to.local.Saving().resolve(({ state, target }) => {
|
|
150
|
+
const { _tag: _, ...fields } = state
|
|
151
|
+
return target.from({ ...fields, attempt: 1 })
|
|
152
|
+
})
|
|
153
|
+
}
|
|
163
154
|
```
|
|
164
155
|
|
|
165
156
|
Omit `schema` when a state represents control flow but owns no data:
|
|
@@ -175,10 +166,11 @@ const States = Machine.states({
|
|
|
175
166
|
}
|
|
176
167
|
})
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
169
|
+
const definition = Machine.make({
|
|
170
|
+
states: States.states,
|
|
171
|
+
events: Machine.events(),
|
|
172
|
+
initial: (to) => to.Form.initial.resolve(({ target }) => target((form) => form.Editing.from()))
|
|
173
|
+
})
|
|
182
174
|
```
|
|
183
175
|
|
|
184
176
|
Schema-less states remain active, targetable, matchable, and visible through
|
|
@@ -217,10 +209,7 @@ const definition = Machine.make({
|
|
|
217
209
|
events: CommandEvent,
|
|
218
210
|
internalEvents: InternalEvent,
|
|
219
211
|
emittedEvents: Emissions,
|
|
220
|
-
initial: {
|
|
221
|
-
target: (to) => to.Idle(),
|
|
222
|
-
resolve: ({ target }) => target.from()
|
|
223
|
-
}
|
|
212
|
+
initial: (to) => to.Idle().resolve(({ target }) => target.from())
|
|
224
213
|
})
|
|
225
214
|
```
|
|
226
215
|
|
|
@@ -322,8 +311,8 @@ Invalid event and emission constructions fail the machine with a typed
|
|
|
322
311
|
### Send explicitly between machines
|
|
323
312
|
|
|
324
313
|
`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 `
|
|
314
|
+
machine mailbox and is processed later. A machine that requires an owner
|
|
315
|
+
declares the subset of parent inputs it may send with `Machine.parent`:
|
|
327
316
|
|
|
328
317
|
```ts
|
|
329
318
|
const ParentEvents = Machine.events(ChildFinished)
|
|
@@ -331,23 +320,16 @@ const ParentEvents = Machine.events(ChildFinished)
|
|
|
331
320
|
const child = Machine.make({
|
|
332
321
|
states: ChildStates.states,
|
|
333
322
|
events: ChildEvents,
|
|
334
|
-
|
|
335
|
-
initial: {
|
|
336
|
-
target: (to) => to.Working(),
|
|
337
|
-
resolve: ({ target }) => target.from()
|
|
338
|
-
}
|
|
323
|
+
parent: Machine.parent(ParentEvents),
|
|
324
|
+
initial: (to) => to.Working().resolve(({ target }) => target.from())
|
|
339
325
|
}).handle({
|
|
340
326
|
Working: {
|
|
341
327
|
on: {
|
|
342
|
-
Finish:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (parent !== undefined) {
|
|
346
|
-
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
347
|
-
}
|
|
328
|
+
Finish: (to) =>
|
|
329
|
+
to.full.Done().resolve(({ parent, target }, enqueue) => {
|
|
330
|
+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
348
331
|
return target.from()
|
|
349
|
-
}
|
|
350
|
-
})
|
|
332
|
+
})
|
|
351
333
|
}
|
|
352
334
|
},
|
|
353
335
|
Done: {}
|
|
@@ -357,10 +339,16 @@ const Child = Machine.child("worker", child)
|
|
|
357
339
|
const ParentInputs = Machine.events(Start, ParentEvents)
|
|
358
340
|
```
|
|
359
341
|
|
|
360
|
-
|
|
361
|
-
`
|
|
362
|
-
|
|
342
|
+
`parent` is statically present in every child callback, and root APIs such as
|
|
343
|
+
`Machine.start`, `Machine.planInitial`, Atom machines, and Cluster machines
|
|
344
|
+
reject this machine. When `Child` is invoked, the parent definition must accept
|
|
345
|
+
every declared parent event; otherwise `.handle(...)` is a compile-time error.
|
|
363
346
|
Inside the child, the parent target accepts only those declared events.
|
|
347
|
+
|
|
348
|
+
Use `parent: Machine.optionalParent(ParentEvents)` when the same machine is
|
|
349
|
+
intentionally valid both as a root and as a child. In that case `parent` is
|
|
350
|
+
`MachineTarget<...> | undefined` and must be narrowed before sending. When no
|
|
351
|
+
parent declaration is present, callbacks do not expose a `parent` property.
|
|
364
352
|
`emit` never sends to the parent: it only publishes on the emitting machine's
|
|
365
353
|
`emissions` stream.
|
|
366
354
|
|
|
@@ -383,14 +371,48 @@ paths. `parent` always means the owning machine target.
|
|
|
383
371
|
| `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root |
|
|
384
372
|
| `target.history` | Restoring a declared history node | The remembered configuration or its typed default |
|
|
385
373
|
|
|
386
|
-
Every
|
|
387
|
-
|
|
374
|
+
Every required transition handler selects a target from its inline `to`
|
|
375
|
+
builder. A bare selection uses the target schema's default construction; call
|
|
376
|
+
`.resolve(...)` when construction depends on handler context. An absent handler
|
|
377
|
+
ignores the trigger; `to.none` handles
|
|
388
378
|
it and retains queued commands, raised events, and emitted events without
|
|
389
|
-
selecting a destination.
|
|
390
|
-
|
|
379
|
+
selecting a destination. Concrete destinations stay narrowed inside their
|
|
380
|
+
resolver, and `to.branches({...})` gives the resolver only the declared named
|
|
381
|
+
`select` builders. Builders describe the
|
|
391
382
|
next logical configuration. Shared states exit and enter only when paths
|
|
392
|
-
change;
|
|
393
|
-
|
|
383
|
+
change; call `.reenter()` for resolver-free reentry or pass `{ reenter: true }`
|
|
384
|
+
to `.resolve(...)` when the source must restart. With `to.none`, reentry
|
|
385
|
+
restarts the source while retaining its configuration.
|
|
386
|
+
|
|
387
|
+
Topology-only definition instructions are values: `to.none`, declared
|
|
388
|
+
`.initial` and history selections, and `to.local.with`. Concrete state and
|
|
389
|
+
choice destinations remain calls such as `to.full.Running()`. Runtime named
|
|
390
|
+
branch builders remain callable, including `select.unchanged()`, because their
|
|
391
|
+
result carries the selected branch evidence.
|
|
392
|
+
|
|
393
|
+
Use `declinable: true` when a resolver may decide that its transition is not
|
|
394
|
+
enabled. Only that resolver receives `decline()`, and its return type expands to
|
|
395
|
+
accept the opaque declined result:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const handlers = {
|
|
399
|
+
Submit: (to) =>
|
|
400
|
+
to.local.Saving().resolve(
|
|
401
|
+
({ event, target, decline }) => accepts(event) ? target.from({ draft: event.draft }) : decline(),
|
|
402
|
+
{ declinable: true }
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Declining discards work enqueued by that resolver. Event and eventless dispatch
|
|
408
|
+
continues with the next eligible ancestor; if no candidate accepts, no
|
|
409
|
+
transition is selected. This differs from `target.none()`, which consumes the
|
|
410
|
+
trigger and prevents an ancestor from handling it. `transitionDefinitions`
|
|
411
|
+
reports each handler's `acceptance` as `"required"` or `"declinable"` while
|
|
412
|
+
preserving the exact declared target branches. Choices and initial routing must
|
|
413
|
+
remain total and cannot use declinable transitions. Completion and invocation
|
|
414
|
+
outcomes have no ancestor candidate: declining one ignores that lifecycle
|
|
415
|
+
occurrence and leaves the current configuration active.
|
|
394
416
|
|
|
395
417
|
## Statechart capabilities
|
|
396
418
|
|
|
@@ -418,52 +440,35 @@ arbitrary asynchronous Effects do not run inside planning.
|
|
|
418
440
|
State-scoped work starts on entry and is interrupted on exit:
|
|
419
441
|
|
|
420
442
|
```ts
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
Waiting: {
|
|
437
|
-
invoke: Machine.invoke({
|
|
438
|
-
id: "save-timeout",
|
|
439
|
-
after: "3 seconds",
|
|
440
|
-
onDone: Machine.transition({
|
|
441
|
-
target: (to) => to.full.Failed(),
|
|
442
|
-
resolve: ({ target }) => target.from({ message: "Timed out" })
|
|
443
|
-
})
|
|
444
|
-
})
|
|
445
|
-
}
|
|
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
|
+
})
|
|
446
456
|
```
|
|
447
457
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
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:
|
|
454
463
|
|
|
455
464
|
```ts
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
onFailure: Machine.transition({
|
|
464
|
-
target: (to) => to.full.Failed(),
|
|
465
|
-
resolve: ({ error, target }) => target.from({ message: error.message })
|
|
466
|
-
})
|
|
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
|
+
}
|
|
467
472
|
})
|
|
468
473
|
```
|
|
469
474
|
|
|
@@ -472,78 +477,80 @@ is mapped by `onElement`, and the next element is not pulled until that parent
|
|
|
472
477
|
macrostep commits:
|
|
473
478
|
|
|
474
479
|
```ts
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
resolve: ({ error, target }) => target.from({ error })
|
|
488
|
-
})
|
|
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
|
+
}
|
|
489
492
|
})
|
|
490
493
|
```
|
|
491
494
|
|
|
492
|
-
`
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
495
|
+
`to.none` is the targetless transition value. Return it directly to keep the
|
|
496
|
+
current configuration, or call `.resolve(...)` when the transition only needs
|
|
497
|
+
to enqueue commands. A block resolver may omit its return because it is
|
|
498
|
+
contextually typed to return `undefined`.
|
|
496
499
|
|
|
497
|
-
Inside `.handle(...)`, `
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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:
|
|
501
504
|
|
|
502
505
|
```ts
|
|
503
506
|
const machine = Machine.make({
|
|
504
507
|
events: Commands,
|
|
505
508
|
internalEvents: InternalEvents,
|
|
506
|
-
|
|
509
|
+
parent: Machine.parent(ParentEvents)
|
|
507
510
|
// ...
|
|
508
511
|
}).handle({
|
|
509
512
|
Saving: {
|
|
510
|
-
invoke:
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
resolve: ({ parent, self }, enqueue) => {
|
|
516
|
-
enqueue.sendTo(self, Commands.Save())
|
|
517
|
-
if (parent !== undefined) {
|
|
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
518
|
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}),
|
|
523
|
-
onFailure: Machine.transition({
|
|
524
|
-
target: (to) => to.none(),
|
|
525
|
-
resolve: () => undefined
|
|
526
|
-
})
|
|
527
|
-
})
|
|
519
|
+
})
|
|
520
|
+
)
|
|
521
|
+
.onFailure((to) => to.none)
|
|
528
522
|
}
|
|
529
523
|
})
|
|
530
524
|
```
|
|
531
525
|
|
|
532
|
-
|
|
533
|
-
|
|
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.
|
|
534
530
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
```
|
|
539
543
|
|
|
540
544
|
`onDone` is required for a non-`never` output, and `onFailure` is required for a
|
|
541
|
-
non-`never` typed error
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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.
|
|
547
554
|
|
|
548
555
|
## Reactivity
|
|
549
556
|
|