Skip to content

Commit

Permalink
Allow inline actor logic (#4806)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
davidkpiano and Andarist authored Apr 16, 2024
1 parent 4513ae8 commit f4e0ec4
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 126 deletions.
22 changes: 22 additions & 0 deletions .changeset/healthy-pigs-happen.md
Original file line number Diff line number Diff line change
@@ -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
})
// ...
}
});
```
42 changes: 26 additions & 16 deletions packages/core/src/actions/spawnChild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TActor>]: unknown;
}
],
IsNotNever<RequiredActorOptions<TActor>>
>
: never;
> =
| (TActor extends any
? ConditionalRequired<
[
src: TActor['src'],
options?: SpawnActionOptions<
TContext,
TExpressionEvent,
TEvent,
TActor
> & {
[K in RequiredActorOptions<TActor>]: unknown;
}
],
IsNotNever<RequiredActorOptions<TActor>>
>
: never)
| [
src: AnyActorLogic,
options?: SpawnActionOptions<
TContext,
TExpressionEvent,
TEvent,
ProvidedActor
> & { id?: never }
];

type SpawnArguments<
TContext extends MachineContext,
Expand Down
19 changes: 15 additions & 4 deletions packages/core/src/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,21 @@ type GetConcreteLogic<
export type Spawner<TActor extends ProvidedActor> = IsLiteralString<
TActor['src']
> extends true
? <TSrc extends TActor['src']>(
logic: TSrc,
...[options]: SpawnOptions<TActor, TSrc>
) => ActorRefFrom<GetConcreteLogic<TActor, TSrc>>
? {
<TSrc extends TActor['src']>(
logic: TSrc,
...[options]: SpawnOptions<TActor, TSrc>
): ActorRefFrom<GetConcreteLogic<TActor, TSrc>>;
<TLogic extends AnyActorLogic>(
src: TLogic,
options?: {
id?: never;
systemId?: string;
input?: InputFrom<TLogic>;
syncSnapshot?: boolean;
}
): ActorRefFrom<TLogic>;
}
: <TLogic extends AnyActorLogic | string>(
src: TLogic,
options?: {
Expand Down
199 changes: 128 additions & 71 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TContext, TEvent, InputFrom<TSpecificActor['logic']>, TEvent>
| InputFrom<TSpecificActor['logic']>;
/**
* The transition to take upon the invoked child machine reaching its final top-level state.
*/
onDone?:
| string
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
DoneActorEvent<OutputFrom<TSpecificActor['logic']>>,
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<SnapshotFrom<TSpecificActor['logic']>>,
TEvent,
TActor,
TAction,
TGuard,
TDelay,
TEmitted
>
>;
} & { [K in RequiredActorOptions<TSpecificActor>]: 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<TSpecificActor['logic']>,
TEvent
>
| InputFrom<TSpecificActor['logic']>;
/**
* The transition to take upon the invoked child machine reaching its final top-level state.
*/
onDone?:
| string
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
DoneActorEvent<OutputFrom<TSpecificActor['logic']>>,
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<SnapshotFrom<TSpecificActor['logic']>>,
TEvent,
TActor,
TAction,
TGuard,
TDelay,
TEmitted
>
>;
} & { [K in RequiredActorOptions<TSpecificActor>]: unknown }
>
| {
id?: never;
systemId?: string;
src: AnyActorLogic;
input?:
| Mapper<TContext, TEvent, NonReducibleUnknown, TEvent>
| NonReducibleUnknown;
onDone?:
| string
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
DoneActorEvent<unknown>,
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<
Expand Down
34 changes: 30 additions & 4 deletions packages/core/test/setup.types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -962,9 +988,9 @@ describe('setup()', () => {
)
}
}).createMachine({
// @ts-expect-error
invoke: {
src: 'fetchUser',
// @ts-expect-error
input: 4157
}
});
Expand Down Expand Up @@ -1016,9 +1042,9 @@ describe('setup()', () => {
)
}
}).createMachine({
// @ts-expect-error
invoke: {
src: 'fetchUser',
// @ts-expect-error
input:
Math.random() > 0.5
? {
Expand All @@ -1040,9 +1066,9 @@ describe('setup()', () => {
)
}
}).createMachine({
// @ts-expect-error
invoke: {
src: 'fetchUser',
// @ts-expect-error
input: () => 42
}
});
Expand Down Expand Up @@ -1079,9 +1105,9 @@ describe('setup()', () => {
)
}
}).createMachine({
// @ts-expect-error
invoke: {
src: 'fetchUser',
// @ts-expect-error
input: () =>
Math.random() > 0.5
? {
Expand Down
Loading

0 comments on commit f4e0ec4

Please sign in to comment.