Skip to content

Allow target plus update transitions to enter choice states - #240

Merged
SandroMaglione merged 2 commits into
mainfrom
update-into-choice-states
Oct 22, 2026
Merged

SandroMaglione merged 2 commits into
mainfrom
update-into-choice-states

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Summary

  • A transition that declares { target, update, data } can now target a choice pseudo-state, both inline and in a declared branch group (select.branch({ update: { data } })). The owner is updated first, the choice resolver reads the new value through containingState / ancestors / root, and the chosen branch is entered. With a choice target, data constructs only the owner value.
  • Every branch reachable from the choice, transitively through nested choices, must stay below the updated owner, so an update still never leaves its owner. Escaping branches are a branded type diagnostic (TransitionDefinitionError, e.g. Choice 'Flow.Second' branch 'out' targets 'Outside' outside the updated owner 'Flow') checked in on, always, onDone, invocation outcomes and declared branch groups, and handle rejects them at construction with Machine transition from "Flow.Idle" updates "Flow" before choice "Flow.Second", whose branch "out" targets "Outside" outside that owner. Invalid owners keep the existing message.
  • The generic planner already applied the update before choice resolution; retained transitions record the update on the entering transition and the choice microsteps exactly as a plain choice target does. Indexed execution plans already reject machines with choice nodes, so the compiled path fails closed to the generic plan; differential coverage checks that, plus generic versus compiled managed runtimes.
  • Tests: runtime (resolver sees the updated value, retained evidence, nested choices, reentry, targetless update, declared branch, construction errors, MachineTest laws, text and Mermaid visualization), strategy differential, tstyche acceptance and rejection. README, agent guide and update JSDoc updated.

Changeset

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

Minor: new modelling capability for @typeonce/effect-machine.

Validation

  • pnpm check (format verified locally with the pinned dprint plugin versions)
  • Automated type-performance measurement passed or was not required (pnpm perf:types budgets pass locally)
  • Automated runtime- and memory-performance measurement passed or was not required (pnpm perf:runtime smoke run locally)

Generated by Claude Code

The owner is updated before the choice resolves, so the resolver reads the
new value. Every branch the choice can reach, transitively through nested
choices, must stay below the owner: escaping branches are a branded type
error and are rejected by handle at construction.

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,449 +101 (+0.3%)
declared transition (10 named branches) 60,167 60,402 +235 (+0.4%)
atomic construction, guards, and reentry 85,404 85,710 +306 (+0.4%)
registered invocation (input-taking Effect) 39,707 39,819 +112 (+0.3%)
machine.handle (depth 24) 194,528 194,531 +3 (+0.0%)
machine.handle (wide depth 16) 218,235 218,238 +3 (+0.0%)
machine.handle (parallel/history/choice) 91,635 91,642 +7 (+0.0%)
machine definition (3 independent implementations) 98,227 99,173 +946 (+1.0%)
machine exact input/output/error/services 82,794 82,941 +147 (+0.2%)
execution adapter readiness 71,420 71,428 +8 (+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,517 +101 (+0.3%)
declared transition (10 named branches) 51,614 51,849 +235 (+0.5%)
atomic construction, guards, and reentry 70,064 70,370 +306 (+0.4%)
registered invocation (input-taking Effect) 32,570 32,682 +112 (+0.3%)
machine.handle (depth 24) 177,219 177,222 +3 (+0.0%)
machine.handle (wide depth 16) 201,704 201,707 +3 (+0.0%)
machine.handle (parallel/history/choice) 79,137 79,144 +7 (+0.0%)
machine definition (3 independent implementations) 87,546 88,492 +946 (+1.1%)
machine exact input/output/error/services 70,585 70,732 +147 (+0.2%)
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.06s
Machine.make (3 states, 2 events) 0.10s 0.11s
machine.handle (3 states, 2 transitions) 0.22s 0.23s
declared transition (10 named branches) 0.30s 0.32s
atomic construction, guards, and reentry 0.38s 0.38s
registered invocation (input-taking Effect) 0.23s 0.23s
machine.handle (depth 24) 0.45s 0.45s
machine.handle (wide depth 16) 0.50s 0.52s
machine.handle (parallel/history/choice) 0.39s 0.41s
machine definition (3 independent implementations) 0.39s 0.41s
machine exact input/output/error/services 0.36s 0.42s
execution adapter readiness 0.32s 0.34s

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.21.0.

Pull request baseline

Scenario Effect Machine
Plan counter transitions 150,032 transitions/s
Drain burst with terminal fence 501,625 increments/s
Drain burst with a change observer 470,465 increments/s
Lookup and send to one child 426,962 increments/s
Start and stop a machine 157,011 machines/s
Start and stop a parent with one child 46,900 families/s
Complete an Effect invocation with tracing enabled 13,049 invocations/s
Complete an Effect invocation with tracing disabled 16,938 invocations/s
Plan transitions through a compound state 134,761 transitions/s
Plan transitions through parallel regions 108,465 transitions/s
Drain burst through a compound state 423,070 events/s
Drain burst through two parallel regions 395,983 events/s
Drain a compound-state burst with a change observer 400,803 events/s

Process runtime reference points

Scenario Effect Machine
Start and stop a raw generic process 38,212 processes/s
Start and stop a raw compiled process 93,668 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 149,196 transitions/s 1.5% MAD 150,032 transitions/s 0.9% MAD +0.6%
Drain burst with terminal fence 507,180 increments/s 0.6% MAD 501,625 increments/s 0.6% MAD -1.1%
Drain burst with a change observer 477,201 increments/s 0.8% MAD 470,465 increments/s 0.2% MAD -1.4%
Lookup and send to one child 430,647 increments/s 0.9% MAD 426,962 increments/s 0.4% MAD -0.9%
Start and stop a machine 155,788 machines/s 0.6% MAD 157,011 machines/s 0.5% MAD +0.8%
Start and stop a parent with one child 46,594 families/s 1.0% MAD 46,900 families/s 0.6% MAD +0.7%
Complete an Effect invocation with tracing enabled 13,192 invocations/s 1.0% MAD 13,049 invocations/s 1.1% MAD -1.1%
Complete an Effect invocation with tracing disabled 17,287 invocations/s 0.6% MAD 16,938 invocations/s 1.0% MAD -2.0%
Plan transitions through a compound state 133,983 transitions/s 0.6% MAD 134,761 transitions/s 0.4% MAD +0.6%
Plan transitions through parallel regions 108,930 transitions/s 0.9% MAD 108,465 transitions/s 0.6% MAD -0.4%
Drain burst through a compound state 420,674 events/s 1.2% MAD 423,070 events/s 0.7% MAD +0.6%
Drain burst through two parallel regions 393,518 events/s 0.9% MAD 395,983 events/s 0.3% MAD +0.6%
Drain a compound-state burst with a change observer 400,661 events/s 0.7% MAD 400,803 events/s 0.4% MAD +0.0%
Idle machine heap per unit 1.9 KiB 0.1% MAD 1.9 KiB 0.2% MAD +0.3%
Raw generic managed process heap per unit 11.7 KiB 0.0% MAD 11.7 KiB 0.0% MAD -0.0%
Raw compiled process heap per unit 3.1 KiB 0.0% MAD 3.1 KiB 0.1% MAD +1.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 5.9 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.5% MAD +0.5%
Parent with observed invoked child snapshots heap per unit 6.9 KiB 0.1% MAD 6.9 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,140 processes/s 0.5% MAD 38,212 processes/s 1.2% MAD +0.2%
Start and stop a raw compiled process 93,580 processes/s 0.8% MAD 93,668 processes/s 0.6% MAD +0.1%

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 edebb70 into main Oct 22, 2026
9 checks passed
@SandroMaglione
SandroMaglione deleted the update-into-choice-states branch October 22, 2026 10:45
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