Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Wildcard emitted event listener #4905

Merged
merged 4 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
Loading