Skip to content

Commit c01c7d2

Browse files
Add explicit runtime command semantics (#87)
1 parent 0dbb538 commit c01c7d2

10 files changed

Lines changed: 1166 additions & 19 deletions

File tree

.changeset/tidy-runtimes-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": minor
3+
---
4+
5+
Add explicitly named causal and enqueue-oriented runtime command runners. Causal command tests now retain an exact probe step for every processed send, support probe-bound asynchronous waits, attribute processing failures to the submitted command, and format replayable causal transcripts. Deprecate the ambiguous `runRuntimeCommands` and `formatRuntimeTranscript` names in favor of their explicit enqueue-oriented replacements.

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,43 @@ It does not wait for timers or invoked processes to finish. Production code
774774
continues to use enqueue-only `ref.send`; probes are exported only from the
775775
separate testing entry point.
776776

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+
777814
## Current limits
778815

779816
Declarative first-class guards are not part of the current API. Ordinary

docs/agent-guide.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,31 @@ acknowledgement covers the submitted event's synchronous macrostep, state
815815
commit, emissions, and invoke startup; it does not wait for an invoke or timer
816816
to complete. Application code should continue to use `MachineRef.send`.
817817

818+
For generated runtime command sequences, select delivery behavior by name:
819+
820+
```ts
821+
yield* MachineTest.runCausalCommands(probe, commands, causalModel)
822+
yield* MachineTest.runEnqueuedCommands(ref, commands, enqueueModel)
823+
```
824+
825+
Prefer `runCausalCommands` for semantic and reference-model properties. Every
826+
accepted send produces a `SendProcessed` result containing its exact
827+
`ProbeStep`, including ignored and targetless events. A machine processing
828+
error fails that exact command and retains its checked prefix for shrinking.
829+
The next command does not begin until the submitted send's managed macrostep
830+
has completed.
831+
832+
Use `probe.await.until(predicate)` in a causal model step only when the
833+
assertion also requires later timer, invoke, or child activity. It observes the
834+
current runtime snapshot before waiting for subsequent publications, so it
835+
does not miss work that completed immediately after the causal boundary.
836+
837+
Use `runEnqueuedCommands` only when outstanding mailbox work is intentional,
838+
such as burst ordering and queue behavior. Its `RuntimeSynchronization`
839+
policies observe public snapshots but do not turn send acceptance into causal
840+
completion. Do not use the deprecated `runRuntimeCommands` name in new code;
841+
it is an alias for enqueue behavior and hides that important distinction.
842+
818843
## Common compiler errors
819844

820845
### `initial` is not callable

src/internal/testing/machine/probe.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export const probe = <M extends AnyMachine, Error, Output>(
4949
return Effect.succeed(Object.freeze({
5050
machine,
5151
ref,
52+
await: Object.freeze({
53+
none: { _tag: "None" } as const,
54+
until: (
55+
predicate: (
56+
snapshot: Machine.RuntimeSnapshot<
57+
Machine.Machine.Snapshot<Machine.Machine.States<M>>,
58+
Error,
59+
Output
60+
>
61+
) => boolean
62+
) => ({ _tag: "Until", predicate } as const)
63+
}),
5264
sendAndAwait: (event: Machine.Machine.InputEvent<M>) =>
5365
acknowledged.call(ref, event).pipe(
5466
Effect.map(({ after, before, plan }) => {

0 commit comments

Comments
 (0)