From f4e0ec48cccbbe3e74de8a6a5b25eaa727512a83 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 16 Apr 2024 10:13:28 -0400 Subject: [PATCH] Allow inline actor logic (#4806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow inline actor logic * Move the ts-expect-errors to more precise locations * disallow ids for invoked inline actors when actor types are configured * tweak spawn-related types * Add changeset --------- Co-authored-by: Mateusz BurzyƄski --- .changeset/healthy-pigs-happen.md | 22 +++ packages/core/src/actions/spawnChild.ts | 42 +++-- packages/core/src/spawn.ts | 19 ++- packages/core/src/types.ts | 199 +++++++++++++++--------- packages/core/test/setup.types.test.ts | 34 +++- packages/core/test/types.test.ts | 160 +++++++++++++++---- 6 files changed, 350 insertions(+), 126 deletions(-) create mode 100644 .changeset/healthy-pigs-happen.md diff --git a/.changeset/healthy-pigs-happen.md b/.changeset/healthy-pigs-happen.md new file mode 100644 index 0000000000..b3f670d86d --- /dev/null +++ b/.changeset/healthy-pigs-happen.md @@ -0,0 +1,22 @@ +--- +'xstate': minor +--- + +Inline actor logic is now permitted when named actors are present. Defining inline actors will no longer cause a TypeScript error: + +```ts +const machine = setup({ + actors: { + existingActor: fromPromise(async () => { + // ... + }) + } +}).createMachine({ + invoke: { + src: fromPromise(async () => { + // Inline actor + }) + // ... + } +}); +``` diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index 6312e2f724..14a5ac9d2c 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -139,22 +139,32 @@ type DistributeActors< TExpressionEvent extends EventObject, TEvent extends EventObject, TActor extends ProvidedActor -> = TActor extends any - ? ConditionalRequired< - [ - src: TActor['src'], - options?: SpawnActionOptions< - TContext, - TExpressionEvent, - TEvent, - TActor - > & { - [K in RequiredActorOptions]: unknown; - } - ], - IsNotNever> - > - : never; +> = + | (TActor extends any + ? ConditionalRequired< + [ + src: TActor['src'], + options?: SpawnActionOptions< + TContext, + TExpressionEvent, + TEvent, + TActor + > & { + [K in RequiredActorOptions]: unknown; + } + ], + IsNotNever> + > + : never) + | [ + src: AnyActorLogic, + options?: SpawnActionOptions< + TContext, + TExpressionEvent, + TEvent, + ProvidedActor + > & { id?: never } + ]; type SpawnArguments< TContext extends MachineContext, diff --git a/packages/core/src/spawn.ts b/packages/core/src/spawn.ts index 6f5621a926..6533e6a34b 100644 --- a/packages/core/src/spawn.ts +++ b/packages/core/src/spawn.ts @@ -46,10 +46,21 @@ type GetConcreteLogic< export type Spawner = IsLiteralString< TActor['src'] > extends true - ? ( - logic: TSrc, - ...[options]: SpawnOptions - ) => ActorRefFrom> + ? { + ( + logic: TSrc, + ...[options]: SpawnOptions + ): ActorRefFrom>; + ( + src: TLogic, + options?: { + id?: never; + systemId?: string; + input?: InputFrom; + syncSnapshot?: boolean; + } + ): ActorRefFrom; + } : ( src: TLogic, options?: { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 24eb9d7861..65a62eb94c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -581,77 +581,134 @@ type DistributeActors< TEmitted extends EventObject, TSpecificActor extends ProvidedActor > = TSpecificActor extends { src: infer TSrc } - ? Compute< - { - systemId?: string; - /** - * The source of the machine to be invoked, or the machine itself. - */ - src: TSrc; - - /** - * The unique identifier for the invoked machine. If not specified, this - * will be the machine's own `id`, or the URL (from `src`). - */ - id?: TSpecificActor['id']; - - // TODO: currently we do not enforce required inputs here - // in a sense, we shouldn't - they could be provided within the `implementations` object - // how do we verify if the required input has been provided? - input?: - | Mapper, TEvent> - | InputFrom; - /** - * The transition to take upon the invoked child machine reaching its final top-level state. - */ - onDone?: - | string - | SingleOrArray< - TransitionConfigOrTarget< - TContext, - DoneActorEvent>, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted - > - >; - /** - * The transition to take upon the invoked child machine sending an error event. - */ - onError?: - | string - | SingleOrArray< - TransitionConfigOrTarget< - TContext, - ErrorActorEvent, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted - > - >; - - onSnapshot?: - | string - | SingleOrArray< - TransitionConfigOrTarget< - TContext, - SnapshotEvent>, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TEmitted - > - >; - } & { [K in RequiredActorOptions]: unknown } - > + ? + | Compute< + { + systemId?: string; + /** + * The source of the machine to be invoked, or the machine itself. + */ + src: TSrc; + + /** + * The unique identifier for the invoked machine. If not specified, this + * will be the machine's own `id`, or the URL (from `src`). + */ + id?: TSpecificActor['id']; + + // TODO: currently we do not enforce required inputs here + // in a sense, we shouldn't - they could be provided within the `implementations` object + // how do we verify if the required input has been provided? + input?: + | Mapper< + TContext, + TEvent, + InputFrom, + TEvent + > + | InputFrom; + /** + * The transition to take upon the invoked child machine reaching its final top-level state. + */ + onDone?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + DoneActorEvent>, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + > + >; + /** + * The transition to take upon the invoked child machine sending an error event. + */ + onError?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + ErrorActorEvent, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + > + >; + + onSnapshot?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + SnapshotEvent>, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + > + >; + } & { [K in RequiredActorOptions]: unknown } + > + | { + id?: never; + systemId?: string; + src: AnyActorLogic; + input?: + | Mapper + | NonReducibleUnknown; + onDone?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + DoneActorEvent, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + > + >; + onError?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + ErrorActorEvent, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + > + >; + + onSnapshot?: + | string + | SingleOrArray< + TransitionConfigOrTarget< + TContext, + SnapshotEvent, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + > + >; + } : never; export type InvokeConfig< diff --git a/packages/core/test/setup.types.test.ts b/packages/core/test/setup.types.test.ts index 1406c435b1..4fdbe68f9c 100644 --- a/packages/core/test/setup.types.test.ts +++ b/packages/core/test/setup.types.test.ts @@ -855,6 +855,32 @@ describe('setup()', () => { }); }); + it('should allow anonymous inline actor outside of the configured actors', () => { + setup({ + actors: { + known: fromPromise(async () => 'known') + } + }).createMachine({ + invoke: { + src: fromPromise(async () => 'inline') + } + }); + }); + + it('should disallow anonymous inline actor with an id outside of the configured actors', () => { + setup({ + actors: { + known: fromPromise(async () => 'known') + } + }).createMachine({ + invoke: { + src: fromPromise(async () => 'inline'), + // @ts-expect-error + id: 'myChild' + } + }); + }); + it('should not accept an incompatible provided logic', () => { setup({ actors: { @@ -962,9 +988,9 @@ describe('setup()', () => { ) } }).createMachine({ - // @ts-expect-error invoke: { src: 'fetchUser', + // @ts-expect-error input: 4157 } }); @@ -1016,9 +1042,9 @@ describe('setup()', () => { ) } }).createMachine({ - // @ts-expect-error invoke: { src: 'fetchUser', + // @ts-expect-error input: Math.random() > 0.5 ? { @@ -1040,9 +1066,9 @@ describe('setup()', () => { ) } }).createMachine({ - // @ts-expect-error invoke: { src: 'fetchUser', + // @ts-expect-error input: () => 42 } }); @@ -1079,9 +1105,9 @@ describe('setup()', () => { ) } }).createMachine({ - // @ts-expect-error invoke: { src: 'fetchUser', + // @ts-expect-error input: () => Math.random() > 0.5 ? { diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 5f02489810..e8d50344ba 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -870,10 +870,13 @@ describe('spawnChild action', () => { logic: typeof child; }; }, - entry: spawnChild('child', { + entry: spawnChild( // @ts-expect-error - id: 'child' - }) + 'child', + { + id: 'child' + } + ) }); }); @@ -922,7 +925,7 @@ describe('spawnChild action', () => { }); }); - it(`should not allow anonymous inline actors outside of the configured ones`, () => { + it(`should allow anonymous inline actor outside of the configured actors`, () => { const child1 = createMachine({ context: { counter: 0 @@ -942,9 +945,36 @@ describe('spawnChild action', () => { logic: typeof child1; }; }, - entry: + entry: spawnChild(child2) + }); + }); + + it(`should disallow anonymous inline actor with an id outside of the configured actors`, () => { + const child1 = createMachine({ + context: { + counter: 0 + } + }); + + const child2 = createMachine({ + context: { + answer: '' + } + }); + + createMachine({ + types: {} as { + actors: { + src: 'child'; + logic: typeof child1; + id: 'myChild'; + }; + }, + entry: spawnChild( // @ts-expect-error - spawnChild(child2) + child2, + { id: 'myChild' } + ) }); }); @@ -960,10 +990,13 @@ describe('spawnChild action', () => { logic: typeof child; }; }, - entry: spawnChild('child', { + entry: spawnChild( // @ts-expect-error - input: 'hello' - }) + 'child', + { + input: 'hello' + } + ) }); }); @@ -1015,10 +1048,13 @@ describe('spawnChild action', () => { logic: typeof child; }; }, - entry: spawnChild('child', { + entry: spawnChild( // @ts-expect-error - input: Math.random() > 0.5 ? 'string' : 42 - }) + 'child', + { + input: Math.random() > 0.5 ? 'string' : 42 + } + ) }); }); @@ -1034,10 +1070,13 @@ describe('spawnChild action', () => { logic: typeof child; }; }, - entry: spawnChild('child', { + entry: spawnChild( // @ts-expect-error - input: () => 'hello' - }) + 'child', + { + input: () => 'hello' + } + ) }); }); @@ -1071,10 +1110,13 @@ describe('spawnChild action', () => { logic: typeof child; }; }, - entry: spawnChild('child', { + entry: spawnChild( // @ts-expect-error - input: () => (Math.random() > 0.5 ? 42 : 'hello') - }) + 'child', + { + input: () => (Math.random() > 0.5 ? 42 : 'hello') + } + ) }); }); @@ -1236,8 +1278,8 @@ describe('spawner in assign', () => { }; }, entry: assign(({ spawn }) => { + // @ts-expect-error spawn('child', { - // @ts-expect-error id: 'child' }); return {}; @@ -1298,7 +1340,7 @@ describe('spawner in assign', () => { }); }); - it(`should not allow anonymous inline actors outside of the configured ones`, () => { + it(`should allow anonymous inline actor outside of the configured actors`, () => { const child1 = createMachine({ context: { counter: 0 @@ -1319,13 +1361,41 @@ describe('spawner in assign', () => { }; }, entry: assign(({ spawn }) => { - // @ts-expect-error spawn(child2); return {}; }) }); }); + it(`should no allow anonymous inline actor with an id outside of the configured ones`, () => { + const child1 = createMachine({ + context: { + counter: 0 + } + }); + + const child2 = createMachine({ + context: { + answer: '' + } + }); + + createMachine({ + types: {} as { + actors: { + src: 'child'; + logic: typeof child1; + id: 'myChild'; + }; + }, + entry: assign(({ spawn }) => { + // @ts-expect-error + spawn(child2, { id: 'myChild' }); + return {}; + }) + }); + }); + it(`should reject static wrong input`, () => { const child = fromPromise(({}: { input: number }) => Promise.resolve('foo') @@ -1339,8 +1409,8 @@ describe('spawner in assign', () => { }; }, entry: assign(({ spawn }) => { + // @ts-expect-error spawn('child', { - // @ts-expect-error input: 'hello' }); return {}; @@ -1403,8 +1473,8 @@ describe('spawner in assign', () => { }; }, entry: assign(({ spawn }) => { + // @ts-expect-error spawn('child', { - // @ts-expect-error input: Math.random() > 0.5 ? 'string' : 42 }); return {}; @@ -1425,8 +1495,8 @@ describe('spawner in assign', () => { }; }, entry: assign(({ spawn }) => { + // @ts-expect-error spawn('child', { - // @ts-expect-error input: () => 42 }); return {}; @@ -1648,8 +1718,8 @@ describe('invoke', () => { logic: typeof child; }; }, - // @ts-expect-error invoke: { + // @ts-expect-error id: 'child', src: 'child' } @@ -1707,7 +1777,7 @@ describe('invoke', () => { }); }); - it(`should not allow anonymous inline actors outside of the configured ones`, () => { + it(`should allow anonymous inline actor outside of the configured actors`, () => { const child1 = createMachine({ context: { counter: 0 @@ -1727,13 +1797,41 @@ describe('invoke', () => { logic: typeof child1; }; }, - // @ts-expect-error invoke: { src: child2 } }); }); + it(`should diallow anonymous inline actor with an id outside of the configured actors`, () => { + const child1 = createMachine({ + context: { + counter: 0 + } + }); + + const child2 = createMachine({ + context: { + answer: '' + } + }); + + createMachine({ + types: {} as { + actors: { + src: 'child'; + logic: typeof child1; + id: 'myChild'; + }; + }, + invoke: { + src: child2, + // @ts-expect-error + id: 'myChild' + } + }); + }); + it(`should reject static wrong input`, () => { const child = fromPromise(({}: { input: number }) => Promise.resolve('foo') @@ -1746,9 +1844,9 @@ describe('invoke', () => { logic: typeof child; }; }, - // @ts-expect-error invoke: { src: 'child', + // @ts-expect-error input: 'hello' } }); @@ -1804,9 +1902,9 @@ describe('invoke', () => { logic: typeof child; }; }, - // @ts-expect-error invoke: { src: 'child', + // @ts-expect-error input: Math.random() > 0.5 ? 'string' : 42 } }); @@ -1824,9 +1922,9 @@ describe('invoke', () => { logic: typeof child; }; }, - // @ts-expect-error invoke: { src: 'child', + // @ts-expect-error input: () => 'hello' } }); @@ -1863,9 +1961,9 @@ describe('invoke', () => { logic: typeof child; }; }, - // @ts-expect-error invoke: { src: 'child', + // @ts-expect-error input: () => (Math.random() > 0.5 ? 42 : 'hello') } });