Skip to content

Commit 163d595

Browse files
committed
Add trigger
1 parent c81c75f commit 163d595

File tree

5 files changed

+165
-10
lines changed

5 files changed

+165
-10
lines changed

packages/core/src/createActor.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ export class Actor<TLogic extends AnyActorLogic>
132132
public system: AnyActorSystem;
133133
private _doneEvent?: DoneActorEvent;
134134

135+
public trigger: ActorRef<
136+
SnapshotFrom<TLogic>,
137+
EventFromLogic<TLogic>,
138+
EmittedFrom<TLogic>
139+
>['trigger'];
140+
135141
public src: string | AnyActorLogic;
136142

137143
/**
@@ -235,6 +241,20 @@ export class Actor<TLogic extends AnyActorLogic>
235241
// Ensure that the send method is bound to this Actor instance
236242
// if destructured
237243
this.send = this.send.bind(this);
244+
this.trigger = new Proxy(
245+
{} as ActorRef<
246+
SnapshotFrom<TLogic>,
247+
EventFromLogic<TLogic>,
248+
EmittedFrom<TLogic>
249+
>['trigger'],
250+
{
251+
get: (_, eventType: string) => {
252+
return (payload?: any) => {
253+
this.send({ ...payload, type: eventType });
254+
};
255+
}
256+
}
257+
);
238258

239259
// unified '@xstate.transition' event replaces '@xstate.actor'
240260

packages/core/src/createMachine.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ export function createMachine<
8585
TOutputSchema extends StandardSchemaV1,
8686
TMetaSchema extends StandardSchemaV1,
8787
TTagSchema extends StandardSchemaV1,
88-
// TContext extends MachineContext,
8988
_TEvent extends EventObject,
9089
TActor extends ProvidedActor,
9190
TActionMap extends Implementations['actions'],

packages/core/src/types.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export type IndexByProp<T extends Record<P, string>, P extends keyof T> = {
6565

6666
export type IndexByType<T extends { type: string }> = IndexByProp<T, 'type'>;
6767

68+
export type IsEmptyObject<T> = keyof T extends never ? true : false;
69+
6870
export type Equals<A1, A2> =
6971
(<A>() => A extends A2 ? true : false) extends <A>() => A extends A1
7072
? true
@@ -1705,13 +1707,16 @@ export interface ActorRef<
17051707
emitted: TEmitted & (TType extends '*' ? unknown : { type: TType })
17061708
) => void
17071709
) => Subscription;
1710+
trigger: {
1711+
[K in TEvent['type']]: IsEmptyObject<
1712+
Omit<Extract<TEvent, { type: K }>, 'type'>
1713+
> extends true
1714+
? () => void
1715+
: (payload: Omit<Extract<TEvent, { type: K }>, 'type'>) => void;
1716+
};
17081717
}
17091718

1710-
export type AnyActorRef = ActorRef<
1711-
any,
1712-
AnyEventObject, // TODO: shouldn't this be AnyEventObject?
1713-
any
1714-
>;
1719+
export type AnyActorRef = ActorRef<any, any, any>;
17151720

17161721
export type ActorRefLike = Pick<
17171722
AnyActorRef,

packages/core/test/actor.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ describe('spawning machines', () => {
211211
});
212212
service.start();
213213

214-
service.send({ type: 'ADD', id: 42 });
215-
service.send({ type: 'SET_COMPLETE', id: 42 });
214+
service.trigger.ADD({ id: 42 });
215+
service.trigger.SET_COMPLETE({ id: 42 });
216216
return promise;
217217
});
218218

@@ -383,7 +383,11 @@ describe('spawning callbacks', () => {
383383
callbackRef: z
384384
.custom<CallbackActorRef<{ type: 'START' }>>()
385385
.optional()
386-
})
386+
}),
387+
events: {
388+
START_CB: z.object({}),
389+
SEND_BACK: z.object({})
390+
}
387391
},
388392
id: 'callback',
389393
initial: 'idle',
@@ -435,7 +439,7 @@ describe('spawning callbacks', () => {
435439
});
436440

437441
callbackService.start();
438-
callbackService.send({ type: 'START_CB' });
442+
callbackService.trigger.START_CB();
439443
return promise;
440444
});
441445

@@ -918,6 +922,7 @@ describe('communicating with spawned actors', () => {
918922
existingRef: z.custom<typeof existingService>().optional()
919923
}),
920924
events: {
925+
// TODO: this causes parentMachine to be any
921926
ACTIVATE: z.object({ origin: z.custom<typeof parentService>() }),
922927
'EXISTING.DONE': z.object({})
923928
}

packages/core/test/trigger.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { createMachine, createActor } from '../src/index.ts';
2+
import { z } from 'zod';
3+
4+
describe('actor.trigger', () => {
5+
it('should send events via trigger', () => {
6+
const machine = createMachine({
7+
initial: 'idle',
8+
states: {
9+
idle: {
10+
on: {
11+
NEXT: 'active'
12+
}
13+
},
14+
active: {}
15+
}
16+
});
17+
18+
const actor = createActor(machine).start();
19+
20+
expect(actor.getSnapshot().value).toBe('idle');
21+
22+
actor.trigger.NEXT();
23+
24+
expect(actor.getSnapshot().value).toBe('active');
25+
});
26+
27+
it('should send events with payload via trigger', () => {
28+
const machine = createMachine({
29+
schemas: {
30+
context: z.object({ count: z.number() }),
31+
events: {
32+
INC: z.object({ by: z.number() })
33+
}
34+
},
35+
context: { count: 0 },
36+
initial: 'idle',
37+
states: {
38+
idle: {
39+
on: {
40+
INC: ({ context, event }) => ({
41+
context: { count: context.count + event.by }
42+
})
43+
}
44+
}
45+
}
46+
});
47+
48+
const actor = createActor(machine).start();
49+
50+
expect(actor.getSnapshot().context.count).toBe(0);
51+
52+
actor.trigger.INC({ by: 5 });
53+
54+
expect(actor.getSnapshot().context.count).toBe(5);
55+
});
56+
57+
it('should work with events with only type (no payload)', () => {
58+
const events: string[] = [];
59+
60+
const machine = createMachine({
61+
initial: 'a',
62+
states: {
63+
a: {
64+
on: {
65+
GO: (_, enq) => {
66+
enq(() => events.push('GO'));
67+
return { target: 'b' };
68+
}
69+
}
70+
},
71+
b: {}
72+
}
73+
});
74+
75+
const actor = createActor(machine).start();
76+
77+
actor.trigger.GO();
78+
79+
expect(events).toEqual(['GO']);
80+
expect(actor.getSnapshot().value).toBe('b');
81+
});
82+
83+
it('should work with multiple event types', () => {
84+
const machine = createMachine({
85+
schemas: {
86+
context: z.object({ count: z.number() }),
87+
events: {
88+
INC: z.object({}),
89+
DEC: z.object({}),
90+
SET: z.object({ value: z.number() })
91+
}
92+
},
93+
context: { count: 0 },
94+
initial: 'active',
95+
states: {
96+
active: {
97+
on: {
98+
INC: ({ context }) => ({
99+
context: { count: context.count + 1 }
100+
}),
101+
DEC: ({ context }) => ({
102+
context: { count: context.count - 1 }
103+
}),
104+
SET: ({ event }) => ({
105+
context: { count: event.value }
106+
})
107+
}
108+
}
109+
}
110+
});
111+
112+
const actor = createActor(machine).start();
113+
114+
actor.trigger.INC();
115+
expect(actor.getSnapshot().context.count).toBe(1);
116+
117+
actor.trigger.INC();
118+
expect(actor.getSnapshot().context.count).toBe(2);
119+
120+
actor.trigger.DEC();
121+
expect(actor.getSnapshot().context.count).toBe(1);
122+
123+
actor.trigger.SET({ value: 100 });
124+
expect(actor.getSnapshot().context.count).toBe(100);
125+
});
126+
});

0 commit comments

Comments
 (0)