Skip to content

Commit

Permalink
[core] Wildcard emitted event listener (#4905)
Browse files Browse the repository at this point in the history
* Wildcard emitted event listener

* Remove from ActorRef types (doesn't belong there)

* Cleanup

* Add back on: ( ... ) to ActorRef
  • Loading branch information
davidkpiano committed Jun 1, 2024
1 parent 0a9af79 commit dbeafeb
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 18 deletions.
11 changes: 11 additions & 0 deletions .changeset/big-ghosts-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'xstate': patch
---

You can now use a wildcard to listen for _any_ emitted event from an actor:

```ts
actor.on('*', (emitted) => {
console.log(emitted); // Any emitted event
});
```
24 changes: 13 additions & 11 deletions packages/core/src/createActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ const defaultOptions = {
* An Actor is a running process that can receive events, send events and change its behavior based on the events it receives, which can cause effects outside of the actor. When you run a state machine, it becomes an actor.
*/
export class Actor<TLogic extends AnyActorLogic>
implements
ActorRef<SnapshotFrom<TLogic>, EventFromLogic<TLogic>, EmittedFrom<TLogic>>
implements ActorRef<SnapshotFrom<TLogic>, EventFromLogic<TLogic>>
{
/**
* The current internal state of the actor.
Expand Down Expand Up @@ -107,11 +106,7 @@ export class Actor<TLogic extends AnyActorLogic>
public _parent?: AnyActorRef;
/** @internal */
public _syncSnapshot?: boolean;
public ref: ActorRef<
SnapshotFrom<TLogic>,
EventFromLogic<TLogic>,
EmittedFrom<TLogic>
>;
public ref: ActorRef<SnapshotFrom<TLogic>, EventFromLogic<TLogic>>;
// TODO: add typings for system
private _actorScope: ActorScope<
SnapshotFrom<TLogic>,
Expand Down Expand Up @@ -194,10 +189,15 @@ export class Actor<TLogic extends AnyActorLogic>
},
emit: (emittedEvent) => {
const listeners = this.eventListeners.get(emittedEvent.type);
if (!listeners) {
const wildcardListener = this.eventListeners.get('*');
if (!listeners && !wildcardListener) {
return;
}
for (const handler of Array.from(listeners)) {
const allListeners = new Set([
...(listeners ? listeners.values() : []),
...(wildcardListener ? wildcardListener.values() : [])
]);
for (const handler of Array.from(allListeners)) {
handler(emittedEvent);
}
}
Expand Down Expand Up @@ -419,9 +419,11 @@ export class Actor<TLogic extends AnyActorLogic>
};
}

public on<TType extends EmittedFrom<TLogic>['type']>(
public on<TType extends EmittedFrom<TLogic>['type'] | '*'>(
type: TType,
handler: (emitted: EmittedFrom<TLogic> & { type: TType }) => void
handler: (
emitted: EmittedFrom<TLogic> & (TType extends '*' ? {} : { type: TType })
) => void
): Subscription {
let listeners = this.eventListeners.get(type);
if (!listeners) {
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2171,13 +2171,17 @@ export interface ActorRef<
/** @internal */
_processingStatus: ProcessingStatus;
src: string | AnyActorLogic;
on: <TType extends TEmitted['type']>(
// TODO: remove from ActorRef interface
// (should only be available on Actor)
on: <TType extends TEmitted['type'] | '*'>(
type: TType,
handler: (emitted: TEmitted & { type: TType }) => void
handler: (
emitted: TEmitted & (TType extends '*' ? {} : { type: TType })
) => void
) => Subscription;
}

export type AnyActorRef = ActorRef<any, any, any>;
export type AnyActorRef = ActorRef<any, any>;

export type ActorLogicFrom<T> = ReturnTypeOrValue<T> extends infer R
? R extends StateMachine<
Expand Down Expand Up @@ -2228,8 +2232,7 @@ export type ActorRefFrom<T> = ReturnTypeOrValue<T> extends infer R
TOutput,
TMeta
>,
TEvent,
TEmitted
TEvent
>
: R extends Promise<infer U>
? ActorRefFrom<PromiseActorLogic<U>>
Expand All @@ -2240,7 +2243,7 @@ export type ActorRefFrom<T> = ReturnTypeOrValue<T> extends infer R
infer _TSystem,
infer TEmitted
>
? ActorRef<TSnapshot, TEvent, TEmitted>
? ActorRef<TSnapshot, TEvent>
: never
: never;

Expand Down Expand Up @@ -2338,7 +2341,7 @@ export interface ActorScope<
TSystem extends AnyActorSystem = AnyActorSystem,
TEmitted extends EventObject = EventObject
> {
self: ActorRef<TSnapshot, TEvent, TEmitted>;
self: ActorRef<TSnapshot, TEvent>;
id: string;
sessionId: string;
logger: (...args: any[]) => void;
Expand Down
33 changes: 33 additions & 0 deletions packages/core/test/emit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,37 @@ describe('event emitter', () => {
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('b');
});

it('wildcard listeners should be able to receive all emitted events', () => {
const spy = jest.fn();

const machine = setup({
types: {
events: {} as { type: 'event' },
emitted: {} as { type: 'emitted' } | { type: 'anotherEmitted' }
}
}).createMachine({
on: {
event: {
actions: emit({ type: 'emitted' })
}
}
});

const actor = createActor(machine);

actor.on('*', (ev) => {
ev.type satisfies 'emitted' | 'anotherEmitted';

// @ts-expect-error
ev.type satisfies 'whatever';
spy(ev);
});

actor.start();

actor.send({ type: 'event' });

expect(spy).toHaveBeenCalledTimes(1);
});
});

0 comments on commit dbeafeb

Please sign in to comment.