Skip to content

Deliver early atom machine commands and add bridge helper types - #234

Merged
SandroMaglione merged 1 commit into
mainfrom
atom-send-buffering-and-bridge-types
Oct 22, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
atom-send-buffering-and-bridge-types

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Summary

  • MachineAtom.send/stop and ChildMachineAtom.send/stop no longer drop writes made while the machine is still starting (for example while a bound Atom.runtime layer builds after hydration). Such writes are held, the write result is waiting, and they are delivered in write order once the ref is ready. They settle to Success, to the command failure, or to the startup failure. Later writes queue behind held writes so they cannot overtake them. Writes after stop/completion still fail with StoppedError immediately; child writes still fail with ChildNotActiveError when the owning state is inactive.
  • NotReadyError is removed from AtomMachine and from the send/stop failure types. No alias is kept.
  • New helper types: AtomMachine.MachineAtom.Any, MachineAtom.State<A>, MachineAtom.Event<A>, ChildMachineAtom.Any, ChildMachineAtom.State<A>. An event-less bridge (Event = never) is assignable to MachineAtom.Any. The internal AnyMachineAtom/FamilyBridge aliases (core and react) now use them.
  • Implementation: one ordered command queue per bridge per registry, drained by one fiber that waits with AtomRegistry.getResult. Nothing blocks or busy-waits. The write context gets the registry through an internal keep-alive get.registry atom.
  • Tests: new test/unstable/reactivity/AtomMachineCommands.test.ts covers the async-runtime reproduction, write order across readiness, StoppedError after stop, startup failure settling held writes, stop before ready, child writes before parent readiness, and ChildNotActiveError. There is also a React test that uses useAtomSet(machine.send) in a first-mount useEffect with an async runtime, plus typetests for the helpers and command failure types. The new runtime tests fail against the previous implementation.
  • Docs: effect-atom-react.md describes early writes and adds a "Generic helpers" section.

Changeset

  • Added or updated for a library or package-metadata change
  • Not required because this PR does not change a publishable package

One minor changeset for @typeonce/effect-machine. The react package only swaps an internal alias, and its public API is unchanged (same fixed group).

Validation

  • pnpm check. Every step passed locally except format:check, because the dprint plugin host is unreachable from the dev sandbox. Changed files were checked with the same dprint plugin versions installed from npm. One unrelated devtools StaticSite test hit its 5s timeout under load and passed when rerun alone.
  • Automated type-performance measurement passed or was not required. pnpm perf:types passed locally within budgets.
  • Automated runtime- and memory-performance measurement passed or was not required. pnpm perf:runtime was run locally as a smoke test; the CI comparison is authoritative.

Generated by Claude Code

Writes to MachineAtom and ChildMachineAtom send/stop made while the
machine is still starting are now held and delivered in write order
instead of failing with NotReadyError, which is removed. Adds
MachineAtom.Any/State/Event and ChildMachineAtom.Any/State helpers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHnF2EL6ReDTKwoM9TVH7L
@github-actions

Copy link
Copy Markdown
Contributor

Type performance

Measured with TypeScript 6.0.3 and skipLibCheck=true.

Scenario Base PR Difference
Effect only 55 55 0 (0.0%)
Import effect-machine 55 55 0 (0.0%)
Machine.state (3 children) 2,664 2,664 0 (0.0%)
Machine.make (3 states, 2 events) 8,932 8,932 0 (0.0%)
machine.handle (3 states, 2 transitions) 39,348 39,348 0 (0.0%)
declared transition (10 named branches) 60,165 60,165 0 (0.0%)
atomic construction, guards, and reentry 85,402 85,402 0 (0.0%)
registered invocation (input-taking Effect) 39,467 39,467 0 (0.0%)
machine.handle (depth 24) 194,526 194,526 0 (0.0%)
machine.handle (wide depth 16) 218,233 218,233 0 (0.0%)
machine.handle (parallel/history/choice) 91,633 91,633 0 (0.0%)
machine definition (3 independent implementations) 98,225 98,225 0 (0.0%)
machine exact input/output/error/services 82,791 82,791 0 (0.0%)
execution adapter readiness 71,416 71,416 0 (0.0%)

Marginal instantiations are measured against the matching setup without that API call:

Scenario Base PR Difference
Import effect-machine 0 0 0
Machine.state (3 children) 2,609 2,609 0 (0.0%)
Machine.make (3 states, 2 events) 6,260 6,260 0 (0.0%)
machine.handle (3 states, 2 transitions) 30,416 30,416 0 (0.0%)
declared transition (10 named branches) 51,612 51,612 0 (0.0%)
atomic construction, guards, and reentry 70,062 70,062 0 (0.0%)
registered invocation (input-taking Effect) 32,330 32,330 0 (0.0%)
machine.handle (depth 24) 177,217 177,217 0 (0.0%)
machine.handle (wide depth 16) 201,702 201,702 0 (0.0%)
machine.handle (parallel/history/choice) 79,135 79,135 0 (0.0%)
machine definition (3 independent implementations) 87,544 87,544 0 (0.0%)
machine exact input/output/error/services 70,582 70,582 0 (0.0%)
execution adapter readiness 31,918 31,918 0 (0.0%)
Check times (informational)
Scenario Base PR
Effect only 0.02s 0.02s
Import effect-machine 0.02s 0.02s
Machine.state (3 children) 0.05s 0.05s
Machine.make (3 states, 2 events) 0.08s 0.08s
machine.handle (3 states, 2 transitions) 0.22s 0.19s
declared transition (10 named branches) 0.22s 0.24s
atomic construction, guards, and reentry 0.32s 0.30s
registered invocation (input-taking Effect) 0.18s 0.18s
machine.handle (depth 24) 0.35s 0.34s
machine.handle (wide depth 16) 0.43s 0.39s
machine.handle (parallel/history/choice) 0.32s 0.30s
machine definition (3 independent implementations) 0.33s 0.31s
machine exact input/output/error/services 0.31s 0.29s
execution adapter readiness 0.27s 0.26s

Type instantiations are the comparison metric. Check time varies with runner load and is informational only.

@github-actions

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on AMD EPYC 9V74 80-Core Processor with Node v24.22.0.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 148,861 transitions/s
Drain burst with terminal fence 502,368 increments/s
Drain burst with a change observer 471,768 increments/s
Lookup and send to one child 425,840 increments/s
Start and stop a machine 168,663 machines/s
Start and stop a parent with one child 49,456 families/s
Complete an Effect invocation with tracing enabled 13,071 invocations/s
Complete an Effect invocation with tracing disabled 17,239 invocations/s
Plan transitions through a compound state 131,223 transitions/s
Plan transitions through parallel regions 107,518 transitions/s
Drain burst through a compound state 423,422 events/s
Drain burst through two parallel regions 394,248 events/s
Drain a compound-state burst with a change observer 398,263 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 38,257 processes/s
Start and stop a raw compiled process 97,800 processes/s
Memory profile Effect Machine
Idle machine 1.8 KiB
Raw generic managed process 11.5 KiB
Raw compiled process 3.0 KiB
Two independent idle machines 3.4 KiB
Idle parent with one child 5.9 KiB
Parent with observed child registry 9.4 KiB
Parent with observed invoked child snapshots 6.4 KiB

Effect Machine change from base

Metric Base Base variability PR PR variability Difference
Plan counter transitions 149,318 transitions/s 0.5% MAD 148,861 transitions/s 0.6% MAD -0.3%
Drain burst with terminal fence 506,189 increments/s 0.4% MAD 502,368 increments/s 0.4% MAD -0.8%
Drain burst with a change observer 472,223 increments/s 0.3% MAD 471,768 increments/s 0.6% MAD -0.1%
Lookup and send to one child 428,949 increments/s 0.3% MAD 425,840 increments/s 0.1% MAD -0.7%
Start and stop a machine 169,520 machines/s 1.2% MAD 168,663 machines/s 1.0% MAD -0.5%
Start and stop a parent with one child 50,279 families/s 1.0% MAD 49,456 families/s 0.0% MAD -1.6%
Complete an Effect invocation with tracing enabled 13,374 invocations/s 0.6% MAD 13,071 invocations/s 0.2% MAD -2.3%
Complete an Effect invocation with tracing disabled 17,620 invocations/s 1.5% MAD 17,239 invocations/s 0.7% MAD -2.2%
Plan transitions through a compound state 132,088 transitions/s 0.2% MAD 131,223 transitions/s 0.9% MAD -0.7%
Plan transitions through parallel regions 108,110 transitions/s 0.7% MAD 107,518 transitions/s 0.2% MAD -0.5%
Drain burst through a compound state 424,259 events/s 0.6% MAD 423,422 events/s 1.0% MAD -0.2%
Drain burst through two parallel regions 395,340 events/s 0.5% MAD 394,248 events/s 0.2% MAD -0.3%
Drain a compound-state burst with a change observer 397,848 events/s 0.1% MAD 398,263 events/s 0.7% MAD +0.1%
Idle machine heap per unit 1.8 KiB 4.1% MAD 1.8 KiB 0.3% MAD +0.2%
Raw generic managed process heap per unit 11.4 KiB 0.1% MAD 11.5 KiB 0.0% MAD +1.0%
Raw compiled process heap per unit 3.0 KiB 0.1% MAD 3.0 KiB 0.1% MAD +0.4%
Two independent idle machines heap per unit 3.4 KiB 0.9% MAD 3.4 KiB 0.9% MAD -0.6%
Idle parent with one child heap per unit 5.9 KiB 0.0% MAD 5.9 KiB 0.0% MAD 0.0%
Parent with observed child registry heap per unit 9.4 KiB 0.0% MAD 9.4 KiB 0.0% MAD +0.0%
Parent with observed invoked child snapshots heap per unit 6.4 KiB 0.1% MAD 6.4 KiB 0.0% MAD +0.1%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 38,242 processes/s 0.3% MAD 38,257 processes/s 0.4% MAD +0.0%
Start and stop a raw compiled process 98,474 processes/s 1.0% MAD 97,800 processes/s 0.3% MAD -0.7%

Regression guard

No large, noise-adjusted throughput or heap regressions detected.

Versions and interpretation
  • Effect Machine: 0.41.1

Higher throughput is better; lower heap is better. Variability is the median absolute deviation across independent processes, relative to their median. Small differences on shared GitHub-hosted hardware remain informational; the required guard rejects only large changes beyond the measured noise allowance.

@SandroMaglione
SandroMaglione merged commit 4e9d412 into main Oct 22, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the atom-send-buffering-and-bridge-types branch October 22, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants