Skip to content

Serve stable server values from machine atoms - #241

Merged
SandroMaglione merged 1 commit into
mainfrom
claude/gifted-bardeen-c3ehlr
Oct 22, 2026
Merged

SandroMaglione merged 1 commit into
mainfrom
claude/gifted-bardeen-c3ehlr

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Summary

  • Bridge atoms built a new server value on every server read, so React warned during hydration that "The result of getServerSnapshot should be cached to avoid an infinite loop", including through MachineState.
  • Each bridge atom's server value is now memoized on the identity of its inputs: the bridge plan for snapshot and child snapshot, and the source server values for result, child result, selectors (select*, matches*), can, and family projections. Repeated reads, including reads from different registries, return the same reference. Derived atoms stay stable because their sources are.
  • useRetainedResult in @typeonce/effect-machine-react computes its server snapshot once per store.
  • Tests: a server-value stability test covering bridge, child, selector, can, family and familyChild atoms, and a hydration assertion that React logs no getServerSnapshot warning. Both fail on main.

Changeset

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

Patch for @typeonce/effect-machine and @typeonce/effect-machine-react.

Validation

  • pnpm check
  • Automated type-performance measurement passed or was not required
  • Automated runtime- and memory-performance measurement passed or was not required

Local pnpm check notes: dprint plugin downloads are blocked in the local sandbox, so formatting was checked with the same plugin versions loaded from npm. packages/devtools/test/StaticSite.test.ts times out locally on main as well; CI is authoritative. pnpm perf:runtime ran successfully as a smoke test.


Generated by Claude Code

Memoize every bridge atom's server value on the identity of its inputs so
repeated server reads return the same reference, and cache the server
snapshot in the React result hook. React no longer warns that
getServerSnapshot should be cached while hydrating MachineState.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01H9xMXPkpeqmvAbWNGY7R55
@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,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.03s 0.03s
Import effect-machine 0.03s 0.03s
Machine.state (3 children) 0.09s 0.09s
Machine.make (3 states, 2 events) 0.14s 0.15s
machine.handle (3 states, 2 transitions) 0.31s 0.34s
declared transition (10 named branches) 0.38s 0.40s
atomic construction, guards, and reentry 0.52s 0.58s
registered invocation (input-taking Effect) 0.31s 0.32s
machine.handle (depth 24) 0.64s 0.66s
machine.handle (wide depth 16) 0.73s 0.73s
machine.handle (parallel/history/choice) 0.54s 0.54s
machine definition (3 independent implementations) 0.55s 0.55s
machine exact input/output/error/services 0.53s 0.51s
execution adapter readiness 0.47s 0.45s

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 INTEL(R) XEON(R) PLATINUM 8573C with Node v24.21.0.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 135,435 transitions/s
Drain burst with terminal fence 464,008 increments/s
Drain burst with a change observer 438,540 increments/s
Lookup and send to one child 400,072 increments/s
Start and stop a machine 147,951 machines/s
Start and stop a parent with one child 42,695 families/s
Complete an Effect invocation with tracing enabled 16,835 invocations/s
Complete an Effect invocation with tracing disabled 20,493 invocations/s
Plan transitions through a compound state 121,952 transitions/s
Plan transitions through parallel regions 97,188 transitions/s
Drain burst through a compound state 378,160 events/s
Drain burst through two parallel regions 341,647 events/s
Drain a compound-state burst with a change observer 359,364 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 35,102 processes/s
Start and stop a raw compiled process 84,381 processes/s
Memory profile Effect Machine
Idle machine 1.9 KiB
Raw generic managed process 11.7 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 134,678 transitions/s 1.3% MAD 135,435 transitions/s 0.3% MAD +0.6%
Drain burst with terminal fence 466,286 increments/s 1.4% MAD 464,008 increments/s 1.0% MAD -0.5%
Drain burst with a change observer 441,794 increments/s 1.3% MAD 438,540 increments/s 0.6% MAD -0.7%
Lookup and send to one child 407,093 increments/s 0.5% MAD 400,072 increments/s 1.0% MAD -1.7%
Start and stop a machine 147,210 machines/s 1.0% MAD 147,951 machines/s 0.9% MAD +0.5%
Start and stop a parent with one child 42,305 families/s 0.8% MAD 42,695 families/s 2.5% MAD +0.9%
Complete an Effect invocation with tracing enabled 17,085 invocations/s 0.5% MAD 16,835 invocations/s 0.9% MAD -1.5%
Complete an Effect invocation with tracing disabled 20,536 invocations/s 0.1% MAD 20,493 invocations/s 0.9% MAD -0.2%
Plan transitions through a compound state 120,607 transitions/s 0.5% MAD 121,952 transitions/s 0.1% MAD +1.1%
Plan transitions through parallel regions 96,414 transitions/s 0.5% MAD 97,188 transitions/s 0.5% MAD +0.8%
Drain burst through a compound state 381,602 events/s 2.1% MAD 378,160 events/s 3.2% MAD -0.9%
Drain burst through two parallel regions 348,352 events/s 3.7% MAD 341,647 events/s 2.3% MAD -1.9%
Drain a compound-state burst with a change observer 360,803 events/s 2.2% MAD 359,364 events/s 2.0% MAD -0.4%
Idle machine heap per unit 1.9 KiB 0.3% MAD 1.9 KiB 0.6% MAD +0.7%
Raw generic managed process heap per unit 11.7 KiB 0.0% MAD 11.7 KiB 0.0% MAD +0.1%
Raw compiled process heap per unit 3.1 KiB 0.1% MAD 3.1 KiB 1.1% MAD +1.0%
Two independent idle machines heap per unit 3.5 KiB 0.1% MAD 3.5 KiB 0.1% MAD +0.1%
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.1%
Parent with observed invoked child snapshots heap per unit 6.9 KiB 0.0% MAD 6.9 KiB 0.2% MAD +0.1%

Process runtime reference change from base

Metric Base Base variability PR PR variability Difference
Start and stop a raw generic process 34,789 processes/s 0.6% MAD 35,102 processes/s 0.6% MAD +0.9%
Start and stop a raw compiled process 84,005 processes/s 0.2% MAD 84,381 processes/s 1.4% MAD +0.4%

Regression guard

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

Versions and interpretation
  • Effect Machine: 0.42.0

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 1d6c808 into main Oct 22, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the claude/gifted-bardeen-c3ehlr branch October 22, 2026 22:31
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