Skip to content

(WIP) v6 #5257

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

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d99bffe
Support transition functions WIP
davidkpiano Mar 13, 2025
1c1de5e
Enqueue actions WIP
davidkpiano Mar 13, 2025
5fb99ab
Guards
davidkpiano Mar 15, 2025
b4631d3
Move enqueue obj
davidkpiano Mar 22, 2025
5224ffd
More tests
davidkpiano Mar 25, 2025
98ac2d5
Add children to action arg, More tests
davidkpiano Mar 28, 2025
06cd686
Merge branch 'main' into v6
davidkpiano Mar 28, 2025
8f0d641
Add evaluateCandidate, next set of tests
davidkpiano Mar 30, 2025
6fdd543
Add guards
davidkpiano Mar 30, 2025
fa0cc07
Add more v6 tests
davidkpiano Apr 8, 2025
5492fb5
Move id counter to be per-system, add children to transition function
davidkpiano Apr 10, 2025
050fa9b
Reenter
davidkpiano Apr 12, 2025
8611632
WIP
davidkpiano Apr 12, 2025
30086a1
Add some missing impls
davidkpiano Apr 14, 2025
b9d13d4
Add test
davidkpiano Apr 15, 2025
b572c3d
Add invoke v6 tests
davidkpiano Apr 16, 2025
f545e7c
Add invoke v6 tests
davidkpiano Apr 16, 2025
c5345a9
Update snapshot
davidkpiano Apr 16, 2025
d7c7f9e
More tests
davidkpiano Apr 17, 2025
9572d90
More tests
davidkpiano Apr 21, 2025
2ec7f75
Rehydration tests WIP
davidkpiano Apr 21, 2025
d3c40cf
Support for emitted events as actions and enq.action(fn, ...args)
davidkpiano Apr 27, 2025
a18159d
Make sure to return new snapshot for transient transitions that updat…
davidkpiano Apr 27, 2025
4218bf7
Add more test files
davidkpiano Apr 27, 2025
4ae4f82
Add state.v6.test.ts
davidkpiano May 11, 2025
5b64c4f
Merge branch 'main' into v6
davidkpiano May 17, 2025
29088dd
Refactor state machine transition functions to use concise syntax
davidkpiano May 26, 2025
d4744da
Refactor cont'd
davidkpiano May 26, 2025
62ff9ed
Fix test
davidkpiano May 26, 2025
288d4f6
WIP
davidkpiano Jun 6, 2025
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
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@
"ajv": "^8.12.0",
"pkg-up": "^3.1.0",
"rxjs": "^7.8.1",
"xml-js": "^1.6.11"
"xml-js": "^1.6.11",
"zod": "^3.25.51"
},
"preconstruct": {
"umdName": "XState",
Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import isDevelopment from '#is-development';
import { $$ACTOR_TYPE } from './createActor.ts';
import type { StateNode } from './StateNode.ts';
import type { StateMachine } from './StateMachine.ts';
import { getStateValue } from './stateUtils.ts';
import {
getStateValue,
getTransitionResult,
getTransitionActions
} from './stateUtils.ts';
import type {
ProvidedActor,
AnyMachineSnapshot,
Expand Down Expand Up @@ -317,7 +321,15 @@ const machineSnapshotCan = function can(
return (
!!transitionData?.length &&
// Check that at least one transition is not forbidden
transitionData.some((t) => t.target !== undefined || t.actions.length)
transitionData.some((t) => {
const res = getTransitionResult(t, this, event);
return (
t.target !== undefined ||
res.targets?.length ||
res.context ||
getTransitionActions(t, this, event, { self: {} }).length
);
})
);
};

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,7 @@ export class StateMachine<
source: this.root,
reenter: true,
actions: [],
eventType: null as any,
toJSON: null as any // TODO: fix
eventType: null as any
}
],
preInitialState,
Expand Down
62 changes: 24 additions & 38 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { MachineSnapshot } from './State.ts';
import type { StateMachine } from './StateMachine.ts';
import { NULL_EVENT, STATE_DELIMITER } from './constants.ts';
import { evaluateGuard } from './guards.ts';
import { memo } from './memo.ts';
import {
BuiltinAction,
evaluateCandidate,
formatInitialTransition,
formatTransition,
formatTransitions,
Expand All @@ -31,7 +31,8 @@ import type {
AnyStateNodeConfig,
ProvidedActor,
NonReducibleUnknown,
EventDescriptor
EventDescriptor,
Action2
} from './types.ts';
import {
createInvokeId,
Expand Down Expand Up @@ -100,8 +101,10 @@ export class StateNode<
public history: false | 'shallow' | 'deep';
/** The action(s) to be executed upon entering the state node. */
public entry: UnknownAction[];
public entry2: Action2<any, any, any> | undefined;
/** The action(s) to be executed upon exiting the state node. */
public exit: UnknownAction[];
public exit2: Action2<any, any, any> | undefined;
/** The parent state node. */
public parent?: StateNode<TContext, TEvent>;
/** The root machine node. */
Expand Down Expand Up @@ -211,8 +214,17 @@ export class StateNode<
this.config.history === true ? 'shallow' : this.config.history || false;

this.entry = toArray(this.config.entry).slice();
this.entry2 = this.config.entry2;
if (this.entry2) {
// @ts-ignore
this.entry2._special = true;
}
this.exit = toArray(this.config.exit).slice();

this.exit2 = this.config.exit2;
if (this.exit2) {
// @ts-ignore
this.exit2._special = true;
}
this.meta = this.config.meta;
this.output =
this.type === 'final' || !this.parent ? this.config.output : undefined;
Expand All @@ -224,7 +236,7 @@ export class StateNode<
this.transitions = formatTransitions(this);
if (this.config.always) {
this.always = toTransitionConfigArray(this.config.always).map((t) =>
formatTransition(this, NULL_EVENT, t)
typeof t === 'function' ? t : formatTransition(this, NULL_EVENT, t)
);
}

Expand All @@ -246,13 +258,7 @@ export class StateNode<
source: this,
actions: this.initial.actions.map(toSerializableAction),
eventType: null as any,
reenter: false,
toJSON: () => ({
target: this.initial.target.map((t) => `#${t.id}`),
source: `#${this.id}`,
actions: this.initial.actions.map(toSerializableAction),
eventType: null as any
})
reenter: false
}
: undefined,
history: this.history,
Expand Down Expand Up @@ -388,35 +394,15 @@ export class StateNode<
);

for (const candidate of candidates) {
const { guard } = candidate;
const resolvedContext = snapshot.context;

let guardPassed = false;

try {
guardPassed =
!guard ||
evaluateGuard<TContext, TEvent>(
guard,
resolvedContext,
event,
snapshot
);
} catch (err: any) {
const guardType =
typeof guard === 'string'
? guard
: typeof guard === 'object'
? guard.type
: undefined;
throw new Error(
`Unable to evaluate guard ${
guardType ? `'${guardType}' ` : ''
}in transition for event '${eventType}' in state node '${
this.id
}':\n${err.message}`
);
}
const guardPassed = evaluateCandidate(
candidate,
resolvedContext,
event,
snapshot,
this
);

if (guardPassed) {
actions.push(...candidate.actions);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/actions/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ function resolveAssign(
spawnedChildren
),
self: actorScope.self,
system: actorScope.system
system: actorScope.system,
children: snapshot.children
};
let partialUpdate: Record<string, unknown> = {};
if (typeof assignment === 'function') {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/createActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@ export class Actor<TLogic extends AnyActorLogic>
const saveExecutingCustomAction = executingCustomAction;
try {
executingCustomAction = true;
action.exec(action.info, action.params);
// v6
const actionArgs = action.args.length
? [] // already bound
: [action.info, action.params];
action.exec(...actionArgs);
} finally {
executingCustomAction = saveExecutingCustomAction;
}
Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/createMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ToChildren,
MetaObject
} from './types.ts';
import { Next_MachineConfig } from './types.v6.ts';

type TestValue =
| string
Expand Down Expand Up @@ -162,3 +163,78 @@ export function createMachine<
any // TStateSchema
>(config as any, implementations as any);
}

export function next_createMachine<
TContext extends MachineContext,
TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here
TActor extends ProvidedActor,
TAction extends ParameterizedObject,
TGuard extends ParameterizedObject,
TDelay extends string,
TTag extends string,
TInput,
TOutput extends NonReducibleUnknown,
TEmitted extends EventObject,
TMeta extends MetaObject,
// it's important to have at least one default type parameter here
// it allows us to benefit from contextual type instantiation as it makes us to pass the hasInferenceCandidatesOrDefault check in the compiler
// we should be able to remove this when we start inferring TConfig, with it we'll always have an inference candidate
_ = any
>(
config: {
schemas?: unknown;
} & Next_MachineConfig<
TContext,
TEvent,
TDelay,
TTag,
TInput,
TOutput,
TEmitted,
TMeta
>,
implementations?: InternalMachineImplementations<
ResolvedStateMachineTypes<
TContext,
TEvent,
TActor,
TAction,
TGuard,
TDelay,
TTag,
TEmitted
>
>
): StateMachine<
TContext,
TEvent,
Cast<ToChildren<TActor>, Record<string, AnyActorRef | undefined>>,
TActor,
TAction,
TGuard,
TDelay,
StateValue,
TTag & string,
TInput,
TOutput,
TEmitted,
TMeta, // TMeta
TODO // TStateSchema
> {
return new StateMachine<
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any, // TEmitted
any, // TMeta
any // TStateSchema
>(config as any, implementations as any);
}
119 changes: 119 additions & 0 deletions packages/core/src/createMachine2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// @ts-nocheck
import { EventObject, MachineContext, MetaObject } from './types';

type EnqueueObj<TContext extends MachineContext, TEvent extends EventObject> = {
context: TContext;
event: TEvent;
enqueue: (fn: any) => void;
};

type StateTransition<
TContext extends MachineContext,
TEvent extends EventObject,
TStateMap extends Record<string, any>
> = (obj: EnqueueObj<TContext, TEvent>) => {
target: keyof TStateMap;
context: TContext;
};

type StateConfig<
TContext extends MachineContext,
TEvent extends EventObject,
TStateMap extends Record<string, any>
> = {
entry?: (obj: EnqueueObj<TContext, TEvent>) => void;
exit?: (obj: EnqueueObj<TContext, TEvent>) => void;
on?: {
[K in TEvent['type']]?: StateTransition<TContext, TEvent, TStateMap>;
};
after?: {
[K in string | number]: StateTransition<TContext, TEvent, TStateMap>;
};
always?: StateTransition<TContext, TEvent, TStateMap>;
meta?: MetaObject;
id?: string;
tags?: string[];
description?: string;
} & (
| {
type: 'parallel';
initial?: never;
states: States<TContext, TEvent, TStateMap>;
}
| {
type: 'final';
initial?: never;
states?: never;
}
| {
type: 'history';
history?: 'shallow' | 'deep';
default?: keyof TStateMap;
}
| {
type?: 'compound';
initial: NoInfer<keyof TStateMap>;
states: States<TContext, TEvent, TStateMap>;
}
| {
type?: 'atomic';
initial?: never;
states?: never;
}
);

type States<
TContext extends MachineContext,
TEvent extends EventObject,
TStateMap extends Record<string, any>
> = {
[K in keyof TStateMap]: StateConfig<TContext, TEvent, TStateMap>;
};

export function createMachine2<
TContext extends MachineContext,
TEvent extends EventObject,
TStateMap extends Record<string, any>
>(
config: {
context: TContext;
version?: string;
} & StateConfig<TContext, TEvent, TStateMap>
) {}

const light = createMachine2({
context: {},
initial: 'green',
states: {
green: {
on: {
timer: () => ({
target: 'yellow',
context: {}
})
}
},
yellow: {
on: {
timer: () => ({
target: 'red',
context: {}
})
}
},
red: {
on: {
timer: () => ({
target: 'green',
context: {}
})
}
},
hist: {
type: 'history',
history: 'shallow'
}
}
});

light;
Loading