Skip to content

Commit 8c4b706

Browse files
[core] Transition function (#4954)
* Use defaultActionExecutor (temp) * Convert entry/exit events * UnknownAction -> UnknownActionObject * Use defaultActionExecutor * Cleanup * WIP * Fixing tests * Add toJSON to built-in actions * Add docs * Update packages/core/src/actions/spawnChild.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Update packages/core/src/stateUtils.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Unify convertAction * Update packages/core/src/stateUtils.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Remove TODO * Introduce `executeAction`, remove `action.execute()` * Enqueue actions test * Fix type error * Expose `executeAction(…)` * Provide actor to action * Fix type issue * Changeset * Deprecate getNextSnapshot and getInitialSnapshot * Update packages/core/src/stateUtils.ts Co-authored-by: Mateusz Burzyński <[email protected]> * Restore getNextSnapshot.test.ts file * function -> exec * Delayed raise action test * Getting close * Update scheduler to handle delayed sendTo actions without an initially resolved target * Fix types * WIP * Remove test code * Default actor * Serialization in test * Revert invoke.test.ts * Cancel action execution * WIP * Update launch.json and jest.config.js * Proof of concept for invoked actions * Include resolved input & systemId in spawnChild action * Refactor action types to use ExecutableActionObject and add startedAt timestamp to raise and send actions * Add ExecutableActionsFrom * Clean up types * Lint * Lint for real * Lint lint * Back to any * Update packages/core/test/transition.test.ts Co-authored-by: Mateusz Burzyński <[email protected]> * use sleep * remove outdated comment * add `ExecutableSendToAction` to `SpecialExecutableAction` * remove `startedAt` * add failing raise test case * tweak test title * add extra cancel tests * add failing test for invalid event delivery * Revert test (happens in main) * Remove invalid test: The <cancel> element is used to cancel a delayed <send> event. * Fixed tests * remove unused import * add warn assertions * Revert "Remove invalid test: The <cancel> element is used to cancel a delayed <send> event." This reverts commit ab4bad0. * make it green, make it green * add action resolution capabilities to machine.executeAction * share `resolvedInfo` between branches * bring back `ExecutableActionObject['exec']` * add a failing boilerplate for `cancel` execution * Fix cancel action * Add SpecialActionResolution type * Add test for sendTo action * actorId -> targetId * Rename * Add tests for emit and log * Remove switch statement in executeAction * Undo * Remove executeAction for now * bring back one `toSerializableAction` call * fix one type issue * tweak test titles * remove redundant test * dont export `getAction` --------- Co-authored-by: Mateusz Burzyński <[email protected]>
1 parent d67b71d commit 8c4b706

34 files changed

+1501
-315
lines changed

.changeset/lemon-needles-play.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'xstate': minor
3+
---
4+
5+
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:
6+
7+
```ts
8+
import { transition } from 'xstate';
9+
10+
const [nextState, actions] = transition(actorLogic, currentState, event);
11+
// Execute actions as needed
12+
```
13+
14+
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:
15+
16+
```ts
17+
import { initialTransition } from 'xstate';
18+
19+
const [initialState, actions] = initialTransition(actorLogic, input);
20+
// Execute actions as needed
21+
```
22+
23+
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.

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"console": "integratedTerminal",
1212
"internalConsoleOptions": "neverOpen",
1313
"windows": {
14-
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
14+
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js"
1515
}
1616
},
1717
{
@@ -30,7 +30,7 @@
3030
"console": "integratedTerminal",
3131
"internalConsoleOptions": "neverOpen",
3232
"windows": {
33-
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
33+
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js"
3434
}
3535
}
3636
]

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,16 @@ Read [📽 the slides](http://slides.com/davidkhourshid/finite-state-machines) (
181181

182182
## Packages
183183

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

195195
## Finite State Machines
196196

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { constants } = require('jest-config');
33
/** @type {import('@jest/types').Config.InitialOptions} */
44
module.exports = {
55
prettierPath: null,
6-
setupFilesAfterEnv: ['@xstate-repo/jest-utils/setup'],
6+
setupFilesAfterEnv: ['<rootDir>/scripts/jest-utils/setup'],
77
transform: {
88
[constants.DEFAULT_JS_PATTERN]: 'babel-jest',
99
'^.+\\.vue$': '@vue/vue3-jest',

packages/core/src/StateMachine.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ export class StateMachine<
293293
TMeta,
294294
TConfig
295295
> {
296-
return macrostep(snapshot, event, actorScope).snapshot as typeof snapshot;
296+
return macrostep(snapshot, event, actorScope, [])
297+
.snapshot as typeof snapshot;
297298
}
298299

299300
/**
@@ -328,7 +329,7 @@ export class StateMachine<
328329
TConfig
329330
>
330331
> {
331-
return macrostep(snapshot, event, actorScope).microstates;
332+
return macrostep(snapshot, event, actorScope, []).microstates;
332333
}
333334

334335
public getTransitionData(
@@ -386,7 +387,8 @@ export class StateMachine<
386387
initEvent,
387388
actorScope,
388389
[assign(assignment)],
389-
internalQueue
390+
internalQueue,
391+
undefined
390392
) as SnapshotFrom<this>;
391393
}
392394

packages/core/src/StateNode.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NULL_EVENT, STATE_DELIMITER } from './constants.ts';
44
import { evaluateGuard } from './guards.ts';
55
import { memo } from './memo.ts';
66
import {
7+
BuiltinAction,
78
formatInitialTransition,
89
formatTransition,
910
formatTransitions,
@@ -47,7 +48,7 @@ const toSerializableAction = (action: UnknownAction) => {
4748
}
4849
if (typeof action === 'function') {
4950
if ('resolve' in action) {
50-
return { type: (action as any).type };
51+
return { type: (action as BuiltinAction).type };
5152
}
5253
return {
5354
type: action.name
@@ -296,21 +297,22 @@ export class StateNode<
296297
toArray(this.config.invoke).map((invokeConfig, i) => {
297298
const { src, systemId } = invokeConfig;
298299
const resolvedId = invokeConfig.id ?? createInvokeId(this.id, i);
299-
const resolvedSrc =
300+
const sourceName =
300301
typeof src === 'string'
301302
? src
302303
: `xstate.invoke.${createInvokeId(this.id, i)}`;
304+
303305
return {
304306
...invokeConfig,
305-
src: resolvedSrc,
307+
src: sourceName,
306308
id: resolvedId,
307309
systemId: systemId,
308310
toJSON() {
309311
const { onDone, onError, ...invokeDefValues } = invokeConfig;
310312
return {
311313
...invokeDefValues,
312314
type: 'xstate.invoke',
313-
src: resolvedSrc,
315+
src: sourceName,
314316
id: resolvedId
315317
};
316318
}

packages/core/src/actions/assign.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import isDevelopment from '#is-development';
22
import { cloneMachineSnapshot } from '../State.ts';
3+
import { executingCustomAction } from '../createActor.ts';
34
import { Spawner, createSpawner } from '../spawn.ts';
4-
import { executingCustomAction } from '../stateUtils.ts';
55
import type {
66
ActionArgs,
77
AnyActorScope,
@@ -15,7 +15,8 @@ import type {
1515
ParameterizedObject,
1616
PropertyAssigner,
1717
ProvidedActor,
18-
ActionFunction
18+
ActionFunction,
19+
BuiltinActionResolution
1920
} from '../types.ts';
2021

2122
export interface AssignArgs<
@@ -39,7 +40,7 @@ function resolveAssign(
3940
| Assigner<any, any, any, any, any>
4041
| PropertyAssigner<any, any, any, any, any>;
4142
}
42-
) {
43+
): BuiltinActionResolution {
4344
if (!snapshot.context) {
4445
throw new Error(
4546
'Cannot assign to undefined `context`. Ensure that `context` is defined in the machine config.'
@@ -83,7 +84,9 @@ function resolveAssign(
8384
...spawnedChildren
8485
}
8586
: snapshot.children
86-
})
87+
}),
88+
undefined,
89+
undefined
8790
];
8891
}
8992

packages/core/src/actions/cancel.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
EventObject,
66
MachineContext,
77
ActionArgs,
8-
ParameterizedObject
8+
ParameterizedObject,
9+
BuiltinActionResolution
910
} from '../types.ts';
1011

1112
type ResolvableSendId<
@@ -26,15 +27,15 @@ function resolveCancel(
2627
actionArgs: ActionArgs<any, any, any>,
2728
actionParams: ParameterizedObject['params'] | undefined,
2829
{ sendId }: { sendId: ResolvableSendId<any, any, any, any> }
29-
) {
30+
): BuiltinActionResolution {
3031
const resolvedSendId =
3132
typeof sendId === 'function' ? sendId(actionArgs, actionParams) : sendId;
32-
return [snapshot, resolvedSendId];
33+
return [snapshot, { sendId: resolvedSendId }, undefined];
3334
}
3435

35-
function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) {
36+
function executeCancel(actorScope: AnyActorScope, params: { sendId: string }) {
3637
actorScope.defer(() => {
37-
actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId);
38+
actorScope.system.scheduler.cancel(actorScope.self, params.sendId);
3839
});
3940
}
4041

packages/core/src/actions/emit.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import isDevelopment from '#is-development';
2-
import { executingCustomAction } from '../stateUtils.ts';
2+
import { executingCustomAction } from '../createActor.ts';
33
import {
44
ActionArgs,
55
ActionFunction,
@@ -10,7 +10,8 @@ import {
1010
EventObject,
1111
MachineContext,
1212
ParameterizedObject,
13-
SendExpr
13+
SendExpr,
14+
BuiltinActionResolution
1415
} from '../types.ts';
1516

1617
function resolveEmit(
@@ -31,12 +32,12 @@ function resolveEmit(
3132
EventObject
3233
>;
3334
}
34-
) {
35+
): BuiltinActionResolution {
3536
const resolvedEvent =
3637
typeof eventOrExpr === 'function'
3738
? eventOrExpr(args, actionParams)
3839
: eventOrExpr;
39-
return [snapshot, { event: resolvedEvent }];
40+
return [snapshot, { event: resolvedEvent }, undefined];
4041
}
4142

4243
function executeEmit(

packages/core/src/actions/enqueueActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
MachineContext,
1313
ParameterizedObject,
1414
ProvidedActor,
15+
BuiltinActionResolution,
1516
UnifiedArg
1617
} from '../types.ts';
1718
import { assign } from './assign.ts';
@@ -130,7 +131,7 @@ function resolveEnqueueActions(
130131
EventObject
131132
>;
132133
}
133-
) {
134+
): BuiltinActionResolution {
134135
const actions: any[] = [];
135136
const enqueue: Parameters<typeof collect>[0]['enqueue'] = function enqueue(
136137
action

0 commit comments

Comments
 (0)