Skip to content

Commit bf6119a

Browse files
Required input for spawn when defined inside referenced actor (#5139)
* Make `spawn` input required when defined (with tests) * changeset `spawn` input required
1 parent f6f0a64 commit bf6119a

File tree

5 files changed

+155
-48
lines changed

5 files changed

+155
-48
lines changed

.changeset/big-pumas-search.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'xstate': patch
3+
---
4+
5+
Make `spawn` input required when defined inside referenced actor:
6+
7+
```ts
8+
const childMachine = createMachine({
9+
types: { input: {} as { value: number } }
10+
});
11+
12+
const machine = createMachine({
13+
types: {} as { context: { ref: ActorRefFrom<typeof childMachine> } },
14+
context: ({ spawn }) => ({
15+
ref: spawn(
16+
childMachine,
17+
// Input is now required!
18+
{ input: { value: 42 } }
19+
)
20+
})
21+
});
22+
```

packages/core/src/spawn.ts

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
IsNotNever,
1414
ProvidedActor,
1515
RequiredActorOptions,
16-
TODO
16+
TODO,
17+
type RequiredLogicInput
1718
} from './types.ts';
1819
import { resolveReferencedActor } from './utils.ts';
1920

@@ -36,42 +37,56 @@ type SpawnOptions<
3637
>
3738
: never;
3839

39-
export type Spawner<TActor extends ProvidedActor> = IsLiteralString<
40-
TActor['src']
41-
> extends true
42-
? {
43-
<TSrc extends TActor['src']>(
44-
logic: TSrc,
45-
...[options]: SpawnOptions<TActor, TSrc>
46-
): ActorRefFromLogic<GetConcreteByKey<TActor, 'src', TSrc>['logic']>;
47-
<TLogic extends AnyActorLogic>(
48-
src: TLogic,
49-
options?: {
50-
id?: never;
51-
systemId?: string;
52-
input?: InputFrom<TLogic>;
53-
syncSnapshot?: boolean;
54-
}
55-
): ActorRefFromLogic<TLogic>;
56-
}
57-
: <TLogic extends AnyActorLogic | string>(
58-
src: TLogic,
59-
options?: {
60-
id?: string;
61-
systemId?: string;
62-
input?: TLogic extends string ? unknown : InputFrom<TLogic>;
63-
syncSnapshot?: boolean;
40+
export type Spawner<TActor extends ProvidedActor> =
41+
IsLiteralString<TActor['src']> extends true
42+
? {
43+
<TSrc extends TActor['src']>(
44+
logic: TSrc,
45+
...[options]: SpawnOptions<TActor, TSrc>
46+
): ActorRefFromLogic<GetConcreteByKey<TActor, 'src', TSrc>['logic']>;
47+
<TLogic extends AnyActorLogic>(
48+
src: TLogic,
49+
...[options]: ConditionalRequired<
50+
[
51+
options?: {
52+
id?: never;
53+
systemId?: string;
54+
input?: InputFrom<TLogic>;
55+
syncSnapshot?: boolean;
56+
} & { [K in RequiredLogicInput<TLogic>]: unknown }
57+
],
58+
IsNotNever<RequiredLogicInput<TLogic>>
59+
>
60+
): ActorRefFromLogic<TLogic>;
6461
}
65-
) => TLogic extends AnyActorLogic ? ActorRefFromLogic<TLogic> : AnyActorRef;
62+
: <TLogic extends AnyActorLogic | string>(
63+
src: TLogic,
64+
...[options]: ConditionalRequired<
65+
[
66+
options?: {
67+
id?: string;
68+
systemId?: string;
69+
input?: TLogic extends string ? unknown : InputFrom<TLogic>;
70+
syncSnapshot?: boolean;
71+
} & (TLogic extends AnyActorLogic
72+
? { [K in RequiredLogicInput<TLogic>]: unknown }
73+
: {})
74+
],
75+
IsNotNever<
76+
TLogic extends AnyActorLogic ? RequiredLogicInput<TLogic> : never
77+
>
78+
>
79+
) => TLogic extends AnyActorLogic
80+
? ActorRefFromLogic<TLogic>
81+
: AnyActorRef;
6682

6783
export function createSpawner(
6884
actorScope: AnyActorScope,
6985
{ machine, context }: AnyMachineSnapshot,
7086
event: AnyEventObject,
7187
spawnedChildren: Record<string, AnyActorRef>
7288
): Spawner<any> {
73-
const spawn: Spawner<any> = (src, options = {}) => {
74-
const { systemId, input } = options;
89+
const spawn: Spawner<any> = ((src, options) => {
7590
if (typeof src === 'string') {
7691
const logic = resolveReferencedActor(machine, src);
7792

@@ -82,38 +97,38 @@ export function createSpawner(
8297
}
8398

8499
const actorRef = createActor(logic, {
85-
id: options.id,
100+
id: options?.id,
86101
parent: actorScope.self,
87-
syncSnapshot: options.syncSnapshot,
102+
syncSnapshot: options?.syncSnapshot,
88103
input:
89-
typeof input === 'function'
90-
? input({
104+
typeof options?.input === 'function'
105+
? options.input({
91106
context,
92107
event,
93108
self: actorScope.self
94109
})
95-
: input,
110+
: options?.input,
96111
src,
97-
systemId
112+
systemId: options?.systemId
98113
}) as any;
99114

100115
spawnedChildren[actorRef.id] = actorRef;
101116

102117
return actorRef;
103118
} else {
104119
const actorRef = createActor(src, {
105-
id: options.id,
120+
id: options?.id,
106121
parent: actorScope.self,
107-
syncSnapshot: options.syncSnapshot,
108-
input: options.input,
122+
syncSnapshot: options?.syncSnapshot,
123+
input: options?.input,
109124
src,
110-
systemId
125+
systemId: options?.systemId
111126
});
112127

113128
return actorRef;
114129
}
115-
};
116-
return (src, options) => {
130+
}) as Spawner<any>;
131+
return ((src, options) => {
117132
const actorRef = spawn(src, options) as TODO; // TODO: fix types
118133
spawnedChildren[actorRef.id] = actorRef;
119134
actorScope.defer(() => {
@@ -123,5 +138,5 @@ export function createSpawner(
123138
actorRef.start();
124139
});
125140
return actorRef;
126-
};
141+
}) as Spawner<any>;
127142
}

packages/core/src/types.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type { MachineSnapshot } from './State.ts';
22
import type { StateMachine } from './StateMachine.ts';
33
import type { StateNode } from './StateNode.ts';
44
import { AssignArgs } from './actions/assign.ts';
5+
import { ExecutableRaiseAction } from './actions/raise.ts';
6+
import { ExecutableSendToAction } from './actions/send.ts';
57
import { PromiseActorLogic } from './actors/promise.ts';
6-
import { Guard, GuardPredicate, UnknownGuard } from './guards.ts';
78
import type { Actor, ProcessingStatus } from './createActor.ts';
9+
import { Guard, GuardPredicate, UnknownGuard } from './guards.ts';
10+
import { InspectionEvent } from './inspection.ts';
811
import { Spawner } from './spawn.ts';
912
import { AnyActorSystem, Clock } from './system.js';
10-
import { InspectionEvent } from './inspection.ts';
11-
import { ExecutableRaiseAction } from './actions/raise.ts';
12-
import { ExecutableSendToAction } from './actions/send.ts';
1313

1414
export type Identity<T> = { [K in keyof T]: T[K] };
1515

@@ -805,8 +805,8 @@ export type InvokeConfig<
805805
>
806806
>;
807807
/**
808-
* The transition to take upon the invoked child machine sending an
809-
* error event.
808+
* The transition to take upon the invoked child machine sending an error
809+
* event.
810810
*/
811811
onError?:
812812
| string
@@ -2452,6 +2452,9 @@ export type RequiredActorOptions<TActor extends ProvidedActor> =
24522452
| (undefined extends TActor['id'] ? never : 'id')
24532453
| (undefined extends InputFrom<TActor['logic']> ? never : 'input');
24542454

2455+
export type RequiredLogicInput<TLogic extends AnyActorLogic> =
2456+
undefined extends InputFrom<TLogic> ? never : 'input';
2457+
24552458
type ExtractLiteralString<T extends string | undefined> = T extends string
24562459
? string extends T
24572460
? never

packages/core/test/spawn.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ActorRefFrom, createActor, createMachine } from '../src';
2+
3+
describe('spawn inside machine', () => {
4+
it('input is required when defined in actor', () => {
5+
const childMachine = createMachine({
6+
types: { input: {} as { value: number } }
7+
});
8+
const machine = createMachine({
9+
types: {} as { context: { ref: ActorRefFrom<typeof childMachine> } },
10+
context: ({ spawn }) => ({
11+
ref: spawn(childMachine, { input: { value: 42 }, systemId: 'test' })
12+
})
13+
});
14+
15+
const actor = createActor(machine).start();
16+
expect(actor.system.get('test')).toBeDefined();
17+
});
18+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ActorRefFrom, assign, createMachine } from '../src';
2+
3+
describe('spawn inside machine', () => {
4+
it('input is required when defined in actor', () => {
5+
const childMachine = createMachine({
6+
types: { input: {} as { value: number } }
7+
});
8+
createMachine({
9+
types: {} as { context: { ref: ActorRefFrom<typeof childMachine> } },
10+
context: ({ spawn }) => ({
11+
ref: spawn(childMachine, { input: { value: 42 } })
12+
}),
13+
initial: 'idle',
14+
states: {
15+
Idle: {
16+
on: {
17+
event: {
18+
actions: assign(({ spawn }) => ({
19+
ref: spawn(childMachine, { input: { value: 42 } })
20+
}))
21+
}
22+
}
23+
}
24+
}
25+
});
26+
});
27+
28+
it('input is not required when not defined in actor', () => {
29+
const childMachine = createMachine({});
30+
createMachine({
31+
types: {} as { context: { ref: ActorRefFrom<typeof childMachine> } },
32+
context: ({ spawn }) => ({
33+
ref: spawn(childMachine)
34+
}),
35+
initial: 'idle',
36+
states: {
37+
Idle: {
38+
on: {
39+
some: {
40+
actions: assign(({ spawn }) => ({
41+
ref: spawn(childMachine)
42+
}))
43+
}
44+
}
45+
}
46+
}
47+
});
48+
});
49+
});

0 commit comments

Comments
 (0)