Skip to content

Serve planned snapshots from machine atoms on the server and hydrate MachineState without suspending - #239

Merged
SandroMaglione merged 3 commits into
mainfrom
server-safe-machine-atoms
Oct 22, 2026
Merged

SandroMaglione merged 3 commits into
mainfrom
server-safe-machine-atoms

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Summary

  • Reading any AtomMachine bridge atom on the server (Atom.getServerValue, and therefore the server render of @effect/atom-react hooks) no longer starts the machine. Every exported atom now carries an Effect Atom server value derived from one lazily computed, cached startup plan per bridge (generic planner planInitialSync; resumed bridges use their normalized resumed snapshot). Client starts never compute the plan.
    • ref, send, stop: Initial
    • snapshot: Success({ status: "active", state }), waiting (done when the plan completes)
    • result: Success(state), waiting
    • select / selectSnapshot / matches / can: projection of the planned state, waiting
    • child(Child): Success(Option.some(childState)) when the planned parent configuration invokes the fixed descriptor (same entry order, entry context and synchronous input mapper as the runtime), otherwise Success(Option.none()); child projections follow
    • family / familyChild projections: server value of the projected bridge atom
    • Anything that cannot be planned synchronously: Initial, never a defect
  • MachineState (react) no longer suspends. It reads through useSyncExternalStore (server snapshot = the atom's server value), so SSR and hydration render the planned snapshot and the same tree then receives live snapshots: inputs keep their DOM node, text and focus. New optional pending prop renders before any snapshot exists; once a snapshot has rendered it is kept while the machine is still starting (e.g. a bound runtime layer still building after hydration). Failures still throw to the nearest error boundary.
  • Docs: effect-atom-react.md server-rendering semantics, Provider/Suspense guidance and a new "Server rendering and hydration" section; react README updated.

Changeset

  • Added or updated for a library or package-metadata change (minor for @typeonce/effect-machine and @typeonce/effect-machine-react)
  • Not required because this PR does not change a publishable package

Validation

  • pnpm check (all steps; format:check run locally with the same dprint plugin versions on changed files because the plugin download is blocked in the local environment)
  • Automated type-performance measurement passed or was not required
  • Automated runtime- and memory-performance measurement passed or was not required

Generated by Claude Code

Every AtomMachine bridge atom now declares an Effect Atom server value
derived from one lazily planned startup snapshot per bridge, so server
reads never start a machine. MachineState renders through
useSyncExternalStore without suspending, hydrating the planned snapshot
into the live machine without remounting form inputs.

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

github-actions Bot commented Oct 22, 2026 •

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,449 39,449 0 (0.0%)
declared transition (10 named branches) 60,402 60,402 0 (0.0%)
atomic construction, guards, and reentry 85,710 85,710 0 (0.0%)
registered invocation (input-taking Effect) 39,819 39,819 0 (0.0%)
machine.handle (depth 24) 194,531 194,531 0 (0.0%)
machine.handle (wide depth 16) 218,238 218,238 0 (0.0%)
machine.handle (parallel/history/choice) 91,642 91,642 0 (0.0%)
machine definition (3 independent implementations) 99,173 99,173 0 (0.0%)
machine exact input/output/error/services 82,941 82,941 0 (0.0%)
execution adapter readiness 71,428 71,428 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,517 30,517 0 (0.0%)
declared transition (10 named branches) 51,849 51,849 0 (0.0%)
atomic construction, guards, and reentry 70,370 70,370 0 (0.0%)
registered invocation (input-taking Effect) 32,682 32,682 0 (0.0%)
machine.handle (depth 24) 177,222 177,222 0 (0.0%)
machine.handle (wide depth 16) 201,707 201,707 0 (0.0%)
machine.handle (parallel/history/choice) 79,144 79,144 0 (0.0%)
machine definition (3 independent implementations) 88,492 88,492 0 (0.0%)
machine exact input/output/error/services 70,732 70,732 0 (0.0%)
execution adapter readiness 31,922 31,922 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.07s 0.07s
Machine.make (3 states, 2 events) 0.10s 0.11s
machine.handle (3 states, 2 transitions) 0.23s 0.24s
declared transition (10 named branches) 0.29s 0.33s
atomic construction, guards, and reentry 0.38s 0.40s
registered invocation (input-taking Effect) 0.24s 0.24s
machine.handle (depth 24) 0.47s 0.47s
machine.handle (wide depth 16) 0.54s 0.53s
machine.handle (parallel/history/choice) 0.39s 0.41s
machine definition (3 independent implementations) 0.41s 0.43s
machine exact input/output/error/services 0.38s 0.43s
execution adapter readiness 0.34s 0.37s

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

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Runtime performance

Median of 5 independent benchmark processes on AMD EPYC 7763 64-Core Processor with Node v24.21.0.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 114,882 transitions/s
Drain burst with terminal fence 400,683 increments/s
Drain burst with a change observer 376,301 increments/s
Lookup and send to one child 328,225 increments/s
Start and stop a machine 117,426 machines/s
Start and stop a parent with one child 22,956 families/s
Complete an Effect invocation with tracing enabled 7,097 invocations/s
Complete an Effect invocation with tracing disabled 8,577 invocations/s
Plan transitions through a compound state 101,823 transitions/s
Plan transitions through parallel regions 81,576 transitions/s
Drain burst through a compound state 343,029 events/s
Drain burst through two parallel regions 315,913 events/s
Drain a compound-state burst with a change observer 323,137 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 19,381 processes/s
Start and stop a raw compiled process 68,227 processes/s
Memory profile Effect Machine
Idle machine 1.9 KiB
Raw generic managed process 11.6 KiB
Raw compiled process 3.1 KiB
Two independent idle machines 3.5 KiB
Idle parent with one child 6.0 KiB
Parent with observed child registry 9.5 KiB
Parent with observed invoked child snapshots 6.9 KiB

Effect Machine change from base

Metric Base Base variability PR PR variability Difference
Plan counter transitions 113,160 transitions/s 0.6% MAD 114,882 transitions/s 1.2% MAD +1.5%
Drain burst with terminal fence 405,309 increments/s 0.2% MAD 400,683 increments/s 0.6% MAD -1.1%
Drain burst with a change observer 378,510 increments/s 0.3% MAD 376,301 increments/s 0.4% MAD -0.6%
Lookup and send to one child 331,023 increments/s 0.9% MAD 328,225 increments/s 0.8% MAD -0.8%
Start and stop a machine 115,513 machines/s 0.3% MAD 117,426 machines/s 0.1% MAD +1.7%
Start and stop a parent with one child 23,641 families/s 2.3% MAD 22,956 families/s 1.7% MAD -2.9%
Complete an Effect invocation with tracing enabled 7,069 invocations/s 0.4% MAD 7,097 invocations/s 0.8% MAD +0.4%
Complete an Effect invocation with tracing disabled 8,572 invocations/s 1.7% MAD 8,577 invocations/s 1.3% MAD +0.1%
Plan transitions through a compound state 99,764 transitions/s 1.2% MAD 101,823 transitions/s 0.8% MAD +2.1%
Plan transitions through parallel regions 80,926 transitions/s 0.1% MAD 81,576 transitions/s 0.4% MAD +0.8%
Drain burst through a compound state 343,031 events/s 0.1% MAD 343,029 events/s 1.0% MAD -0.0%
Drain burst through two parallel regions 315,889 events/s 0.2% MAD 315,913 events/s 0.9% MAD +0.0%
Drain a compound-state burst with a change observer 322,644 events/s 0.3% MAD 323,137 events/s 0.6% MAD +0.2%
Idle machine heap per unit 1.9 KiB 0.3% MAD 1.9 KiB 0.2% MAD -0.0%
Raw generic managed process heap per unit 11.6 KiB 0.0% MAD 11.6 KiB 0.0% MAD -0.0%
Raw compiled process heap per unit 3.1 KiB 0.1% MAD 3.1 KiB 0.0% MAD +0.0%
Two independent idle machines heap per unit 3.5 KiB 0.0% MAD 3.5 KiB 0.0% MAD -0.0%
Idle parent with one child heap per unit 6.0 KiB 0.0% MAD 6.0 KiB 0.0% MAD -0.0%
Parent with observed child registry heap per unit 9.5 KiB 0.0% MAD 9.5 KiB 0.0% MAD -0.0%
Parent with observed invoked child snapshots heap per unit 6.9 KiB 0.0% MAD 6.9 KiB 0.0% MAD -0.0%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 19,419 processes/s 0.2% MAD 19,381 processes/s 1.3% MAD -0.2%
Start and stop a raw compiled process 68,367 processes/s 0.7% MAD 68,227 processes/s 0.7% MAD -0.2%

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHnF2EL6ReDTKwoM9TVH7L
@SandroMaglione
SandroMaglione merged commit 5eda754 into main Oct 22, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the server-safe-machine-atoms branch October 22, 2026 10:55
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