Skip to content

fix(core): clarify interpret defaults, consolidate invoked submachine completion, and add test coverage - #268

Open
sytabaresa wants to merge 7 commits into
matthewp:mainfrom
sytabaresa:fix/invoke-errors
Open

sytabaresa wants to merge 7 commits into
matthewp:mainfrom
sytabaresa:fix/invoke-errors

Conversation

@sytabaresa

@sytabaresa sytabaresa commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Motivation

This PR cleans up a few rough edges around ergonomics, runtime safety, and consistency across the library:

  1. No more runtime crashes without onChange: Calling interpret(machine) without an onChange callback previously threw a TypeError on state transitions. Defaulting it to a no-op (() => {}) makes passing a callback optional, matching what the docs and TypeScript types already promised.
  2. Consistent child machine completion: Synchronous and asynchronous submachines handled completion slightly differently. Route completion through send() unifies this behavior—giving you helpful errors in debug mode (d._send) while failing gracefully in production if a 'done' transition isn't defined.
  3. Better test coverage: Filled in missing unit tests for core concepts on the Docs (state creation, transitions, guards, context, and composition) to prevent regressions down the line.
  4. In invokeFnType.enter, the variable name rn was an ambiguous abbreviation for "return value". Renaming it to invokedResult makes the codebase self-documenting by explicitly indicating that the variable contains the result (either a Promise or a child machine) returned from invoking this.fn.call(service, ...). During debugging when the user put a non machine, promise, or function that return a promise in the invoke state, this will help the user track that malformed argument.
  5. Accurate service.child cleanup: When a machine exits an invoked state—via 'done', 'error', or an external transition—we now explicitly clean up service.child (delete service.child) so the reference only exists while a child is actively running. This is important because a parent machine can 'cancel' a child machine by transitioning out of the invoked state, even before the child machine has reached a final state. in this case the library needs to free this machine to align with the API. This not includes a self transition in the parent machine, because some events needs to be handle by the parent, and some by the child machine. and always reset break backtrack compatibility and can be very uncovenient.

Summary of Changes

  • interpret(): Added a default () => {} fallback for onChange and updated the docs to reflect that it's optional.

  • Child Machine Management & Completion::

    • Unified immediate/sync child completion through send(service, { type: 'done', data }). In debug mode, throws a clear error if the parent state lacks a 'done' handler; in normal mode, cleans up cleanly without blowing up.
    • Child Machine Lifecycle (delete service.child): Updated transitionTo() so delete service.child only executes when transitioning to a different state (!isSelfTransition). Self-transitions on invoked states preserve the active child machine instance and its extended state (context) rather than re-initializing them.
    • Renamed rn to invokedResult inside invokeFnType.enter.
  • Test Suite Expansion (packages/core/test/):

    • test-create-machine.js: Initial state selection, context factories, and machine immutability.
    • test-interpret.js: Optional onChange, self-transitions, event payloads, and unhandled events in both debug and normal modes.
    • test-transition.js: Transitions, guard branching, and lifecycle execution order (Guard → Reducer/Action → State Change → Listener).
    • test-reduce.js: Reducer sequence execution and event arguments.
    • test-guard.js: Chained guard short-circuit behavior.
    • test-invoke.js: Child machine completion across debug and normal modes.

Test Results

  • Total: 56 tests (167 assertions)
  • Status: 100% passing (0 failures)

…w a error on normal execution if the 'done' transition don't exists. on debugging execution throws the unknown state error as usual.
…reducers, and add comprehensive test coverage
@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0579bae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
robot3 Minor
robot-docs Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@@ -0,0 +1,6 @@
---
"robot3": minor
"robot-docs": minor

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't version our docs.

Comment thread packages/core/machine.js
let data = service.child.context;
delete service.child;
return transitionTo(service, machine, { type: 'done', data }, this.transitions.get('done'));
return send(service, { type: 'done', data }) || machine;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a big change, but it's hidden in this massive PR. I'd like to see a PR specifically for this change and it spelled out exactly why to do this.

Comment thread packages/core/machine.js

if (d._onEnter) d._onEnter(machine, to, service.context, context, fromEvent);
let isSelfTransition = machine.current === to;
if (!isSelfTransition) delete service.child;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Comment thread packages/core/machine.js
let state = newMachine.state.value;
service.machine = newMachine;
let ret = state.enter(newMachine, service, fromEvent);
let ret = (isSelfTransition && service.child) ?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a change of behavior, arguably a breaking change. This is another I'd like to see submitted as a dedicated PR so it's easier to reason about what it solves.

Comment thread packages/core/machine.js
};

export function interpret(machine, onChange, initialContext, event) {
export function interpret(machine, onChange = () => { }, initialContext, event) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I'm necessarily against it, but what does this solve?

@matthewp

matthewp commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Left some comments. Mostly I'd like to see this broken up into smaller changes since at least a couple of them are changing long-standing behavior and it's difficult to understand the motivation given how much this PR is doing.

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