Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bd67a8d
Add basic event emitter
davidkpiano Feb 18, 2024
0bfb107
Merge branch 'main' into davidkpiano/event-emitter
davidkpiano Feb 24, 2024
177b5af
Remove id and delay
davidkpiano Feb 24, 2024
eb6219d
Fix types
davidkpiano Feb 25, 2024
f3b4420
Rename
davidkpiano Feb 25, 2024
cb3d527
Add machine types
davidkpiano Feb 25, 2024
488e5f6
Add TEmitted type... everywhere
davidkpiano Feb 25, 2024
10e379b
Avoid upsetting devs who rely on order of ActorLogic<…> generics
davidkpiano Feb 25, 2024
1776853
Same for ActorScope<…>
davidkpiano Feb 25, 2024
e53bc8e
Update packages/core/src/actions/emit.ts
davidkpiano Feb 26, 2024
906ecea
Update packages/core/src/actions/emit.ts
davidkpiano Feb 26, 2024
631531c
Update packages/core/src/State.ts
davidkpiano Feb 26, 2024
e21516f
Update packages/core/src/actions/emit.ts
davidkpiano Feb 26, 2024
b921e0c
Update packages/core/src/actions/emit.ts
davidkpiano Feb 26, 2024
a6f25c2
Update packages/core/src/actions/emit.ts
davidkpiano Feb 26, 2024
4ea771a
Update packages/core/test/types.test.ts
davidkpiano Feb 26, 2024
d9f2adf
Update packages/core/src/createMachine.ts
davidkpiano Feb 26, 2024
b371646
Update packages/core/src/actions/emit.ts
davidkpiano Feb 26, 2024
438aa35
Fix TS error
davidkpiano Feb 26, 2024
486bb19
Add emit to enqueueActions
davidkpiano Feb 26, 2024
6c450c2
Add default
davidkpiano Feb 26, 2024
6744bd7
Wrap handler
davidkpiano Feb 26, 2024
de00cb0
Check for errors
davidkpiano Feb 26, 2024
9d3cc49
Add changeset
davidkpiano Feb 29, 2024
caeec03
Types
davidkpiano Feb 29, 2024
035c78d
Merge branch 'main' into davidkpiano/event-emitter
davidkpiano Feb 29, 2024
f7326fb
small tweaks
Andarist Feb 29, 2024
af70340
fix types
Andarist Feb 29, 2024
50bbddf
tweak things
Andarist Feb 29, 2024
82fa0bd
fix small issues around listeners management
Andarist Feb 29, 2024
911def1
rename stuff
Andarist Feb 29, 2024
193f513
tighten up one default
Andarist Feb 29, 2024
8a3cb25
remove unused type
Andarist Feb 29, 2024
343c852
fixed `MachineImplementationsActions`
Andarist Feb 29, 2024
497d25f
No need for defer
davidkpiano Feb 29, 2024
dbc672b
Add test
davidkpiano Feb 29, 2024
9976ae4
rewrite test to make it fail correctly
Andarist Feb 29, 2024
98ada6f
defer again
Andarist Feb 29, 2024
91de30c
Add jsdocs
davidkpiano Mar 1, 2024
10f8dca
Update packages/core/src/actions/emit.ts
Andarist Mar 1, 2024
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
152 changes: 152 additions & 0 deletions packages/core/src/actions/emit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import isDevelopment from '#is-development';
import {
ActionArgs,
AnyActorScope,
AnyActor,
AnyMachineSnapshot,
DelayExpr,
EventObject,
MachineContext,
NoInfer,
RaiseActionOptions,
SendExpr,
ParameterizedObject,
AnyEventObject
} from '../types.ts';

function resolveEmit(
_: AnyActorScope,
snapshot: AnyMachineSnapshot,
args: ActionArgs<any, any, any>,
actionParams: ParameterizedObject['params'] | undefined,
{
event: eventOrExpr,
id,
delay
}: {
event:
| EventObject
| SendExpr<
MachineContext,
EventObject,
ParameterizedObject['params'] | undefined,
EventObject,
EventObject
>;
id: string | undefined;
delay:
| string
| number
| DelayExpr<
MachineContext,
EventObject,
ParameterizedObject['params'] | undefined,
EventObject
>
| undefined;
},
{ internalQueue }: { internalQueue: AnyEventObject[] }
) {
const delaysMap = snapshot.machine.implementations.delays;

if (typeof eventOrExpr === 'string') {
throw new Error(
`Only event objects may be used with emit; use emit({ type: "${eventOrExpr}" }) instead`
);
}
const resolvedEvent =
typeof eventOrExpr === 'function'
? eventOrExpr(args, actionParams)
: eventOrExpr;

let resolvedDelay: number | undefined;
if (typeof delay === 'string') {
const configDelay = delaysMap && delaysMap[delay];
resolvedDelay =
typeof configDelay === 'function'
? configDelay(args, actionParams)
: configDelay;
} else {
resolvedDelay =
typeof delay === 'function' ? delay(args, actionParams) : delay;
}
if (typeof resolvedDelay !== 'number') {
internalQueue.push(resolvedEvent);
}
return [snapshot, { event: resolvedEvent, id, delay: resolvedDelay }];
}

function executeEmit(
actorScope: AnyActorScope,
params: {
event: EventObject;
id: string | undefined;
delay: number | undefined;
}
) {
const { event, delay, id } = params;
// if (typeof delay === 'number') {
actorScope.defer(() => {
actorScope.emit(event);
});
return;
// }
}

export interface RaiseAction<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TParams extends ParameterizedObject['params'] | undefined,
TEvent extends EventObject,
TDelay extends string
> {
(args: ActionArgs<TContext, TExpressionEvent, TEvent>, params: TParams): void;
_out_TEvent?: TEvent;
_out_TDelay?: TDelay;
}

/**
* Raises an event. This places the event in the internal event queue, so that
* the event is immediately consumed by the machine in the current step.
*
* @param eventType The event to raise.
*/
export function emit<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TEvent extends EventObject = TExpressionEvent,
TParams extends ParameterizedObject['params'] | undefined =
| ParameterizedObject['params']
| undefined,
TDelay extends string = string
>(
eventOrExpr:
| AnyEventObject
| SendExpr<TContext, TExpressionEvent, TParams, AnyEventObject, TEvent>,
options?: RaiseActionOptions<
TContext,
TExpressionEvent,
TParams,
NoInfer<TEvent>,
NoInfer<TDelay>
>
): RaiseAction<TContext, TExpressionEvent, TParams, TEvent, TDelay> {
function emit(
args: ActionArgs<TContext, TExpressionEvent, TEvent>,
params: TParams
) {
if (isDevelopment) {
throw new Error(`This isn't supposed to be called`);
}
}

emit.type = 'xstate.emit';
emit.event = eventOrExpr;
emit.id = options?.id;
emit.delay = options?.delay;

emit.resolve = resolveEmit;
emit.execute = executeEmit;

return emit;
}
25 changes: 25 additions & 0 deletions packages/core/src/createActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AnyActorSystem, Clock, createSystem } from './system.ts';
import type {
ActorScope,
AnyActorLogic,
AnyEventObject,
ConditionalRequired,
DoneActorEvent,
EventFromLogic,
Expand Down Expand Up @@ -44,6 +45,7 @@ export type EventListener<TEvent extends EventObject = EventObject> = (

export type Listener = () => void;
export type ErrorListener = (error: any) => void;
export type EmittedEventHandler = (event: AnyEventObject) => void;

// those values are currently used by @xstate/react directly so it's important to keep the assigned values in sync
export enum ProcessingStatus {
Expand Down Expand Up @@ -91,6 +93,7 @@ export class Actor<TLogic extends AnyActorLogic>
);

private observers: Set<Observer<SnapshotFrom<TLogic>>> = new Set();
private eventListeners: Map<string, Set<EmittedEventHandler>> = new Map();
private logger: (...args: any[]) => void;

/** @internal */
Expand Down Expand Up @@ -178,6 +181,12 @@ export class Actor<TLogic extends AnyActorLogic>
);
}
(child as any)._stop();
},
emit: (emittedEvent) => {
for (const handler of this.eventListeners.get(emittedEvent.type) ??
[]) {
handler(emittedEvent);
}
}
};

Expand Down Expand Up @@ -397,6 +406,22 @@ export class Actor<TLogic extends AnyActorLogic>
};
}

public on(
emittedEventType: string,
handler: EmittedEventHandler
): Subscription {
const set = this.eventListeners.get(emittedEventType) ?? new Set();
set.add(handler);
this.eventListeners.set(emittedEventType, set);

return {
unsubscribe: () => {
const set = this.eventListeners.get(emittedEventType)!;
set?.delete(handler);
}
};
}

/**
* Starts the Actor from the initial state
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1952,6 +1952,10 @@ export interface ActorRef<
/** @internal */
_processingStatus: ProcessingStatus;
src: string | AnyActorLogic;
on: (
emittedEventType: string,
handler: (emittedEvent: AnyEventObject) => void
) => Subscription;
}

export type AnyActorRef = ActorRef<any, any>;
Expand Down Expand Up @@ -2105,6 +2109,7 @@ export interface ActorScope<
sessionId: string;
logger: (...args: any[]) => void;
defer: (fn: () => void) => void;
emit: (event: AnyEventObject) => void;
system: TSystem;
stopChild: (child: AnyActorRef) => void;
}
Expand Down
26 changes: 26 additions & 0 deletions packages/core/test/eventEmitter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AnyEventObject, createActor, createMachine } from '../src';
import { emit } from '../src/actions/emit';

describe('event emitter', () => {
it('emits events that can be listened to on actorRef.on(…)', async () => {
const machine = createMachine({
on: {
someEvent: {
actions: emit({ type: 'emitted', foo: 'bar' })
}
}
});

const actor = createActor(machine).start();
setTimeout(() => {
actor.send({
type: 'someEvent'
});
});
const event = await new Promise<AnyEventObject>((res) => {
actor.on('emitted', res);
});

expect(event.foo).toBe('bar');
});
});