Skip to content

Commit

Permalink
[core] Transition function (#4954)
Browse files Browse the repository at this point in the history
* 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 <mateuszburzynski@gmail.com>

* Update packages/core/src/stateUtils.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Unify convertAction

* Update packages/core/src/stateUtils.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* 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 <mateuszburzynski@gmail.com>

* 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 <mateuszburzynski@gmail.com>

* 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 <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist authored Nov 12, 2024
1 parent d67b71d commit 8c4b706
Show file tree
Hide file tree
Showing 34 changed files with 1,501 additions and 315 deletions.
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
8 changes: 5 additions & 3 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,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 +329,7 @@ export class StateMachine<
TConfig
>
> {
return macrostep(snapshot, event, actorScope).microstates;
return macrostep(snapshot, event, actorScope, []).microstates;
}

public getTransitionData(
Expand Down Expand Up @@ -386,7 +387,8 @@ export class StateMachine<
initEvent,
actorScope,
[assign(assignment)],
internalQueue
internalQueue,
undefined
) as SnapshotFrom<this>;
}

Expand Down
10 changes: 6 additions & 4 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 {
BuiltinAction,
formatInitialTransition,
formatTransition,
formatTransitions,
Expand Down Expand Up @@ -47,7 +48,7 @@ const toSerializableAction = (action: UnknownAction) => {
}
if (typeof action === 'function') {
if ('resolve' in action) {
return { type: (action as any).type };
return { type: (action as BuiltinAction).type };
}
return {
type: action.name
Expand Down Expand Up @@ -296,21 +297,22 @@ 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)}`;

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
11 changes: 7 additions & 4 deletions 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 All @@ -15,7 +15,8 @@ import type {
ParameterizedObject,
PropertyAssigner,
ProvidedActor,
ActionFunction
ActionFunction,
BuiltinActionResolution
} from '../types.ts';

export interface AssignArgs<
Expand All @@ -39,7 +40,7 @@ function resolveAssign(
| Assigner<any, any, any, any, any>
| PropertyAssigner<any, any, any, any, any>;
}
) {
): BuiltinActionResolution {
if (!snapshot.context) {
throw new Error(
'Cannot assign to undefined `context`. Ensure that `context` is defined in the machine config.'
Expand Down Expand Up @@ -83,7 +84,9 @@ function resolveAssign(
...spawnedChildren
}
: snapshot.children
})
}),
undefined,
undefined
];
}

Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/actions/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
EventObject,
MachineContext,
ActionArgs,
ParameterizedObject
ParameterizedObject,
BuiltinActionResolution
} from '../types.ts';

type ResolvableSendId<
Expand All @@ -26,15 +27,15 @@ function resolveCancel(
actionArgs: ActionArgs<any, any, any>,
actionParams: ParameterizedObject['params'] | undefined,
{ sendId }: { sendId: ResolvableSendId<any, any, any, any> }
) {
): BuiltinActionResolution {
const resolvedSendId =
typeof sendId === 'function' ? sendId(actionArgs, actionParams) : sendId;
return [snapshot, resolvedSendId];
return [snapshot, { sendId: resolvedSendId }, undefined];
}

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

Expand Down
9 changes: 5 additions & 4 deletions 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 All @@ -10,7 +10,8 @@ import {
EventObject,
MachineContext,
ParameterizedObject,
SendExpr
SendExpr,
BuiltinActionResolution
} from '../types.ts';

function resolveEmit(
Expand All @@ -31,12 +32,12 @@ function resolveEmit(
EventObject
>;
}
) {
): BuiltinActionResolution {
const resolvedEvent =
typeof eventOrExpr === 'function'
? eventOrExpr(args, actionParams)
: eventOrExpr;
return [snapshot, { event: resolvedEvent }];
return [snapshot, { event: resolvedEvent }, undefined];
}

function executeEmit(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/actions/enqueueActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MachineContext,
ParameterizedObject,
ProvidedActor,
BuiltinActionResolution,
UnifiedArg
} from '../types.ts';
import { assign } from './assign.ts';
Expand Down Expand Up @@ -130,7 +131,7 @@ function resolveEnqueueActions(
EventObject
>;
}
) {
): BuiltinActionResolution {
const actions: any[] = [];
const enqueue: Parameters<typeof collect>[0]['enqueue'] = function enqueue(
action
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/actions/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
EventObject,
LogExpr,
MachineContext,
ParameterizedObject
ParameterizedObject,
BuiltinActionResolution
} from '../types.ts';

type ResolvableLogValue<
Expand All @@ -28,14 +29,15 @@ function resolveLog(
value: ResolvableLogValue<any, any, any, any>;
label: string | undefined;
}
) {
): BuiltinActionResolution {
return [
snapshot,
{
value:
typeof value === 'function' ? value(actionArgs, actionParams) : value,
label
}
},
undefined
];
}

Expand Down
27 changes: 23 additions & 4 deletions packages/core/src/actions/raise.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 All @@ -9,10 +9,12 @@ import {
DelayExpr,
DoNotInfer,
EventObject,
ExecutableActionObject,
MachineContext,
ParameterizedObject,
RaiseActionOptions,
SendExpr
SendExpr,
BuiltinActionResolution
} from '../types.ts';

function resolveRaise(
Expand Down Expand Up @@ -47,7 +49,7 @@ function resolveRaise(
| undefined;
},
{ internalQueue }: { internalQueue: AnyEventObject[] }
) {
): BuiltinActionResolution {
const delaysMap = snapshot.machine.implementations.delays;

if (typeof eventOrExpr === 'string') {
Expand Down Expand Up @@ -75,7 +77,15 @@ function resolveRaise(
if (typeof resolvedDelay !== 'number') {
internalQueue.push(resolvedEvent);
}
return [snapshot, { event: resolvedEvent, id, delay: resolvedDelay }];
return [
snapshot,
{
event: resolvedEvent,
id,
delay: resolvedDelay
},
undefined
];
}

function executeRaise(
Expand Down Expand Up @@ -168,3 +178,12 @@ export function raise<

return raise;
}

export interface ExecutableRaiseAction extends ExecutableActionObject {
type: 'xstate.raise';
params: {
event: EventObject;
id: string | undefined;
delay: number | undefined;
};
}
Loading

0 comments on commit 8c4b706

Please sign in to comment.