Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Transition function #4954

Open
wants to merge 73 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
9f6f015
Use defaultActionExecutor (temp)
davidkpiano Jun 4, 2024
abfe358
Convert entry/exit events
davidkpiano Jun 4, 2024
6e181ea
UnknownAction -> UnknownActionObject
davidkpiano Jun 4, 2024
f1100ab
Use defaultActionExecutor
davidkpiano Jun 5, 2024
8e25fa1
Cleanup
davidkpiano Jun 5, 2024
b2b974a
WIP
davidkpiano Jun 5, 2024
6dd475c
Merge branch 'main' into davidkpiano/transition
davidkpiano Jun 12, 2024
6533540
Merge branch 'main' into davidkpiano/transition
davidkpiano Jun 18, 2024
7641ec5
Fixing tests
davidkpiano Jun 19, 2024
9942382
Add toJSON to built-in actions
davidkpiano Jun 19, 2024
f566149
Merge branch 'main' into davidkpiano/transition
davidkpiano Jun 23, 2024
efaac40
Add docs
davidkpiano Jun 25, 2024
8b936ab
Update packages/core/src/actions/spawnChild.ts
davidkpiano Jul 3, 2024
ae70cc6
Update packages/core/src/stateUtils.ts
davidkpiano Jul 3, 2024
f3c360f
Unify convertAction
davidkpiano Jul 4, 2024
32b278c
Update packages/core/src/stateUtils.ts
davidkpiano Jul 4, 2024
b1d346e
Remove TODO
davidkpiano Jul 4, 2024
4f61ac7
Introduce `executeAction`, remove `action.execute()`
davidkpiano Jul 4, 2024
bc431d1
Enqueue actions test
davidkpiano Jul 4, 2024
96ba826
Fix type error
davidkpiano Jul 4, 2024
abf3a2c
Expose `executeAction(…)`
davidkpiano Jul 8, 2024
d97cb8f
Provide actor to action
davidkpiano Jul 8, 2024
5f6a00b
Fix type issue
davidkpiano Jul 8, 2024
c0597ee
Merge branch 'main' into davidkpiano/transition
davidkpiano Jul 11, 2024
59daf7c
Merge branch 'main' into davidkpiano/transition
davidkpiano Jul 11, 2024
1f13746
Merge branch 'main' into davidkpiano/transition
davidkpiano Jul 13, 2024
a73a0db
Changeset
davidkpiano Jul 13, 2024
4c8d491
Deprecate getNextSnapshot and getInitialSnapshot
davidkpiano Jul 14, 2024
cbb0a7f
Update packages/core/src/stateUtils.ts
davidkpiano Jul 18, 2024
18f0015
Restore getNextSnapshot.test.ts file
davidkpiano Jul 18, 2024
e2e821c
function -> exec
davidkpiano Jul 18, 2024
ad422a8
Merge branch 'main' into davidkpiano/transition
davidkpiano Jul 24, 2024
737f67f
Merge branch 'main' into davidkpiano/transition
davidkpiano Jul 30, 2024
f7991ad
Merge branch 'main' into davidkpiano/transition
davidkpiano Aug 3, 2024
97713c5
Merge branch 'main' into davidkpiano/transition
davidkpiano Aug 7, 2024
0c29c2c
Delayed raise action test
davidkpiano Aug 7, 2024
6e18296
Getting close
davidkpiano Aug 8, 2024
7146bdc
Update scheduler to handle delayed sendTo actions without an initiall…
davidkpiano Aug 9, 2024
e776f2c
Merge branch 'main' into davidkpiano/transition
davidkpiano Aug 10, 2024
3d76b58
Fix types
davidkpiano Aug 10, 2024
7e05a9a
Merge branch 'main' into davidkpiano/transition
davidkpiano Aug 17, 2024
857c96c
WIP
davidkpiano Aug 17, 2024
59eedc2
Remove test code
davidkpiano Aug 28, 2024
574c4c9
Merge branch 'main' into davidkpiano/transition
davidkpiano Sep 1, 2024
4cdd545
Default actor
davidkpiano Sep 1, 2024
d2e583f
Serialization in test
davidkpiano Sep 1, 2024
fea607b
Revert invoke.test.ts
davidkpiano Sep 5, 2024
7a33175
Cancel action execution
davidkpiano Sep 5, 2024
0c80c66
WIP
davidkpiano Sep 5, 2024
f596786
Merge branch 'main' into davidkpiano/transition
davidkpiano Sep 14, 2024
062676c
Update launch.json and jest.config.js
davidkpiano Sep 14, 2024
2d480e1
Proof of concept for invoked actions
davidkpiano Sep 14, 2024
2e450c7
Merge branch 'main' into davidkpiano/transition
davidkpiano Sep 23, 2024
a75ceb9
Include resolved input & systemId in spawnChild action
davidkpiano Sep 23, 2024
200fc12
Refactor action types to use ExecutableActionObject and add startedAt…
davidkpiano Sep 24, 2024
edb0615
Merge branch 'main' into davidkpiano/transition
davidkpiano Sep 26, 2024
96f4ac4
Add ExecutableActionsFrom
davidkpiano Sep 26, 2024
9c077d6
Clean up types
davidkpiano Sep 28, 2024
5bd5e09
Merge branch 'main' into davidkpiano/transition
davidkpiano Oct 9, 2024
cf9d549
Lint
davidkpiano Oct 9, 2024
68654be
Lint for real
davidkpiano Oct 9, 2024
7622a79
Lint lint
davidkpiano Oct 9, 2024
4f61f39
Back to any
davidkpiano Oct 9, 2024
a365418
Update packages/core/test/transition.test.ts
davidkpiano Oct 9, 2024
f6de768
use sleep
Andarist Oct 11, 2024
1977a9d
remove outdated comment
Andarist Oct 11, 2024
d202b93
add `ExecutableSendToAction` to `SpecialExecutableAction`
Andarist Oct 11, 2024
0c344da
remove `startedAt`
Andarist Oct 11, 2024
ece423d
add failing raise test case
Andarist Oct 12, 2024
9576616
tweak test title
Andarist Oct 12, 2024
85b1f67
add extra cancel tests
Andarist Oct 12, 2024
9d3f2a0
add failing test for invalid event delivery
Andarist Oct 12, 2024
4516833
Revert test (happens in main)
davidkpiano Oct 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/lemon-needles-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'xstate': minor
---

Added a new `transition` function that takes an actor logic, a snapshot, and an event, and returns a tuple containing the next snapshot and the actions to execute. This function is a pure function and does not execute the actions itself. It can be used like this:

```ts
import { transition } from 'xstate';

const [nextState, actions] = transition(actorLogic, currentState, event);
// Execute actions as needed
```

Added a new `initialTransition` function that takes an actor logic and an optional input, and returns a tuple containing the initial snapshot and the actions to execute from the initial transition. This function is also a pure function and does not execute the actions itself. It can be used like this:

```ts
import { initialTransition } from 'xstate';

const [initialState, actions] = initialTransition(actorLogic, input);
// Execute actions as needed
```

These new functions provide a way to separate the calculation of the next snapshot and actions from the execution of those actions, allowing for more control and flexibility in the transition process.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js"
}
},
{
Expand All @@ -30,7 +30,7 @@
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js"
}
}
]
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,16 @@ Read [📽 the slides](http://slides.com/davidkhourshid/finite-state-machines) (

## Packages

| Package | Description |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter |
| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState |
| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications |
| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications |
| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications |
| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications |
| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState |
| [🏪 `@xstate/store`](https://github.com/statelyai/xstate/tree/main/packages/xstate-store) | Small library for simple state management |
| Package | Description |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter |
| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState |
| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications |
| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications |
| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications |
| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications |
| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState |
| [🏪 `@xstate/store`](https://github.com/statelyai/xstate/tree/main/packages/xstate-store) | Small library for simple state management |

## Finite State Machines

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { constants } = require('jest-config');
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
prettierPath: null,
setupFilesAfterEnv: ['@xstate-repo/jest-utils/setup'],
setupFilesAfterEnv: ['<rootDir>/scripts/jest-utils/setup'],
transform: {
[constants.DEFAULT_JS_PATTERN]: 'babel-jest',
'^.+\\.vue$': '@vue/vue3-jest',
Expand Down
19 changes: 14 additions & 5 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './State.ts';
import { StateNode } from './StateNode.ts';
import {
executeAction,
getAllStateNodes,
getInitialStateNodes,
getStateNodeByPath,
Expand Down Expand Up @@ -46,9 +47,11 @@ import type {
StateMachineDefinition,
StateValue,
TransitionDefinition,
UnknownActionObject,
ResolvedStateMachineTypes,
StateSchema,
SnapshotStatus
SnapshotStatus,
ExecutableActionObject
} from './types.ts';
import { resolveReferencedActor, toStatePath } from './utils.ts';

Expand Down Expand Up @@ -293,7 +296,8 @@ export class StateMachine<
TMeta,
TConfig
> {
return macrostep(snapshot, event, actorScope).snapshot as typeof snapshot;
return macrostep(snapshot, event, actorScope, [])
.snapshot as typeof snapshot;
}

/**
Expand Down Expand Up @@ -328,7 +332,7 @@ export class StateMachine<
TConfig
>
> {
return macrostep(snapshot, event, actorScope).microstates;
return macrostep(snapshot, event, actorScope, []).microstates;
}

public getTransitionData(
Expand Down Expand Up @@ -385,8 +389,9 @@ export class StateMachine<
preInitial,
initEvent,
actorScope,
[assign(assignment)],
internalQueue
[assign(assignment) as unknown as UnknownActionObject],
internalQueue,
undefined
) as SnapshotFrom<this>;
}

Expand Down Expand Up @@ -629,4 +634,8 @@ export class StateMachine<

return restoredSnapshot;
}

public executeAction(action: ExecutableActionObject, actor?: AnyActorRef) {
return executeAction(action, actor);
}
}
77 changes: 46 additions & 31 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NULL_EVENT, STATE_DELIMITER } from './constants.ts';
import { evaluateGuard } from './guards.ts';
import { memo } from './memo.ts';
import {
convertAction,
formatInitialTransition,
formatTransition,
formatTransitions,
Expand All @@ -30,7 +31,8 @@ import type {
AnyStateNodeConfig,
ProvidedActor,
NonReducibleUnknown,
EventDescriptor
EventDescriptor,
UnknownActionObject
} from './types.ts';
import {
createInvokeId,
Expand All @@ -41,21 +43,6 @@ import {

const EMPTY_OBJECT = {};

const toSerializableAction = (action: UnknownAction) => {
if (typeof action === 'string') {
return { type: action };
}
if (typeof action === 'function') {
if ('resolve' in action) {
return { type: (action as any).type };
}
return {
type: action.name
};
}
return action;
};

interface StateNodeOptions<
TContext extends MachineContext,
TEvent extends EventObject
Expand Down Expand Up @@ -98,9 +85,9 @@ export class StateNode<
*/
public history: false | 'shallow' | 'deep';
/** The action(s) to be executed upon entering the state node. */
public entry: UnknownAction[];
public entry: UnknownActionObject[];
/** The action(s) to be executed upon exiting the state node. */
public exit: UnknownAction[];
public exit: UnknownActionObject[];
/** The parent state node. */
public parent?: StateNode<TContext, TEvent>;
/** The root machine node. */
Expand Down Expand Up @@ -209,8 +196,32 @@ export class StateNode<
this.history =
this.config.history === true ? 'shallow' : this.config.history || false;

this.entry = toArray(this.config.entry).slice();
this.exit = toArray(this.config.exit).slice();
this.entry = toArray(this.config.entry as UnknownAction).map(
(action, actionIndex) => {
const actionObject = convertAction(
action,
this,
undefined,
'entry',
0,
actionIndex
);
return actionObject;
}
);
this.exit = toArray(this.config.exit as UnknownAction).map(
(action, actionIndex) => {
const actionObject = convertAction(
action,
this,
undefined,
'exit',
0,
actionIndex
);
return actionObject;
}
);

this.meta = this.config.meta;
this.output =
Expand All @@ -222,8 +233,8 @@ export class StateNode<
public _initialize() {
this.transitions = formatTransitions(this);
if (this.config.always) {
this.always = toTransitionConfigArray(this.config.always).map((t) =>
formatTransition(this, NULL_EVENT, t)
this.always = toTransitionConfigArray(this.config.always).map((t, i) =>
formatTransition(this, NULL_EVENT, t, i)
);
}

Expand All @@ -243,13 +254,13 @@ export class StateNode<
? {
target: this.initial.target,
source: this,
actions: this.initial.actions.map(toSerializableAction),
actions: this.initial.actions,
eventType: null as any,
reenter: false,
toJSON: () => ({
target: this.initial.target.map((t) => `#${t.id}`),
source: `#${this.id}`,
actions: this.initial.actions.map(toSerializableAction),
actions: this.initial.actions,
eventType: null as any
})
}
Expand All @@ -261,10 +272,10 @@ export class StateNode<
on: this.on,
transitions: [...this.transitions.values()].flat().map((t) => ({
...t,
actions: t.actions.map(toSerializableAction)
actions: t.actions
})),
entry: this.entry.map(toSerializableAction),
exit: this.exit.map(toSerializableAction),
entry: this.entry,
exit: this.exit,
meta: this.meta,
order: this.order || -1,
output: this.output,
Expand Down Expand Up @@ -296,21 +307,25 @@ export class StateNode<
toArray(this.config.invoke).map((invokeConfig, i) => {
const { src, systemId } = invokeConfig;
const resolvedId = invokeConfig.id ?? createInvokeId(this.id, i);
const resolvedSrc =
const sourceName =
typeof src === 'string'
? src
: `xstate.invoke.${createInvokeId(this.id, i)}`;
if (typeof src !== 'string') {
this.machine.implementations.actors[sourceName] = src;
}
Comment on lines +314 to +316
Copy link
Member

@Andarist Andarist Oct 9, 2024

Choose a reason for hiding this comment

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

This leaks in cases like this:

const sharedActors = {};

setup({ actors: sharedActors }).createMachine({
  invoke: {
    src: fromPromise(async () => ""),
  },
});

setup({ actors: sharedActors }).createMachine({
  invoke: {
    src: fromPromise(async () => 100),
  },
});

We should add a test case for this


return {
...invokeConfig,
src: resolvedSrc,
src: sourceName,
id: resolvedId,
systemId: systemId,
toJSON() {
const { onDone, onError, ...invokeDefValues } = invokeConfig;
return {
...invokeDefValues,
type: 'xstate.invoke',
src: resolvedSrc,
src: sourceName,
id: resolvedId
};
}
Expand Down Expand Up @@ -375,7 +390,7 @@ export class StateNode<
event: TEvent
): TransitionDefinition<TContext, TEvent>[] | undefined {
const eventType = event.type;
const actions: UnknownAction[] = [];
const actions: UnknownActionObject[] = [];

let selectedTransition: TransitionDefinition<TContext, TEvent> | undefined;

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/actions/assign.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import isDevelopment from '#is-development';
import { cloneMachineSnapshot } from '../State.ts';
import { executingCustomAction } from '../createActor.ts';
import { Spawner, createSpawner } from '../spawn.ts';
import { executingCustomAction } from '../stateUtils.ts';
import type {
ActionArgs,
AnyActorScope,
Expand Down Expand Up @@ -179,5 +179,9 @@ export function assign<

assign.resolve = resolveAssign;

assign.toJSON = () => ({
...assign
});
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved

return assign;
}
8 changes: 5 additions & 3 deletions packages/core/src/actions/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ function resolveCancel(
}

function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) {
actorScope.defer(() => {
actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId);
});
actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId);
Copy link
Member

Choose a reason for hiding this comment

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

do we have some new test that shows this is a desired change?

Copy link
Member Author

Choose a reason for hiding this comment

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

There is no reason to defer it - it's a clean-up

}

export interface CancelAction<
Expand Down Expand Up @@ -102,5 +100,9 @@ export function cancel<
cancel.resolve = resolveCancel;
cancel.execute = executeCancel;

cancel.toJSON = () => ({
...cancel
});

return cancel;
}
6 changes: 5 additions & 1 deletion packages/core/src/actions/emit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import isDevelopment from '#is-development';
import { executingCustomAction } from '../stateUtils.ts';
import { executingCustomAction } from '../createActor.ts';
import {
ActionArgs,
ActionFunction,
Expand Down Expand Up @@ -146,5 +146,9 @@ export function emit<
emit.resolve = resolveEmit;
emit.execute = executeEmit;

emit.toJSON = () => ({
...emit
});

return emit;
}
16 changes: 15 additions & 1 deletion packages/core/src/actions/enqueueActions.ts
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import isDevelopment from '#is-development';
import { Guard, evaluateGuard } from '../guards.ts';
import { convertAction } from '../stateUtils.ts';
import {
Action,
ActionArgs,
Expand Down Expand Up @@ -135,7 +136,16 @@ function resolveEnqueueActions(
const enqueue: Parameters<typeof collect>[0]['enqueue'] = function enqueue(
action
) {
actions.push(action);
actions.push(
convertAction(
action as any,
snapshot.machine.root,
'enqueue' + Math.random(), // TODO: this should come from state node ID which isn't provided
Copy link
Member

Choose a reason for hiding this comment

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

how hard would it be to fix it here?

undefined,
0,
actions.length
)
);
};
enqueue.assign = (...args) => {
actions.push(assign(...args));
Expand Down Expand Up @@ -323,5 +333,9 @@ export function enqueueActions<
enqueueActions.collect = collect;
enqueueActions.resolve = resolveEnqueueActions;

enqueueActions.toJSON = () => ({
...enqueueActions
});

return enqueueActions;
}
Loading