From 62ab91dec8aff806b087be435b80dd0458b3f104 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 13 Sep 2025 00:45:54 -0400 Subject: [PATCH 1/2] WIP extend --- packages/core/src/setup.ts | 245 +++++++++++++++++++++++++++---------- 1 file changed, 177 insertions(+), 68 deletions(-) diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index 1af397f266..019cadcfc9 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -1,4 +1,5 @@ import { StateMachine } from './StateMachine'; +import { enqueueActions } from './actions'; import { createMachine } from './createMachine'; import { GuardPredicate } from './guards'; @@ -70,82 +71,67 @@ type ToProvidedActor< type RequiredSetupKeys = IsNever extends true ? never : 'actors'; -export function setup< +type SetupReturn< TContext extends MachineContext, - TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here - TActors extends Record = {}, - TChildrenMap extends Record = {}, - TActions extends Record< - string, - ParameterizedObject['params'] | undefined - > = {}, - TGuards extends Record< - string, - ParameterizedObject['params'] | undefined - > = {}, - TDelay extends string = never, - TTag extends string = string, - TInput = NonReducibleUnknown, - TOutput extends NonReducibleUnknown = NonReducibleUnknown, - TEmitted extends EventObject = EventObject, - TMeta extends MetaObject = MetaObject ->({ - schemas, - actors, - actions, - guards, - delays -}: { - schemas?: unknown; - types?: SetupTypes< + TEvent extends AnyEventObject, + TActors extends Record, + TChildrenMap extends Record, + TActions extends Record, + TGuards extends Record, + TDelay extends string, + TTag extends string, + TInput, + TOutput extends NonReducibleUnknown, + TEmitted extends EventObject, + TMeta extends MetaObject +> = { + extend: < + TExtendActions extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TExtendGuards extends Record< + string, + ParameterizedObject['params'] | undefined + > = {} + >({ + actions + }: { + actions?: { + [K in keyof TExtendActions]: ActionFunction< + TContext, + TEvent, + TEvent, + TExtendActions[K], + ToProvidedActor, + ToParameterizedObject, + ToParameterizedObject, + TDelay, + TEmitted + >; + }; + guards?: { + [K in keyof TExtendGuards]: GuardPredicate< + TContext, + TEvent, + TExtendGuards[K], + ToParameterizedObject + >; + }; + }) => SetupReturn< TContext, TEvent, + TActors, TChildrenMap, + TActions & TExtendActions, + TGuards & TExtendGuards, + TDelay, TTag, TInput, TOutput, TEmitted, TMeta >; - actors?: { - // union here enforces that all configured children have to be provided in actors - // it makes those values required here - [K in keyof TActors | Values]: K extends keyof TActors - ? TActors[K] - : never; - }; - actions?: { - [K in keyof TActions]: ActionFunction< - TContext, - TEvent, - TEvent, - TActions[K], - ToProvidedActor, - ToParameterizedObject, - ToParameterizedObject, - TDelay, - TEmitted - >; - }; - guards?: { - [K in keyof TGuards]: GuardPredicate< - TContext, - TEvent, - TGuards[K], - ToParameterizedObject - >; - }; - delays?: { - [K in TDelay]: DelayConfig< - TContext, - TEvent, - ToParameterizedObject['params'], - TEvent - >; - }; -} & { - [K in RequiredSetupKeys]: unknown; -}): { /** * Creates a state config that is strongly typed. This state config can be * used to create a machine. @@ -226,7 +212,97 @@ export function setup< TMeta, TConfig >; -} { +}; + +export function setup< + TContext extends MachineContext, + TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here + TActors extends Record = {}, + TChildrenMap extends Record = {}, + TActions extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TGuards extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TDelay extends string = never, + TTag extends string = string, + TInput = NonReducibleUnknown, + TOutput extends NonReducibleUnknown = NonReducibleUnknown, + TEmitted extends EventObject = EventObject, + TMeta extends MetaObject = MetaObject +>({ + schemas, + actors, + actions, + guards, + delays +}: { + schemas?: unknown; + types?: SetupTypes< + TContext, + TEvent, + TChildrenMap, + TTag, + TInput, + TOutput, + TEmitted, + TMeta + >; + actors?: { + // union here enforces that all configured children have to be provided in actors + // it makes those values required here + [K in keyof TActors | Values]: K extends keyof TActors + ? TActors[K] + : never; + }; + actions?: { + [K in keyof TActions]: ActionFunction< + TContext, + TEvent, + TEvent, + TActions[K], + ToProvidedActor, + ToParameterizedObject, + ToParameterizedObject, + TDelay, + TEmitted + >; + }; + guards?: { + [K in keyof TGuards]: GuardPredicate< + TContext, + TEvent, + TGuards[K], + ToParameterizedObject + >; + }; + delays?: { + [K in TDelay]: DelayConfig< + TContext, + TEvent, + ToParameterizedObject['params'], + TEvent + >; + }; +} & { + [K in RequiredSetupKeys]: unknown; +}): SetupReturn< + TContext, + TEvent, + TActors, + TChildrenMap, + TActions, + TGuards, + TDelay, + TTag, + TInput, + TOutput, + TEmitted, + TMeta +> { return { createStateConfig: (config) => config, createMachine: (config) => @@ -238,6 +314,39 @@ export function setup< guards, delays } - ) + ), + extend: (extended) => + setup({ + schemas, + actors, + actions: { ...actions, ...extended.actions }, + guards: { ...guards, ...extended.guards }, + delays + } as any) }; } + +const s = setup({ + actions: { + doSomething: () => {}, + other: () => {} + } +}); + +s.extend({ + actions: { + foo: enqueueActions((x) => { + x.enqueue({ + type: 'doSomething' + }); + }) + } +}).extend({ + actions: { + bar: enqueueActions((x) => { + x.enqueue({ + type: 'foo' + }); + }) + } +}); From c1653887c0cdd15daf9607469dc9c16727cd67e7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 17 Sep 2025 23:06:57 -0400 Subject: [PATCH 2/2] Add delays + tests --- packages/core/src/setup.ts | 19 +++- packages/core/test/setup.types.test.ts | 128 +++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index 019cadcfc9..aa7a8c5d9b 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -93,9 +93,12 @@ type SetupReturn< TExtendGuards extends Record< string, ParameterizedObject['params'] | undefined - > = {} + > = {}, + TExtendDelays extends string = never >({ - actions + actions, + guards, + delays }: { actions?: { [K in keyof TExtendActions]: ActionFunction< @@ -118,6 +121,14 @@ type SetupReturn< ToParameterizedObject >; }; + delays?: { + [K in TExtendDelays]: DelayConfig< + TContext, + TEvent, + ToParameterizedObject['params'], + TEvent + >; + }; }) => SetupReturn< TContext, TEvent, @@ -125,7 +136,7 @@ type SetupReturn< TChildrenMap, TActions & TExtendActions, TGuards & TExtendGuards, - TDelay, + TDelay | TExtendDelays, TTag, TInput, TOutput, @@ -321,7 +332,7 @@ export function setup< actors, actions: { ...actions, ...extended.actions }, guards: { ...guards, ...extended.guards }, - delays + delays: { ...delays, ...extended.delays } } as any) }; } diff --git a/packages/core/test/setup.types.test.ts b/packages/core/test/setup.types.test.ts index d7ae068b51..cfaf83f1a7 100644 --- a/packages/core/test/setup.types.test.ts +++ b/packages/core/test/setup.types.test.ts @@ -13,6 +13,7 @@ import { fromTransition, log, not, + or, raise, sendParent, sendTo, @@ -2648,3 +2649,130 @@ describe('createStateConfig', () => { }); }); }); + +describe('extend', () => { + it('should allow extending actions and referencing previous ones via enqueueActions across chained extends', () => { + setup({ + actions: { + doSomething: () => {} + } + }) + .extend({ + actions: { + foo: enqueueActions(({ enqueue }) => { + enqueue('doSomething'); + + // @ts-expect-error + enqueue('nonexistent'); + }) + } + }) + .extend({ + actions: { + bar: enqueueActions(({ enqueue }) => { + enqueue('foo'); + + enqueue({ + type: 'doSomething' + }); + + // @ts-expect-error + enqueue('nonexistent'); + }) + } + }) + .createMachine({ + entry: 'bar' + }); + }); + + it('should allow extending guards and referencing previous ones with not/and/or across chained extends', () => { + setup({ + guards: { + truthy: () => true + } + }) + .extend({ + guards: { + alsoTruthy: () => true, + notTruthy: not('truthy'), + // @ts-expect-error + nonexistent: not('existent') + } + }) + .extend({ + guards: { + combined: and(['truthy', 'alsoTruthy']), + alt: or(['notTruthy', 'truthy']), + // @ts-expect-error + nonexistent: or(['existent', 'truthy']) + } + }) + .createMachine({ + on: { + EV: [ + { guard: 'combined', actions: () => {} }, + { guard: 'alt', actions: () => {} }, + { + // @ts-expect-error + guard: 'fake', + actions: () => {} + } + ] + } + }); + }); + + it('should allow extending delays and referencing base delays in actions and transitions', () => { + setup({ + delays: { + short: 10 + } + }) + .extend({ + delays: { + medium: 100 + } + }) + .extend({ + delays: { + long: 1000 + } + }) + .createMachine({ + initial: 'a', + states: { + a: { + entry: [ + raise({ type: 'GO' }, { delay: 'short' }), + raise({ type: 'GO' }, { delay: 'medium' }), + raise({ type: 'GO' }, { delay: 'long' }), + raise( + { type: 'GO' }, + { + // @ts-expect-error + delay: 'nonexistent' + } + ) + ], + on: { + GO: 'b' + } + }, + b: { + after: { + medium: 'c' + } + }, + c: { + after: { + long: 'd', + // @ts-expect-error + nonexistent: 'd' + } + }, + d: {} + } + }); + }); +});