@typeonce/effect-machine 0.3.0 → 0.5.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 +410 -17
- package/dist/Machine.d.ts +1191 -510
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +371 -565
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/activities.d.ts +57 -0
- package/dist/internal/machine/activities.d.ts.map +1 -0
- package/dist/internal/machine/activities.js +49 -0
- package/dist/internal/machine/activities.js.map +1 -0
- package/dist/internal/machine/atom.d.ts +69 -0
- package/dist/internal/machine/atom.d.ts.map +1 -0
- package/dist/{AtomMachine.js → internal/machine/atom.js} +20 -95
- package/dist/internal/machine/atom.js.map +1 -0
- package/dist/internal/machine/cluster.d.ts +63 -0
- package/dist/internal/machine/cluster.d.ts.map +1 -0
- package/dist/{ClusterMachine.js → internal/machine/cluster.js} +15 -119
- package/dist/internal/machine/cluster.js.map +1 -0
- package/dist/internal/machine/command.d.ts +16 -0
- package/dist/internal/machine/command.d.ts.map +1 -0
- package/dist/internal/machine/command.js +45 -0
- package/dist/internal/machine/command.js.map +1 -0
- package/dist/internal/machine/commandRuntime.d.ts +13 -0
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
- package/dist/internal/machine/commandRuntime.js +16 -0
- package/dist/internal/machine/commandRuntime.js.map +1 -0
- package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +12 -61
- package/dist/internal/machine/configuration.d.ts.map +1 -0
- package/dist/internal/{machineModel.js → machine/configuration.js} +250 -545
- package/dist/internal/machine/configuration.js.map +1 -0
- package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +8 -8
- package/dist/internal/machine/errors.d.ts.map +1 -0
- package/dist/internal/{machineErrors.js → machine/errors.js} +8 -8
- package/dist/internal/machine/errors.js.map +1 -0
- package/dist/internal/machine/executionPlan.d.ts +49 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -0
- package/dist/internal/machine/executionPlan.js +676 -0
- package/dist/internal/machine/executionPlan.js.map +1 -0
- package/dist/internal/machine/invocation.d.ts +23 -0
- package/dist/internal/machine/invocation.d.ts.map +1 -0
- package/dist/internal/machine/invocation.js +77 -0
- package/dist/internal/machine/invocation.js.map +1 -0
- package/dist/internal/machine/machine.d.ts +183 -0
- package/dist/internal/machine/machine.d.ts.map +1 -0
- package/dist/internal/machine/machine.js +552 -0
- package/dist/internal/machine/machine.js.map +1 -0
- package/dist/internal/machine/planner.d.ts +182 -0
- package/dist/internal/machine/planner.d.ts.map +1 -0
- package/dist/internal/machine/planner.js +1082 -0
- package/dist/internal/machine/planner.js.map +1 -0
- package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +11 -5
- package/dist/internal/machine/process.d.ts.map +1 -0
- package/dist/internal/machine/process.js +498 -0
- package/dist/internal/machine/process.js.map +1 -0
- package/dist/internal/machine/protocol.d.ts +34 -0
- package/dist/internal/machine/protocol.d.ts.map +1 -0
- package/dist/internal/machine/protocol.js +182 -0
- package/dist/internal/machine/protocol.js.map +1 -0
- package/dist/internal/machine/readiness.d.ts +4 -0
- package/dist/internal/machine/readiness.d.ts.map +1 -0
- package/dist/internal/machine/readiness.js +2 -0
- package/dist/internal/machine/readiness.js.map +1 -0
- package/dist/internal/machine/runtime.d.ts +255 -0
- package/dist/internal/machine/runtime.d.ts.map +1 -0
- package/dist/internal/machine/runtime.js +1489 -0
- package/dist/internal/machine/runtime.js.map +1 -0
- package/dist/internal/machine/serialization.d.ts +14 -0
- package/dist/internal/machine/serialization.d.ts.map +1 -0
- package/dist/internal/machine/serialization.js +338 -0
- package/dist/internal/machine/serialization.js.map +1 -0
- package/dist/internal/machine/stateDefinition.d.ts +15 -0
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
- package/dist/internal/machine/stateDefinition.js +210 -0
- package/dist/internal/machine/stateDefinition.js.map +1 -0
- package/dist/internal/machine/symbols.d.ts +3 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -0
- package/dist/internal/machine/symbols.js +3 -0
- package/dist/internal/machine/symbols.js.map +1 -0
- package/dist/internal/machine/topology.d.ts +98 -0
- package/dist/internal/machine/topology.d.ts.map +1 -0
- package/dist/internal/machine/topology.js +347 -0
- package/dist/internal/machine/topology.js.map +1 -0
- package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
- package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
- package/dist/internal/testing/machine/arbitrary.js +68 -0
- package/dist/internal/testing/machine/arbitrary.js.map +1 -0
- package/dist/internal/testing/machine/exploration.d.ts +30 -0
- package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
- package/dist/internal/testing/machine/exploration.js +237 -0
- package/dist/internal/testing/machine/exploration.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
- package/dist/internal/testing/machine/finiteModel.js +976 -0
- package/dist/internal/testing/machine/finiteModel.js.map +1 -0
- package/dist/internal/testing/machine/invariant.d.ts +31 -0
- package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
- package/dist/internal/testing/machine/invariant.js +214 -0
- package/dist/internal/testing/machine/invariant.js.map +1 -0
- package/dist/internal/testing/machine/probe.d.ts +26 -0
- package/dist/internal/testing/machine/probe.d.ts.map +1 -0
- package/dist/internal/testing/machine/probe.js +45 -0
- package/dist/internal/testing/machine/probe.js.map +1 -0
- package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
- package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
- package/dist/internal/testing/machine/referenceModel.js +1012 -0
- package/dist/internal/testing/machine/referenceModel.js.map +1 -0
- package/dist/internal/testing/machine/runtime.d.ts +631 -0
- package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
- package/dist/internal/testing/machine/runtime.js +726 -0
- package/dist/internal/testing/machine/runtime.js.map +1 -0
- package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
- package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
- package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
- package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
- package/dist/internal/testing/machine/trace.d.ts +17 -0
- package/dist/internal/testing/machine/trace.d.ts.map +1 -0
- package/dist/internal/testing/machine/trace.js +94 -0
- package/dist/internal/testing/machine/trace.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +36 -0
- package/dist/internal/testing/machine/verification.d.ts.map +1 -0
- package/dist/internal/testing/machine/verification.js +1375 -0
- package/dist/internal/testing/machine/verification.js.map +1 -0
- package/dist/testing/MachineTest.d.ts +1588 -0
- package/dist/testing/MachineTest.d.ts.map +1 -0
- package/dist/testing/MachineTest.js +406 -0
- package/dist/testing/MachineTest.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +56 -45
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
- package/dist/unstable/cluster/ClusterMachine.js +144 -0
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
- package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
- package/dist/unstable/cluster/index.d.ts.map +1 -0
- package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
- package/dist/unstable/cluster/index.js.map +1 -0
- package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +116 -50
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
- package/dist/unstable/reactivity/AtomMachine.js +164 -0
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
- package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
- package/dist/unstable/reactivity/index.d.ts.map +1 -0
- package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
- package/dist/unstable/reactivity/index.js.map +1 -0
- package/docs/agent-guide.md +316 -31
- package/package.json +28 -10
- package/dist/AtomMachine.d.ts.map +0 -1
- package/dist/AtomMachine.js.map +0 -1
- package/dist/ClusterMachine.d.ts.map +0 -1
- package/dist/ClusterMachine.js.map +0 -1
- package/dist/cluster.d.ts.map +0 -1
- package/dist/cluster.js.map +0 -1
- package/dist/internal/machineErrors.d.ts.map +0 -1
- package/dist/internal/machineErrors.js.map +0 -1
- package/dist/internal/machineModel.d.ts.map +0 -1
- package/dist/internal/machineModel.js.map +0 -1
- package/dist/internal/machinePlanner.d.ts +0 -74
- package/dist/internal/machinePlanner.d.ts.map +0 -1
- package/dist/internal/machinePlanner.js +0 -797
- package/dist/internal/machinePlanner.js.map +0 -1
- package/dist/internal/machineProcess.d.ts.map +0 -1
- package/dist/internal/machineProcess.js +0 -181
- package/dist/internal/machineProcess.js.map +0 -1
- package/dist/internal/machineRuntime.d.ts +0 -125
- package/dist/internal/machineRuntime.d.ts.map +0 -1
- package/dist/internal/machineRuntime.js +0 -365
- package/dist/internal/machineRuntime.js.map +0 -1
- package/dist/reactivity.d.ts.map +0 -1
- package/dist/reactivity.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ Schema-first state machines and statecharts for Effect.
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
pnpm add @typeonce/effect-machine effect@4.0.0-beta.
|
|
11
|
+
pnpm add @typeonce/effect-machine effect@4.0.0-beta.107
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
`effect` is an exact peer dependency, not a bundled runtime dependency.
|
|
15
|
-
Consumers must install `effect@4.0.0-beta.
|
|
15
|
+
Consumers must install `effect@4.0.0-beta.107`. Upgrading this package may
|
|
16
16
|
require upgrading Effect in lockstep; do not override the peer to another beta.
|
|
17
17
|
|
|
18
18
|
## Entrypoints
|
|
@@ -21,10 +21,11 @@ require upgrading Effect in lockstep; do not override the peer to another beta.
|
|
|
21
21
|
import { Machine } from "@typeonce/effect-machine"
|
|
22
22
|
import { ClusterMachine } from "@typeonce/effect-machine/cluster"
|
|
23
23
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
24
|
+
import { MachineTest } from "@typeonce/effect-machine/testing"
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
Each ESM entrypoint is independent and tree-shakeable. Importing the root does
|
|
27
|
-
not load the reactivity or
|
|
28
|
+
not load the reactivity, cluster, or testing modules.
|
|
28
29
|
|
|
29
30
|
## First machine
|
|
30
31
|
|
|
@@ -66,6 +67,10 @@ const Counter = Machine.make({
|
|
|
66
67
|
})
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
Handler objects mirror the state definition recursively. `effect-machine` does
|
|
71
|
+
not impose a fixed handler-tree depth; inference continues until TypeScript's
|
|
72
|
+
normal, shape-dependent compiler limits.
|
|
73
|
+
|
|
69
74
|
`initial` is always a function. For a machine with an input schema, the
|
|
70
75
|
initializer receives the decoded input.
|
|
71
76
|
|
|
@@ -126,6 +131,27 @@ const machine = Machine.make({
|
|
|
126
131
|
})
|
|
127
132
|
```
|
|
128
133
|
|
|
134
|
+
Construct reusable events with `Machine.event` when the schema is owned by the
|
|
135
|
+
machine protocol:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const save = Machine.event(machine, Command.cases.Save)
|
|
139
|
+
yield * ref.send(save)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The schema constructor runs once and the decoded value is trusted by that
|
|
143
|
+
machine and definitions derived from it with `handle`. This avoids decoding a
|
|
144
|
+
known event again on every delivery. A configured `Schema.TaggedUnion` can use
|
|
145
|
+
either the union schema itself or one of its `cases`. Treat the returned event
|
|
146
|
+
as immutable; sending it to an unrelated machine goes through that machine's
|
|
147
|
+
normal decoder.
|
|
148
|
+
|
|
149
|
+
Ordinary values remain valid and are decoded at every boundary:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
yield * ref.send({ _tag: "Save" })
|
|
153
|
+
```
|
|
154
|
+
|
|
129
155
|
Handlers and machine logic see the complete union. Local public APIs such as
|
|
130
156
|
`MachineRef.send`, `machineAtom.send`, and `Machine.plan` expose only `events`
|
|
131
157
|
in TypeScript. The local planner and runtime still share the complete event
|
|
@@ -147,7 +173,7 @@ masquerade as an internal result.
|
|
|
147
173
|
|
|
148
174
|
## Statechart structure
|
|
149
175
|
|
|
150
|
-
`Machine.defineStates` accepts atomic, compound, parallel, final, and history
|
|
176
|
+
`Machine.defineStates` accepts atomic, compound, parallel, final, choice, and history
|
|
151
177
|
nodes:
|
|
152
178
|
|
|
153
179
|
```ts
|
|
@@ -205,6 +231,65 @@ Put data on the narrowest state where it is valid. If several sibling phases
|
|
|
205
231
|
share data, prefer storing it on their compound parent instead of copying it
|
|
206
232
|
into every child state.
|
|
207
233
|
|
|
234
|
+
### Choice states
|
|
235
|
+
|
|
236
|
+
A choice is a transient, targetable decision point. Declare it with only
|
|
237
|
+
`type: "choice"`; it has no schema, value, children, lifecycle actions, invoke,
|
|
238
|
+
or event handlers. A choice is therefore absent from `StateIdentifier`, stable
|
|
239
|
+
snapshots, configurations, and encoded snapshots.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const States = Machine.defineStates({
|
|
243
|
+
Flow: {
|
|
244
|
+
schema: State.cases.Flow,
|
|
245
|
+
initial: "Routing",
|
|
246
|
+
states: {
|
|
247
|
+
Routing: { type: "choice" },
|
|
248
|
+
Approved: State.cases.Approved,
|
|
249
|
+
Rejected: State.cases.Rejected
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const machine = Machine.make({
|
|
255
|
+
states: States.states,
|
|
256
|
+
events: [Event],
|
|
257
|
+
initial: () =>
|
|
258
|
+
States.initial.Flow(
|
|
259
|
+
State.cases.Flow.make({ score: 80 }),
|
|
260
|
+
(flow) => flow.Routing()
|
|
261
|
+
)
|
|
262
|
+
}).handle({
|
|
263
|
+
Flow: {
|
|
264
|
+
states: {
|
|
265
|
+
Routing: {
|
|
266
|
+
choice: {
|
|
267
|
+
targets: ["Flow.Approved", "Flow.Rejected"],
|
|
268
|
+
transition: ({ parent, target }) =>
|
|
269
|
+
parent.score >= 70
|
|
270
|
+
? target.local.Approved(State.cases.Approved.make({}))
|
|
271
|
+
: target.local.Rejected(State.cases.Rejected.make({}))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The resolver is ordinary TypeScript or an `Effect`. It receives the triggering
|
|
280
|
+
lifecycle event, typed parent values, target builders, and the normal planning
|
|
281
|
+
capabilities, but no `state` because the choice itself has no state value. Its
|
|
282
|
+
declared `targets` are both a compile-time bound and inspectable graph edges.
|
|
283
|
+
The resolver must return one of them; missing, malformed, or undeclared targets
|
|
284
|
+
fail planning. Choice implementations are required before execution APIs are
|
|
285
|
+
available.
|
|
286
|
+
|
|
287
|
+
Initial, event, completion, always, history-default, and other choice
|
|
288
|
+
transitions settle in the same macrostep. Chained choices use the normal
|
|
289
|
+
infinite-transition limit. Choice nodes never run entry or exit actions and
|
|
290
|
+
never become active while their resolution remains visible in traces and
|
|
291
|
+
coverage as a `choice` transition trigger.
|
|
292
|
+
|
|
208
293
|
### History states
|
|
209
294
|
|
|
210
295
|
A history pseudo-state remembers the last active configuration of its parent.
|
|
@@ -254,6 +339,38 @@ machine.handle({
|
|
|
254
339
|
})
|
|
255
340
|
```
|
|
256
341
|
|
|
342
|
+
A history default is source-independent. It must construct a complete root
|
|
343
|
+
configuration containing the history owner, including every inactive ancestor
|
|
344
|
+
above a nested owner and every required region of a parallel ancestor. For a
|
|
345
|
+
top-level owner, its owner snapshot is already a complete root snapshot.
|
|
346
|
+
|
|
347
|
+
For example, a history node owned by `App.Workspace` can be targeted from an
|
|
348
|
+
unrelated `Closed` root and supplies the complete `App` configuration on first
|
|
349
|
+
use:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
Workspace: {
|
|
353
|
+
history: {
|
|
354
|
+
resume: {
|
|
355
|
+
default: ({ target }) =>
|
|
356
|
+
target.App(
|
|
357
|
+
State.cases.App.make({ workspaceId: "default" }),
|
|
358
|
+
(app) =>
|
|
359
|
+
app.Workspace(
|
|
360
|
+
State.cases.Workspace.make({}),
|
|
361
|
+
(workspace) =>
|
|
362
|
+
workspace.Editing(State.cases.Editing.make({}))
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The containing branch is enforced statically: unrelated roots, sibling
|
|
371
|
+
compound branches that omit the owner, owner-only nested snapshots, and
|
|
372
|
+
incomplete parallel configurations are rejected.
|
|
373
|
+
|
|
257
374
|
Deep history restores every remembered descendant value. Shallow history
|
|
258
375
|
restores the parent and direct-child values, then follows normal initial paths.
|
|
259
376
|
Only compound or parallel states that shallow restoration can enter implicitly
|
|
@@ -314,25 +431,58 @@ Refresh: {
|
|
|
314
431
|
keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
|
|
315
432
|
provides the immediate parent directly and is `undefined` at a root state.
|
|
316
433
|
|
|
317
|
-
|
|
434
|
+
Event, eventless, and completion transition contexts also expose `snapshot`, a
|
|
435
|
+
read-only view of the complete logical configuration captured at the beginning
|
|
436
|
+
of that transition microstep. This lets one parallel region inspect a sibling
|
|
437
|
+
without copying active-state facts into parent values:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
BufferReady: ;
|
|
441
|
+
;(({ snapshot, target }) =>
|
|
442
|
+
States.matches(snapshot, "Player.Network.Online")
|
|
443
|
+
? target.local.Playing(State.cases.Playing.make({}))
|
|
444
|
+
: undefined)
|
|
445
|
+
```
|
|
318
446
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
447
|
+
All non-conflicting handlers selected together observe the same captured
|
|
448
|
+
snapshot. Handlers are synchronous and cannot read mutable live runtime state
|
|
449
|
+
later. `snapshot` is intentionally absent from entry, exit,
|
|
450
|
+
invoke, and choice contexts. In particular, startup and chained choices may run
|
|
451
|
+
before a complete stable snapshot containing their pseudo-source exists.
|
|
452
|
+
|
|
453
|
+
Effect Schema annotations are the metadata source for active states. Annotate
|
|
454
|
+
the schema itself; `Machine.stateNodes` exposes the resolved annotation map:
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
const Saving = State.cases.Saving.annotate({
|
|
458
|
+
title: "Saving document",
|
|
459
|
+
description: "Persisting local changes to the server"
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Schema-less choice and history nodes accept only descriptive `title`,
|
|
464
|
+
`description`, and `documentation` annotations. Titles may be used as display
|
|
465
|
+
labels, but structural paths remain the only identity and targeting mechanism.
|
|
466
|
+
|
|
467
|
+
## Synchronous transitions and actor commands
|
|
468
|
+
|
|
469
|
+
Transition, entry, exit, choice, initial, and history callbacks are synchronous.
|
|
470
|
+
They select state and may enqueue only explicit statechart or actor operations:
|
|
471
|
+
raise an internal event, emit to the parent, send to an invoked child, or stop a
|
|
472
|
+
child. Arbitrary Effects are not accepted at this boundary.
|
|
323
473
|
|
|
324
474
|
```ts
|
|
325
475
|
const handlers = {
|
|
326
|
-
Save: ({ target }) =>
|
|
476
|
+
Save: ({ target }, enqueue) => {
|
|
477
|
+
enqueue.emit(new SaveRequested({}))
|
|
478
|
+
return target.local.Saving.from()
|
|
479
|
+
}
|
|
327
480
|
}
|
|
328
481
|
```
|
|
329
482
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
If an action fails, the runtime keeps the previously published state and
|
|
335
|
-
suppresses emissions from that plan.
|
|
483
|
+
Use `Machine.invokeEffect`, `Machine.invoke`, or an invoked child machine for
|
|
484
|
+
asynchronous work. Their results return to the parent as typed events, keeping
|
|
485
|
+
the transition core deterministic and synchronous.
|
|
336
486
|
|
|
337
487
|
`Machine.plan` and `Machine.planInitial` return a `done` discriminator. When
|
|
338
488
|
`done` is `true`, `output` is the schema-derived structural terminal union;
|
|
@@ -407,6 +557,14 @@ Descriptors are matched by id and machine identity, so independently created
|
|
|
407
557
|
descriptors for the same pair address the same child without a global cache.
|
|
408
558
|
Exporting one descriptor remains the clearest module boundary.
|
|
409
559
|
|
|
560
|
+
`Machine.activityDefinitions(machine)` inspects state-owned work without
|
|
561
|
+
executing it. Static descriptors report their source path, lifecycle id, and
|
|
562
|
+
kind. Timers also report normalized duration and emitted event tag;
|
|
563
|
+
`invokeEffect` mappings are described as dynamic; invoked machines expose only
|
|
564
|
+
safe child identity. A function-valued `invoke` factory is reported as dynamic
|
|
565
|
+
and is never evaluated during inspection. The result is serializable and does
|
|
566
|
+
not contain Effects, closures, services, or child runtimes.
|
|
567
|
+
|
|
410
568
|
## Reactivity
|
|
411
569
|
|
|
412
570
|
`AtomMachine.make` creates a lazy bridge backed by one running machine per
|
|
@@ -465,8 +623,243 @@ contain the machine definition, machine version, services, subscriptions, or
|
|
|
465
623
|
running child processes. Store machine identity and migration/version metadata
|
|
466
624
|
alongside it.
|
|
467
625
|
|
|
626
|
+
Resume a decoded logical snapshot explicitly:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
const encoded = yield * Machine.encodeSnapshot(machine, snapshot)
|
|
630
|
+
const decoded = yield * Machine.decodeSnapshot(machine, encoded)
|
|
631
|
+
const ref = yield * Machine.resume(machine, decoded)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
`resume` does not call `initial`, require machine input, or replay entry,
|
|
635
|
+
transition, completion, eventless, raised-event, or emitted-event work that
|
|
636
|
+
produced the snapshot. The decoded snapshot is the first published logical
|
|
637
|
+
state. A final snapshot immediately yields a completed ref with its output.
|
|
638
|
+
|
|
639
|
+
Resumption creates a fresh runtime. Invokes owned by active states start once in
|
|
640
|
+
normal ancestor/document order and receive `Machine.InitialEvent` as their
|
|
641
|
+
lifecycle event. `invokeEffect` runs again, invoked machines start from their
|
|
642
|
+
own initial state, and `Machine.after` timers restart from their full declared
|
|
643
|
+
duration. Spawned children, queued events, subscriptions, fibers, scopes,
|
|
644
|
+
elapsed timer time, child snapshots, and prior `RuntimeSnapshot` status/errors
|
|
645
|
+
are not restored. Completion and history metadata remain logical state and are
|
|
646
|
+
not replayed. A changed machine definition does not cause `resume` itself to
|
|
647
|
+
evaluate newly enabled `always` or `onDone` transitions.
|
|
648
|
+
|
|
649
|
+
Reactive applications use `AtomMachine.resume(machine, decoded)` for a
|
|
650
|
+
service-free machine or `AtomMachine.bind(runtime).resume(machine, decoded)`
|
|
651
|
+
for a service-backed machine. These bridges have the same lazy one-runtime-per-
|
|
652
|
+
registry ownership and disposal behavior as `AtomMachine.make`.
|
|
653
|
+
|
|
468
654
|
`ClusterMachine` provides a separate persisted entity adapter. Its process-local
|
|
469
|
-
restrictions and delivery guarantees are documented on
|
|
655
|
+
restrictions, checkpoint planning, and delivery guarantees are documented on
|
|
656
|
+
that API. `Machine.resume` is logical resumption, not durable process or cluster
|
|
657
|
+
restoration.
|
|
658
|
+
|
|
659
|
+
## Property-based semantic invariants
|
|
660
|
+
|
|
661
|
+
`MachineTest.verify` checks statechart structure and planner lifecycle laws.
|
|
662
|
+
Application semantics belong in invariants that can be reused across generated
|
|
663
|
+
scenarios and, in future, bounded exploration:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
import { MachineTest } from "@typeonce/effect-machine/testing"
|
|
667
|
+
import { Effect } from "effect"
|
|
668
|
+
|
|
669
|
+
const invariant = MachineTest.invariants(accountMachine)
|
|
670
|
+
const laws = [
|
|
671
|
+
invariant.state(
|
|
672
|
+
"balance is never negative",
|
|
673
|
+
({ snapshot }) =>
|
|
674
|
+
snapshot.value.balance >= 0 ||
|
|
675
|
+
`negative balance: ${snapshot.value.balance}`
|
|
676
|
+
),
|
|
677
|
+
invariant.step(
|
|
678
|
+
"withdrawal removes exactly its amount",
|
|
679
|
+
({ before, event, after }) =>
|
|
680
|
+
event._tag !== "Withdraw" ||
|
|
681
|
+
after.value.balance === before.value.balance - event.amount
|
|
682
|
+
)
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
const generated = MachineTest.scenarios(accountMachine, {
|
|
686
|
+
minEvents: 0,
|
|
687
|
+
maxEvents: 30
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
it.effect.prop(
|
|
691
|
+
"preserves account laws",
|
|
692
|
+
{ scenario: generated.arbitrary },
|
|
693
|
+
({ scenario }) =>
|
|
694
|
+
MachineTest.run(accountMachine, scenario).pipe(
|
|
695
|
+
Effect.tap((trace) => MachineTest.verify(accountMachine, trace)),
|
|
696
|
+
Effect.flatMap((trace) => MachineTest.assertInvariants(accountMachine, trace, laws))
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
State invariants observe settled startup and public-event states by default.
|
|
702
|
+
Set `observe` to `"microsteps"`, `"all"`, or `"final"` for a different scope.
|
|
703
|
+
Use `when` for conditional laws. A condition with no matches is reported as
|
|
704
|
+
`untested`; add `require: { minObservations: 1 }` when a particular trace must
|
|
705
|
+
exercise it. `checkInvariants` returns this report, while `assertInvariants`
|
|
706
|
+
returns `void` for direct use in property tests. Failures retain the complete
|
|
707
|
+
shrunk trace and precise event, microstep, configuration, and observation
|
|
708
|
+
location.
|
|
709
|
+
|
|
710
|
+
These APIs inspect planner evidence. Staged action effects, invokes, timing,
|
|
711
|
+
and process scheduling require the runtime command-model APIs instead.
|
|
712
|
+
|
|
713
|
+
Use bounded exploration when random scenarios should be complemented by a
|
|
714
|
+
systematic search over concrete event representatives:
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
const explored = yield * MachineTest.explore(accountMachine, {
|
|
718
|
+
events: ({ snapshot }) => [
|
|
719
|
+
new Deposit({ amount: 1 }),
|
|
720
|
+
new Withdraw({ amount: snapshot.value.balance }),
|
|
721
|
+
new Withdraw({ amount: snapshot.value.balance + 1 })
|
|
722
|
+
],
|
|
723
|
+
stateKey: ({ snapshot }) => `${snapshot.value._tag}:${snapshot.value.balance}`,
|
|
724
|
+
limits: {
|
|
725
|
+
maxDepth: 20,
|
|
726
|
+
maxStates: 1_000,
|
|
727
|
+
maxTransitions: 10_000
|
|
728
|
+
},
|
|
729
|
+
invariants: laws
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
const rejected = yield * MachineTest.assertReachable(
|
|
733
|
+
explored,
|
|
734
|
+
"insufficient funds rejection",
|
|
735
|
+
({ configuration }) => configuration.includes("Rejected")
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
console.log(rejected.trace.scenario.events) // shortest witness
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Exploration is breadth-first, so each retained node owns its shortest trace.
|
|
742
|
+
It is exhaustive only for the concrete events returned by `events` and the
|
|
743
|
+
equivalence relation defined by `stateKey`. Equal keys intentionally collapse
|
|
744
|
+
snapshots and only the first representative is expanded. Results distinguish
|
|
745
|
+
`Complete` from `Truncated` and retain the depth, state, or transition frontier
|
|
746
|
+
that hit a limit. An unreachability assertion succeeds only for a complete
|
|
747
|
+
result; otherwise it fails as inconclusive. Cycles are retained as graph edges,
|
|
748
|
+
but exploration does not enumerate every cyclic path. Invariants are checked
|
|
749
|
+
on startup and on each planned edge extending a node's shortest trace.
|
|
750
|
+
|
|
751
|
+
## Causal runtime probes
|
|
752
|
+
|
|
753
|
+
Pure traces do not execute invokes or the managed runtime. When a test needs to
|
|
754
|
+
prove that one live event has actually left the mailbox, attach a testing-only
|
|
755
|
+
probe to a statechart reference:
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
const ref = yield * Machine.start(machine)
|
|
759
|
+
const probe = yield * MachineTest.probe(machine, ref)
|
|
760
|
+
|
|
761
|
+
const step = yield * probe.sendAndAwait(new CancelRequested({}))
|
|
762
|
+
|
|
763
|
+
assert.strictEqual(step.handled, false)
|
|
764
|
+
assert.deepStrictEqual(step.before, step.after)
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
`sendAndAwait` completes after that event's synchronous macrostep and managed
|
|
768
|
+
commit work. It also completes for ignored events, which publish no snapshot
|
|
769
|
+
and therefore cannot be synchronized by waiting for `ref.changes`.
|
|
770
|
+
|
|
771
|
+
The step retains the exact runtime plan, before/after logical snapshots, and
|
|
772
|
+
whether the event was handled or changed/reentered the active configuration.
|
|
773
|
+
It does not wait for timers or invoked processes to finish. Production code
|
|
774
|
+
continues to use enqueue-only `ref.send`; probes are exported only from the
|
|
775
|
+
separate testing entry point.
|
|
776
|
+
|
|
777
|
+
For command-model and property tests, choose the delivery semantics explicitly.
|
|
778
|
+
`runCausalCommands` requires a probe and completes every accepted send before
|
|
779
|
+
checking it or starting the next command:
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
const transcript = yield * MachineTest.runCausalCommands(
|
|
783
|
+
probe,
|
|
784
|
+
commands,
|
|
785
|
+
{
|
|
786
|
+
initialModel,
|
|
787
|
+
transition: (model, command) =>
|
|
788
|
+
Effect.succeed({
|
|
789
|
+
model: updateModel(model, command),
|
|
790
|
+
expected: expectedResult(model, command)
|
|
791
|
+
}),
|
|
792
|
+
assert: ({ actual, expected }) =>
|
|
793
|
+
Effect.sync(() => {
|
|
794
|
+
if (actual.result._tag === "SendProcessed") {
|
|
795
|
+
assert.deepStrictEqual(actual.result.step.after, expected.snapshot)
|
|
796
|
+
assert.strictEqual(actual.result.step.handled, expected.handled)
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
A causal model step needs no synchronization policy. Use the probe-bound
|
|
804
|
+
`probe.await.until(...)` only for later asynchronous work such as a timer,
|
|
805
|
+
invoke result, or child delivery. The predicate sees the exact runtime snapshot
|
|
806
|
+
type. `actual.awaited` retains every snapshot tested by that explicit wait.
|
|
807
|
+
|
|
808
|
+
Use `runEnqueuedCommands(ref, ...)` when the property intentionally submits
|
|
809
|
+
bursts or retains outstanding mailbox work. Its model steps continue to use
|
|
810
|
+
`RuntimeSynchronization`. The old `runRuntimeCommands` and
|
|
811
|
+
`formatRuntimeTranscript` names are deprecated aliases for the enqueue-oriented
|
|
812
|
+
runner and formatter because their delivery semantics were not visible.
|
|
813
|
+
|
|
814
|
+
### Runtime invariants and planner agreement
|
|
815
|
+
|
|
816
|
+
Planner invariants and runtime invariants are deliberately separate. Runtime
|
|
817
|
+
laws inspect causal command evidence, explicit asynchronous observations, and
|
|
818
|
+
runtime status without requiring a duplicate reference model:
|
|
819
|
+
|
|
820
|
+
```ts
|
|
821
|
+
const invariant = MachineTest.runtimeInvariants(machine)
|
|
822
|
+
const laws = [
|
|
823
|
+
invariant.snapshot("count never becomes negative", ({ snapshot }) => snapshot.state.value.count >= 0),
|
|
824
|
+
invariant.command(
|
|
825
|
+
"every accepted add is processed",
|
|
826
|
+
({ command, result }) =>
|
|
827
|
+
command._tag !== "Send" || command.event._tag !== "Add" ||
|
|
828
|
+
result._tag === "SendProcessed"
|
|
829
|
+
)
|
|
830
|
+
]
|
|
831
|
+
|
|
832
|
+
const transcript = yield * MachineTest.verifyCausalCommands(
|
|
833
|
+
probe,
|
|
834
|
+
commands,
|
|
835
|
+
{ invariants: laws }
|
|
836
|
+
)
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
Use the existing `runCausalCommands` when a simplified application model
|
|
840
|
+
provides exact expected results. Its returned transcript implements the same
|
|
841
|
+
model-independent evidence interface, so reusable runtime laws compose with
|
|
842
|
+
it directly:
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
const transcript = yield * MachineTest.runCausalCommands(probe, commands, model)
|
|
846
|
+
|
|
847
|
+
yield * MachineTest.assertRuntimeInvariants(machine, transcript, laws)
|
|
848
|
+
yield * MachineTest.assertPlannerRuntimeAgreement(machine, transcript)
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
`checkRuntimeInvariants` returns an aggregate report; `assertRuntimeInvariants`
|
|
852
|
+
fails with every predicate and non-vacuity violation. Snapshot laws observe the
|
|
853
|
+
initial and post-command snapshots by default. Select `"awaited"`, `"all"`, or
|
|
854
|
+
`"final"` explicitly when a law targets observations retained by
|
|
855
|
+
`probe.await.until` or only the final runtime snapshot.
|
|
856
|
+
|
|
857
|
+
`assertPlannerRuntimeAgreement` is an explicit consistency check, not an
|
|
858
|
+
application oracle. For each processed send it freshly plans from the receipt's
|
|
859
|
+
`before` snapshot and compares handled/change flags, the public next snapshots,
|
|
860
|
+
completion, command counts, emitted events, and public microstep evidence. It
|
|
861
|
+
does not prove that the planner implements the intended business rules; use a
|
|
862
|
+
reference model and runtime invariants for that.
|
|
470
863
|
|
|
471
864
|
## Current limits
|
|
472
865
|
|