diff --git a/.changeset/big-phones-refuse.md b/.changeset/big-phones-refuse.md new file mode 100644 index 0000000000..51609dc6e2 --- /dev/null +++ b/.changeset/big-phones-refuse.md @@ -0,0 +1,35 @@ +--- +'xstate': minor +--- + +The new `emit(…)` action creator emits events that can be received by listeners. Actors are now event emitters. + +```ts +import { emit } from 'xstate'; + +const machine = createMachine({ + // ... + on: { + something: { + actions: emit({ + type: 'emitted', + some: 'data' + }) + } + } + // ... +}); + +const actor = createActor(machine).start(); + +actor.on('emitted', (event) => { + console.log(event); +}); + +actor.send({ type: 'something' }); +// logs: +// { +// type: 'emitted', +// some: 'data' +// } +``` diff --git a/packages/core/src/State.ts b/packages/core/src/State.ts index 984e9cd3a1..b2a96f0f93 100644 --- a/packages/core/src/State.ts +++ b/packages/core/src/State.ts @@ -68,7 +68,8 @@ interface MachineSnapshotBase< TStateValue, TTag, unknown, - TOutput + TOutput, + EventObject // TEmitted >; /** * The tags of the active state nodes that represent the current state value. diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 22bef4e0ad..8c03a458c7 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -65,6 +65,7 @@ export class StateMachine< TTag extends string, TInput, TOutput, + TEmitted extends EventObject = EventObject, // TODO: remove default TResolvedTypesMeta = ResolveTypegenMeta< TypegenDisabled, NoInfer, @@ -72,14 +73,16 @@ export class StateMachine< TAction, TGuard, TDelay, - TTag + TTag, + TEmitted > > implements ActorLogic< MachineSnapshot, TEvent, TInput, - AnyActorSystem + AnyActorSystem, + TEmitted > { /** @@ -190,6 +193,7 @@ export class StateMachine< TTag, TInput, TOutput, + TEmitted, TResolvedTypesMeta > { const { actions, guards, actors, delays } = this.implementations; @@ -259,7 +263,7 @@ export class StateMachine< TOutput >, event: TEvent, - actorScope: ActorScope + actorScope: ActorScope ): MachineSnapshot { return macrostep(snapshot, event, actorScope).snapshot as typeof snapshot; } @@ -345,7 +349,9 @@ export class StateMachine< public getInitialSnapshot( actorScope: ActorScope< MachineSnapshot, - TEvent + TEvent, + AnyActorSystem, + TEmitted >, input?: TInput ): MachineSnapshot { @@ -445,7 +451,9 @@ export class StateMachine< snapshot: Snapshot, _actorScope: ActorScope< MachineSnapshot, - TEvent + TEvent, + AnyActorSystem, + TEmitted > ): MachineSnapshot { const children: Record = {}; diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index db0a830369..bc1aba2926 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -128,6 +128,7 @@ export class StateNode< any, // tag any, // input any, // output + any, // emitted any // typegen >; /** @@ -159,12 +160,13 @@ export class StateNode< public config: StateNodeConfig< TContext, TEvent, - TODO, // actions TODO, // actors - TODO, // output + TODO, // actions TODO, // guards TODO, // delays - TODO // tags + TODO, // tags + TODO, // output + TODO // emitted >, options: StateNodeOptions ) { diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index 4bbb906beb..9d33903425 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -153,6 +153,7 @@ export function assign< TActor, never, never, + never, never > { function assign( diff --git a/packages/core/src/actions/emit.ts b/packages/core/src/actions/emit.ts new file mode 100644 index 0000000000..ace91965ec --- /dev/null +++ b/packages/core/src/actions/emit.ts @@ -0,0 +1,140 @@ +import isDevelopment from '#is-development'; +import { + ActionArgs, + AnyActorScope, + AnyMachineSnapshot, + EventObject, + MachineContext, + SendExpr, + ParameterizedObject, + ActionFunction +} from '../types.ts'; + +function resolveEmit( + _: AnyActorScope, + snapshot: AnyMachineSnapshot, + args: ActionArgs, + actionParams: ParameterizedObject['params'] | undefined, + { + event: eventOrExpr + }: { + event: + | EventObject + | SendExpr< + MachineContext, + EventObject, + ParameterizedObject['params'] | undefined, + EventObject, + EventObject + >; + } +) { + if (isDevelopment && 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; + return [snapshot, { event: resolvedEvent }]; +} + +function executeEmit( + actorScope: AnyActorScope, + { + event + }: { + event: EventObject; + } +) { + actorScope.defer(() => actorScope.emit(event)); +} + +export interface EmitAction< + TContext extends MachineContext, + TExpressionEvent extends EventObject, + TParams extends ParameterizedObject['params'] | undefined, + TEvent extends EventObject, + TEmitted extends EventObject +> { + (args: ActionArgs, params: TParams): void; + _out_TEmitted?: TEmitted; +} + +/** + * Emits an event to event handlers registered on the actor via `actor.on(event, handler)`. + * + * @example + ```ts + import { emit } from 'xstate'; + + const machine = createMachine({ + // ... + on: { + something: { + actions: emit({ + type: 'emitted', + some: 'data' + }) + } + } + // ... + }); + + const actor = createActor(machine).start(); + + actor.on('emitted', (event) => { + console.log(event); + }); + + actor.send({ type: 'something' }); + // logs: + // { + // type: 'emitted', + // some: 'data' + // } + ``` + */ +export function emit< + TContext extends MachineContext, + TExpressionEvent extends EventObject, + TParams extends ParameterizedObject['params'] | undefined, + TEvent extends EventObject, + TEmitted extends EventObject +>( + /** + * The event to emit, or an expression that returns an event to emit. + */ + eventOrExpr: + | TEmitted + | SendExpr +): ActionFunction< + TContext, + TExpressionEvent, + TEvent, + TParams, + never, + never, + never, + never, + TEmitted +> { + function emit( + args: ActionArgs, + params: TParams + ) { + if (isDevelopment) { + throw new Error(`This isn't supposed to be called`); + } + } + + emit.type = 'xstate.emit'; + emit.event = eventOrExpr; + + emit.resolve = resolveEmit; + emit.execute = executeEmit; + + return emit; +} diff --git a/packages/core/src/actions/enqueueActions.ts b/packages/core/src/actions/enqueueActions.ts index ff3a715d88..b90856a67c 100644 --- a/packages/core/src/actions/enqueueActions.ts +++ b/packages/core/src/actions/enqueueActions.ts @@ -15,6 +15,7 @@ import { } from '../types.ts'; import { assign } from './assign.ts'; import { cancel } from './cancel.ts'; +import { emit } from './emit.ts'; import { raise } from './raise.ts'; import { sendTo } from './send.ts'; import { spawnChild } from './spawnChild.ts'; @@ -27,7 +28,8 @@ interface ActionEnqueuer< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > { ( action: Action< @@ -38,7 +40,8 @@ interface ActionEnqueuer< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > ): void; assign: ( @@ -86,6 +89,11 @@ interface ActionEnqueuer< typeof stopChild > ) => void; + emit: ( + ...args: Parameters< + typeof emit + > + ) => void; } function resolveEnqueueActions( @@ -104,7 +112,8 @@ function resolveEnqueueActions( ProvidedActor, ParameterizedObject, ParameterizedObject, - string + string, + EventObject >; } ) { @@ -136,6 +145,9 @@ function resolveEnqueueActions( enqueue.stopChild = (...args) => { actions.push(stopChild(...args)); }; + enqueue.emit = (...args) => { + actions.push(emit(...args)); + }; collect( { @@ -178,7 +190,8 @@ interface CollectActionsArg< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > extends UnifiedArg { check: ( guard: Guard @@ -190,7 +203,8 @@ interface CollectActionsArg< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; } @@ -202,7 +216,8 @@ type CollectActions< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = ( { context, @@ -217,7 +232,8 @@ type CollectActions< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >, params: TParams ) => void; @@ -250,7 +266,8 @@ export function enqueueActions< TActor extends ProvidedActor = ProvidedActor, TAction extends ParameterizedObject = ParameterizedObject, TGuard extends ParameterizedObject = ParameterizedObject, - TDelay extends string = never + TDelay extends string = never, + TEmitted extends EventObject = EventObject >( collect: CollectActions< TContext, @@ -260,7 +277,8 @@ export function enqueueActions< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > ): ActionFunction< TContext, @@ -270,7 +288,8 @@ export function enqueueActions< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > { function enqueueActions( args: ActionArgs, diff --git a/packages/core/src/actions/raise.ts b/packages/core/src/actions/raise.ts index ddc7af0c66..6f935c4af0 100644 --- a/packages/core/src/actions/raise.ts +++ b/packages/core/src/actions/raise.ts @@ -138,7 +138,8 @@ export function raise< never, never, never, - TDelay + TDelay, + never > { function raise( args: ActionArgs, diff --git a/packages/core/src/actions/send.ts b/packages/core/src/actions/send.ts index 815285fef1..a02ff05e9c 100644 --- a/packages/core/src/actions/send.ts +++ b/packages/core/src/actions/send.ts @@ -230,7 +230,8 @@ export function sendTo< never, never, never, - TDelay + TDelay, + never > { function sendTo( args: ActionArgs, @@ -298,11 +299,11 @@ type Target< TEvent extends EventObject > = | string - | ActorRef + | AnyActorRef | (( args: ActionArgs, params: TParams - ) => string | ActorRef); + ) => string | AnyActorRef); /** * Forwards (sends) an event to the `target` actor. diff --git a/packages/core/src/actions/spawnChild.ts b/packages/core/src/actions/spawnChild.ts index aa383919ec..6312e2f724 100644 --- a/packages/core/src/actions/spawnChild.ts +++ b/packages/core/src/actions/spawnChild.ts @@ -192,6 +192,7 @@ export function spawnChild< TActor, never, never, + never, never > { function spawnChild( diff --git a/packages/core/src/actions/stopChild.ts b/packages/core/src/actions/stopChild.ts index e979d2a031..2f0f1b6917 100644 --- a/packages/core/src/actions/stopChild.ts +++ b/packages/core/src/actions/stopChild.ts @@ -4,6 +4,7 @@ import { ProcessingStatus } from '../createActor.ts'; import { ActionArgs, ActorRef, + AnyActorRef, AnyActorScope, AnyMachineSnapshot, EventObject, @@ -18,11 +19,11 @@ type ResolvableActorRef< TEvent extends EventObject > = | string - | ActorRef + | AnyActorRef | (( args: ActionArgs, params: TParams - ) => ActorRef | string); + ) => AnyActorRef | string); function resolveStop( _: AnyActorScope, @@ -33,7 +34,7 @@ function resolveStop( ) { const actorRefOrString = typeof actorRef === 'function' ? actorRef(args, actionParams) : actorRef; - const resolvedActorRef: ActorRef | undefined = + const resolvedActorRef: AnyActorRef | undefined = typeof actorRefOrString === 'string' ? snapshot.children[actorRefOrString] : actorRefOrString; @@ -52,7 +53,7 @@ function resolveStop( } function executeStop( actorScope: AnyActorScope, - actorRef: ActorRef | undefined + actorRef: AnyActorRef | undefined ) { if (!actorRef) { return; diff --git a/packages/core/src/actors/callback.ts b/packages/core/src/actors/callback.ts index 4c9c7bfd23..c8ed9df29d 100644 --- a/packages/core/src/actors/callback.ts +++ b/packages/core/src/actors/callback.ts @@ -27,7 +27,13 @@ export type CallbackSnapshot = Snapshot & { export type CallbackActorLogic< TEvent extends EventObject, TInput = NonReducibleUnknown -> = ActorLogic, TEvent, TInput, AnyActorSystem>; +> = ActorLogic< + CallbackSnapshot, + TEvent, + TInput, + AnyActorSystem, + EventObject // TEmitted +>; export type CallbackActorRef< TEvent extends EventObject, diff --git a/packages/core/src/actors/observable.ts b/packages/core/src/actors/observable.ts index 99b3d29e13..6fddbe8aa6 100644 --- a/packages/core/src/actors/observable.ts +++ b/packages/core/src/actors/observable.ts @@ -30,7 +30,8 @@ export type ObservableActorLogic< ObservableSnapshot, { type: string; [k: string]: unknown }, TInput, - AnyActorSystem + AnyActorSystem, + EventObject // TEmitted >; export type ObservableActorRef = ActorRefFrom< diff --git a/packages/core/src/actors/promise.ts b/packages/core/src/actors/promise.ts index 65d9c0d630..036ef9b1ae 100644 --- a/packages/core/src/actors/promise.ts +++ b/packages/core/src/actors/promise.ts @@ -3,6 +3,7 @@ import { AnyActorSystem } from '../system.ts'; import { ActorLogic, ActorRefFrom, + EventObject, NonReducibleUnknown, Snapshot } from '../types.ts'; @@ -18,7 +19,8 @@ export type PromiseActorLogic = ActorLogic< PromiseSnapshot, { type: string; [k: string]: unknown }, TInput, // input - AnyActorSystem + AnyActorSystem, + EventObject // TEmitted >; export type PromiseActorRef = ActorRefFrom< diff --git a/packages/core/src/actors/transition.ts b/packages/core/src/actors/transition.ts index f4da5a2718..c474c95a57 100644 --- a/packages/core/src/actors/transition.ts +++ b/packages/core/src/actors/transition.ts @@ -15,8 +15,15 @@ export type TransitionSnapshot = Snapshot & { export type TransitionActorLogic< TContext, TEvent extends EventObject, - TInput extends NonReducibleUnknown -> = ActorLogic, TEvent, TInput, AnyActorSystem>; + TInput extends NonReducibleUnknown, + TEmitted extends EventObject = EventObject +> = ActorLogic< + TransitionSnapshot, + TEvent, + TInput, + AnyActorSystem, + TEmitted +>; export type TransitionActorRef< TContext, @@ -87,7 +94,8 @@ export function fromTransition< TContext, TEvent extends EventObject, TSystem extends AnyActorSystem, - TInput extends NonReducibleUnknown + TInput extends NonReducibleUnknown, + TEmitted extends EventObject = EventObject >( transition: ( snapshot: TContext, @@ -103,7 +111,7 @@ export function fromTransition< input: TInput; self: TransitionActorRef; }) => TContext) // TODO: type -): TransitionActorLogic { +): TransitionActorLogic { return { config: transition, transition: (snapshot, event, actorScope) => { diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 26c0bbf0fa..fa5820f4c1 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -14,8 +14,10 @@ import { AnyActorSystem, Clock, createSystem } from './system.ts'; import type { ActorScope, AnyActorLogic, + AnyActorRef, ConditionalRequired, DoneActorEvent, + EmittedFrom, EventFromLogic, InputFrom, IsNotNever, @@ -69,7 +71,8 @@ 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 - implements ActorRef, EventFromLogic> + implements + ActorRef, EventFromLogic, EmittedFrom> { /** * The current internal state of the actor. @@ -91,21 +94,30 @@ export class Actor ); private observers: Set>> = new Set(); + private eventListeners: Map< + string, + Set<(emittedEvent: EmittedFrom) => void> + > = new Map(); private logger: (...args: any[]) => void; /** @internal */ public _processingStatus: ProcessingStatus = ProcessingStatus.NotStarted; // Actor Ref - public _parent?: ActorRef; + public _parent?: AnyActorRef; /** @internal */ public _syncSnapshot?: boolean; - public ref: ActorRef, EventFromLogic>; + public ref: ActorRef< + SnapshotFrom, + EventFromLogic, + EmittedFrom + >; // TODO: add typings for system private _actorScope: ActorScope< SnapshotFrom, EventFromLogic, - any + AnyActorSystem, + EmittedFrom >; private _systemId: string | undefined; @@ -178,6 +190,15 @@ export class Actor ); } (child as any)._stop(); + }, + emit: (emittedEvent) => { + const listeners = this.eventListeners.get(emittedEvent.type); + if (!listeners) { + return; + } + for (const handler of Array.from(listeners)) { + handler(emittedEvent); + } } }; @@ -397,6 +418,25 @@ export class Actor }; } + public on['type']>( + type: TType, + handler: (emitted: EmittedFrom & { type: TType }) => void + ): Subscription { + let listeners = this.eventListeners.get(type); + if (!listeners) { + listeners = new Set(); + this.eventListeners.set(type, listeners); + } + const wrappedHandler = handler.bind(undefined); + listeners.add(wrappedHandler); + + return { + unsubscribe: () => { + listeners!.delete(wrappedHandler); + } + }; + } + /** * Starts the Actor from the initial state */ diff --git a/packages/core/src/createMachine.ts b/packages/core/src/createMachine.ts index 9cc8d94593..16e1e06c31 100644 --- a/packages/core/src/createMachine.ts +++ b/packages/core/src/createMachine.ts @@ -6,6 +6,7 @@ import { } from './typegenTypes.ts'; import { AnyActorRef, + EventObject, AnyEventObject, Cast, InternalMachineImplementations, @@ -115,6 +116,7 @@ export function createMachine< TTag extends string, TInput, TOutput extends NonReducibleUnknown, + TEmitted extends EventObject, // it's important to have at least one default type parameter here // it allows us to benefit from contextual type instantiation as it makes us to pass the hasInferenceCandidatesOrDefault check in the compiler // we should be able to remove this when we start inferring TConfig, with it we'll always have an inference candidate @@ -131,6 +133,7 @@ export function createMachine< TTag, TInput, TOutput, + TEmitted, TTypesMeta >; schemas?: unknown; @@ -144,6 +147,7 @@ export function createMachine< TTag, TInput, TOutput, + TEmitted, TTypesMeta >, implementations?: InternalMachineImplementations< @@ -155,7 +159,8 @@ export function createMachine< TAction, TGuard, TDelay, - TTag + TTag, + TEmitted > > ): StateMachine< @@ -177,14 +182,25 @@ export function createMachine< TAction, TGuard, TDelay, - TTag + TTag, + TEmitted >['resolved'], 'tags' > & string, TInput, TOutput, - ResolveTypegenMeta + TEmitted, + ResolveTypegenMeta< + TTypesMeta, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TTag, + TEmitted + > > { return new StateMachine< any, @@ -198,6 +214,7 @@ export function createMachine< any, any, any, + any, // TEmitted any >(config as any, implementations as any); } diff --git a/packages/core/src/getNextSnapshot.ts b/packages/core/src/getNextSnapshot.ts index 0ef191fed0..611fcbaff0 100644 --- a/packages/core/src/getNextSnapshot.ts +++ b/packages/core/src/getNextSnapshot.ts @@ -3,6 +3,7 @@ import { ActorScope, AnyActorLogic, AnyActorScope, + EmittedFrom, EventFromLogic, InputFrom, SnapshotFrom @@ -13,14 +14,20 @@ export function createInertActorScope( actorLogic: T ): AnyActorScope { const self = createActor(actorLogic as AnyActorLogic); - const inertActorScope: ActorScope, EventFromLogic, any> = { + const inertActorScope: ActorScope< + SnapshotFrom, + EventFromLogic, + any, + EmittedFrom + > = { self, defer: () => {}, id: '', logger: () => {}, sessionId: '', stopChild: () => {}, - system: self.system + system: self.system, + emit: () => {} }; return inertActorScope; diff --git a/packages/core/src/scxml.ts b/packages/core/src/scxml.ts index ce67399d61..f3216919ec 100644 --- a/packages/core/src/scxml.ts +++ b/packages/core/src/scxml.ts @@ -175,7 +175,7 @@ function createGuard< function mapAction( element: XMLElement -): ActionFunction { +): ActionFunction { switch (element.name) { case 'raise': { return raise({ @@ -338,8 +338,9 @@ return ${element.attributes!.expr}; function mapActions( elements: XMLElement[] -): ActionFunction[] { - const mapped: ActionFunction[] = []; +): ActionFunction[] { + const mapped: ActionFunction[] = + []; for (const element of elements) { if (element.type === 'comment') { diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index 67f737fd52..1666614b9b 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -9,6 +9,7 @@ import { Cast, ConditionalRequired, DelayConfig, + EventObject, Invert, IsNever, MachineConfig, @@ -122,7 +123,8 @@ export function setup< TDelay extends string = never, TTag extends string = string, TInput = NonReducibleUnknown, - TOutput extends NonReducibleUnknown = NonReducibleUnknown + TOutput extends NonReducibleUnknown = NonReducibleUnknown, + TEmitted extends EventObject = EventObject >({ schemas, actors, @@ -131,7 +133,15 @@ export function setup< delays }: { schemas?: unknown; - types?: SetupTypes; + types?: SetupTypes< + TContext, + TEvent, + TChildrenMap, + TTag, + TInput, + TOutput, + TEmitted + >; actors?: { // union here enforces that all configured children have to be provided in actors // it makes those values required here @@ -148,7 +158,8 @@ export function setup< ToProvidedActor, ToParameterizedObject, ToParameterizedObject, - TDelay + TDelay, + TEmitted >; }; guards?: { @@ -181,6 +192,7 @@ export function setup< TTag, TInput, TOutput, + TEmitted, ResolveTypegenMeta< TypegenDisabled, TEvent, @@ -188,7 +200,8 @@ export function setup< ToParameterizedObject, ToParameterizedObject, TDelay, - TTag + TTag, + TEmitted > > >( @@ -208,6 +221,7 @@ export function setup< TTag, TInput, TOutput, + TEmitted, ResolveTypegenMeta< TypegenDisabled, TEvent, @@ -215,7 +229,8 @@ export function setup< ToParameterizedObject, ToParameterizedObject, TDelay, - TTag + TTag, + TEmitted > >; } { diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 09a2fa0a5a..29bb1420f3 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1499,7 +1499,8 @@ function resolveAndExecuteActionsWithContext( ProvidedActor, ParameterizedObject, ParameterizedObject, - string + string, + EventObject > > )[typeof action === 'string' ? action : action.type]; diff --git a/packages/core/src/typegenTypes.ts b/packages/core/src/typegenTypes.ts index ca1c3a4983..a3963d3a9a 100644 --- a/packages/core/src/typegenTypes.ts +++ b/packages/core/src/typegenTypes.ts @@ -215,7 +215,8 @@ export interface ResolveTypegenMeta< TAction extends ParameterizedObject, TGuard extends ParameterizedObject, TDelay extends string, - TTag extends string + TTag extends string, + TEmitted extends EventObject = EventObject > { '@@xstate/typegen': TTypesMeta['@@xstate/typegen']; resolved: { @@ -250,6 +251,7 @@ export interface ResolveTypegenMeta< Prop >; tags: string extends TTag ? Prop : TTag; + emitted: TEmitted; }; disabled: TypegenDisabled & AllImplementationsProvided & @@ -262,6 +264,7 @@ export interface ResolveTypegenMeta< indexedDelays: IndexByType>; invokeSrcNameMap: Record; tags: TTag; + emitted: TEmitted; }; }[IsNever extends true ? 'disabled' diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a1c74fb035..6d4d9d1b92 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -146,7 +146,8 @@ export type InputFrom = T extends StateMachine< infer _TSnapshot, infer _TEvent, infer TInput, - infer _TSystem + infer _TSystem, + infer _TEmitted > ? TInput : never; @@ -155,7 +156,8 @@ export type OutputFrom = T extends ActorLogic< infer TSnapshot, infer _TEvent, infer _TInput, - infer _TSystem + infer _TSystem, + infer _TEmitted > ? (TSnapshot & { status: 'done' })['output'] : T extends ActorRef @@ -170,7 +172,8 @@ export type ActionFunction< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = { (args: ActionArgs, params: TParams): void; _out_TEvent?: TEvent; // TODO: it feels like we should be able to remove this since now `TEvent` is "observable" by `self` @@ -178,30 +181,9 @@ export type ActionFunction< _out_TAction?: TAction; _out_TGuard?: TGuard; _out_TDelay?: TDelay; + _out_TEmitted?: TEmitted; }; -export interface ChooseBranch< - TContext extends MachineContext, - TExpressionEvent extends EventObject, - TEvent extends EventObject = TExpressionEvent, - TActor extends ProvidedActor = ProvidedActor, - TAction extends ParameterizedObject = ParameterizedObject, - TGuard extends ParameterizedObject = ParameterizedObject, - TDelay extends string = string -> { - guard?: Guard; - actions: Actions< - TContext, - TExpressionEvent, - TEvent, - undefined, - TActor, - TAction, - TGuard, - TDelay - >; -} - export type NoRequiredParams = T extends any ? undefined extends T['params'] ? T['type'] @@ -243,7 +225,8 @@ export type Action< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = // TODO: consider merging `NoRequiredParams` and `WithDynamicParams` into one // this way we could iterate over `TAction` (and `TGuard` in the `Guard` type) once and not twice @@ -257,7 +240,8 @@ export type Action< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; export type UnknownAction = Action< @@ -268,7 +252,8 @@ export type UnknownAction = Action< ProvidedActor, ParameterizedObject, ParameterizedObject, - string + string, + EventObject >; export type Actions< @@ -279,7 +264,8 @@ export type Actions< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = SingleOrArray< Action< TContext, @@ -289,7 +275,8 @@ export type Actions< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; @@ -319,7 +306,8 @@ export interface TransitionConfig< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject = EventObject > { guard?: Guard; actions?: Actions< @@ -330,7 +318,8 @@ export interface TransitionConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; reenter?: boolean; target?: TransitionTarget | undefined; @@ -489,7 +478,8 @@ export type StatesConfig< TGuard extends ParameterizedObject, TDelay extends string, TTag extends string, - TOutput + TOutput, + TEmitted extends EventObject > = { [K in string]: StateNodeConfig< TContext, @@ -499,7 +489,8 @@ export type StatesConfig< TGuard, TDelay, TTag, - TOutput + TOutput, + TEmitted >; }; @@ -519,7 +510,8 @@ export type TransitionConfigOrTarget< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = SingleOrArray< | TransitionConfigTarget | TransitionConfig< @@ -529,7 +521,8 @@ export type TransitionConfigOrTarget< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; @@ -539,7 +532,8 @@ export type TransitionsConfig< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = { [K in EventDescriptor]?: TransitionConfigOrTarget< TContext, @@ -548,7 +542,8 @@ export type TransitionsConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; }; @@ -577,6 +572,7 @@ type DistributeActors< TAction extends ParameterizedObject, TGuard extends ParameterizedObject, TDelay extends string, + TEmitted extends EventObject, TSpecificActor extends ProvidedActor > = TSpecificActor extends { src: infer TSrc } ? Compute< @@ -612,7 +608,8 @@ type DistributeActors< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; /** @@ -628,7 +625,8 @@ type DistributeActors< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; @@ -642,7 +640,8 @@ type DistributeActors< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; } & { [K in RequiredActorOptions]: unknown } @@ -655,9 +654,19 @@ export type InvokeConfig< TActor extends ProvidedActor, TAction extends ParameterizedObject, TGuard extends ParameterizedObject, - TDelay extends string + TDelay extends string, + TEmitted extends EventObject > = IsLiteralString extends true - ? DistributeActors + ? DistributeActors< + TContext, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted, + TActor + > : { /** * The unique identifier for the invoked machine. If not specified, this @@ -687,7 +696,8 @@ export type InvokeConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; /** @@ -703,7 +713,8 @@ export type InvokeConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; @@ -717,12 +728,13 @@ export type InvokeConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted > >; }; -export type AnyInvokeConfig = InvokeConfig; +export type AnyInvokeConfig = InvokeConfig; export interface StateNodeConfig< TContext extends MachineContext, @@ -732,7 +744,8 @@ export interface StateNodeConfig< TGuard extends ParameterizedObject, TDelay extends string, TTag extends string, - TOutput + TOutput, + TEmitted extends EventObject > { /** * The initial state transition. @@ -769,19 +782,28 @@ export interface StateNodeConfig< TGuard, TDelay, TTag, - NonReducibleUnknown + NonReducibleUnknown, + TEmitted > | undefined; /** * The services to invoke upon entering this state node. These services will be stopped upon exiting this state node. */ invoke?: SingleOrArray< - InvokeConfig + InvokeConfig >; /** * The mapping of event types to their potential transition(s). */ - on?: TransitionsConfig; + on?: TransitionsConfig< + TContext, + TEvent, + TActor, + TAction, + TGuard, + TDelay, + TEmitted + >; /** * The action(s) to be executed upon entering the state node. */ @@ -793,7 +815,8 @@ export interface StateNodeConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; /** * The action(s) to be executed upon exiting the state node. @@ -806,7 +829,8 @@ export interface StateNodeConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; /** * The potential transition(s) to be taken upon reaching a final child state node. @@ -843,7 +867,8 @@ export interface StateNodeConfig< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; parent?: StateNode; /** @@ -890,6 +915,7 @@ export type AnyStateNodeConfig = StateNodeConfig< any, any, any, + any, any >; @@ -919,7 +945,8 @@ export interface StateNodeDefinition< ParameterizedObject, string, string, - unknown + unknown, + EventObject // TEmitted >['output']; invoke: Array>; description?: string; @@ -960,6 +987,7 @@ export type AnyStateMachine = StateMachine< any, // tag any, // input any, // output + any, // emitted any // typegen >; @@ -976,6 +1004,7 @@ export interface AtomicStateNodeConfig< TODO, TODO, TODO, + TODO, TODO > { initial?: undefined; @@ -997,7 +1026,7 @@ export type SimpleOrStateNodeConfig< TEvent extends EventObject > = | AtomicStateNodeConfig - | StateNodeConfig; + | StateNodeConfig; export type ActionFunctionMap< TContext extends MachineContext, @@ -1005,7 +1034,8 @@ export type ActionFunctionMap< TActor extends ProvidedActor, TAction extends ParameterizedObject = ParameterizedObject, TGuard extends ParameterizedObject = ParameterizedObject, - TDelay extends string = string + TDelay extends string = string, + TEmitted extends EventObject = EventObject > = { [K in TAction['type']]?: ActionFunction< TContext, @@ -1015,7 +1045,8 @@ export type ActionFunctionMap< TActor, TAction, TGuard, - TDelay + TDelay, + TEmitted >; }; @@ -1093,7 +1124,8 @@ type MachineImplementationsActions< 'indexedActions' >, TIndexedGuards = Prop, 'indexedGuards'>, - TIndexedDelays = Prop, 'indexedDelays'> + TIndexedDelays = Prop, 'indexedDelays'>, + TEmitted = Prop, 'emitted'> > = { [K in keyof TIndexedActions]?: ActionFunction< TContext, @@ -1106,7 +1138,8 @@ type MachineImplementationsActions< Cast< Prop, ParameterizedObject - >['type'] + >['type'], + Cast >; }; @@ -1343,6 +1376,7 @@ export type MachineConfig< TTag extends string = string, TInput = any, TOutput = unknown, + TEmitted extends EventObject = EventObject, TTypesMeta = TypegenDisabled > = (Omit< StateNodeConfig< @@ -1353,7 +1387,8 @@ export type MachineConfig< NoInfer, NoInfer, NoInfer, - NoInfer + NoInfer, + NoInfer >, 'output' > & { @@ -1385,7 +1420,8 @@ export interface SetupTypes< TChildrenMap extends Record, TTag extends string, TInput, - TOutput + TOutput, + TEmitted extends EventObject > { context?: TContext; events?: TEvent; @@ -1393,6 +1429,7 @@ export interface SetupTypes< tags?: TTag; input?: TInput; output?: TOutput; + emitted?: TEmitted; } export interface MachineTypes< @@ -1405,6 +1442,7 @@ export interface MachineTypes< TTag extends string, TInput, TOutput, + TEmitted extends EventObject, TTypesMeta = TypegenDisabled > extends SetupTypes< TContext, @@ -1414,7 +1452,8 @@ export interface MachineTypes< never, TTag, TInput, - TOutput + TOutput, + TEmitted > { actors?: TActor; actions?: TAction; @@ -1694,7 +1733,7 @@ export interface StateConfig< * @internal */ _nodes: Array>; - children: Record>; + children: Record; status: 'active' | 'done' | 'error' | 'stopped'; output?: any; error?: unknown; @@ -1709,6 +1748,7 @@ export interface StateConfig< any, any, any, + any, any >; } @@ -1735,7 +1775,7 @@ export interface ActorOptions { * Specifies the logger to be used for `log(...)` actions. Defaults to the native `console.log(...)` method. */ logger?: (...args: any[]) => void; - parent?: ActorRef; + parent?: AnyActorRef; /** * @internal */ @@ -1930,7 +1970,8 @@ export interface ActorLike export interface ActorRef< TSnapshot extends Snapshot, - TEvent extends EventObject + TEvent extends EventObject, + TEmitted extends EventObject = EventObject > extends Subscribable, InteropObservable { /** @@ -1947,14 +1988,18 @@ export interface ActorRef< stop: () => void; toJSON?: () => any; // TODO: figure out how to hide this externally as `sendTo(ctx => ctx.actorRef._parent._parent._parent._parent)` shouldn't be allowed - _parent?: ActorRef; + _parent?: AnyActorRef; system: AnyActorSystem; /** @internal */ _processingStatus: ProcessingStatus; src: string | AnyActorLogic; + on: ( + type: TType, + handler: (emitted: TEmitted & { type: TType }) => void + ) => Subscription; } -export type AnyActorRef = ActorRef; +export type AnyActorRef = ActorRef; export type ActorLogicFrom = ReturnTypeOrValue extends infer R ? R extends StateMachine< @@ -1990,6 +2035,7 @@ export type ActorRefFrom = ReturnTypeOrValue extends infer R infer TTag, infer _TInput, infer TOutput, + infer TEmitted, infer _TResolvedTypesMeta > ? ActorRef< @@ -2001,7 +2047,8 @@ export type ActorRefFrom = ReturnTypeOrValue extends infer R TTag, TOutput >, - TEvent + TEvent, + TEmitted > : R extends Promise ? ActorRefFrom> @@ -2009,9 +2056,10 @@ export type ActorRefFrom = ReturnTypeOrValue extends infer R infer TSnapshot, infer TEvent, infer _TInput, - infer _TSystem + infer _TSystem, + infer TEmitted > - ? ActorRef + ? ActorRef : never : never; @@ -2034,6 +2082,7 @@ export type InterpreterFrom< infer TTag, infer TInput, infer TOutput, + infer TEmitted, infer _TResolvedTypesMeta > ? Actor< @@ -2048,7 +2097,8 @@ export type InterpreterFrom< >, TEvent, TInput, - AnyActorSystem + AnyActorSystem, + TEmitted > > : never; @@ -2068,6 +2118,7 @@ export type MachineImplementationsFrom< infer _TTag, infer _TInput, infer _TOutput, + infer _TEmitted, infer TResolvedTypesMeta > ? InternalMachineImplementations< @@ -2090,6 +2141,7 @@ export type __ResolvedTypesMetaFrom = T extends StateMachine< any, // tag any, // input any, // output + any, // emitted infer TResolvedTypesMeta > ? TResolvedTypesMeta @@ -2098,18 +2150,25 @@ export type __ResolvedTypesMetaFrom = T extends StateMachine< export interface ActorScope< TSnapshot extends Snapshot, TEvent extends EventObject, - TSystem extends AnyActorSystem = AnyActorSystem + TSystem extends AnyActorSystem = AnyActorSystem, + TEmitted extends EventObject = EventObject > { - self: ActorRef; + self: ActorRef; id: string; sessionId: string; logger: (...args: any[]) => void; defer: (fn: () => void) => void; + emit: (event: TEmitted) => void; system: TSystem; stopChild: (child: AnyActorRef) => void; } -export type AnyActorScope = ActorScope; +export type AnyActorScope = ActorScope< + any, // TSnapshot + any, // TEvent + AnyActorSystem, + any // TEmitted +>; export type Snapshot = | { @@ -2145,7 +2204,8 @@ export interface ActorLogic< in out TSnapshot extends Snapshot, // it's invariant because it's also part of `ActorScope["self"]["getSnapshot"]` in out TEvent extends EventObject, // it's invariant because it's also part of `ActorScope["self"]["send"]` in TInput = NonReducibleUnknown, - TSystem extends AnyActorSystem = AnyActorSystem + TSystem extends AnyActorSystem = AnyActorSystem, + in out TEmitted extends EventObject = EventObject // it's invariant because it's also aprt of `ActorScope["self"]["on"]` > { /** The initial setup/configuration used to create the actor logic. */ config?: unknown; @@ -2160,7 +2220,7 @@ export interface ActorLogic< transition: ( snapshot: TSnapshot, message: TEvent, - actorScope: ActorScope + actorScope: ActorScope ) => TSnapshot; /** * Called to provide the initial state of the actor. @@ -2169,7 +2229,7 @@ export interface ActorLogic< * @returns The initial state. */ getInitialSnapshot: ( - actorScope: ActorScope, + actorScope: ActorScope, input: TInput ) => TSnapshot; /** @@ -2181,7 +2241,7 @@ export interface ActorLogic< */ restoreSnapshot?: ( persistedState: Snapshot, - actorScope: ActorScope + actorScope: ActorScope ) => TSnapshot; /** * Called when the actor is started. @@ -2190,7 +2250,7 @@ export interface ActorLogic< */ start?: ( snapshot: TSnapshot, - actorScope: ActorScope + actorScope: ActorScope ) => void; /** * Obtains the internal state of the actor in a representation which can be be persisted. @@ -2208,28 +2268,63 @@ export type AnyActorLogic = ActorLogic< any, // snapshot any, // event any, // input - any // system + any, // system + any // emitted >; -export type UnknownActorLogic = ActorLogic; +export type UnknownActorLogic = ActorLogic< + any, // snapshot + any, // event + never, // input + AnyActorSystem, + any // emitted +>; export type SnapshotFrom = ReturnTypeOrValue extends infer R ? R extends ActorRef ? TSnapshot : R extends Actor ? SnapshotFrom - : R extends ActorLogic + : R extends ActorLogic< + infer _TSnapshot, + infer _TEvent, + infer _TInput, + infer _TEmitted, + infer _TSystem + > ? ReturnType - : R extends ActorScope + : R extends ActorScope< + infer TSnapshot, + infer _TEvent, + infer _TEmitted, + infer _TSystem + > ? TSnapshot : never : never; -export type EventFromLogic> = - TLogic extends ActorLogic +export type EventFromLogic = + TLogic extends ActorLogic< + infer _TSnapshot, + infer TEvent, + infer _TInput, + infer _TEmitted, + infer _TSystem + > ? TEvent : never; +export type EmittedFrom = + TLogic extends ActorLogic< + infer _TSnapshot, + infer _TEvent, + infer _TInput, + infer _TSystem, + infer TEmitted + > + ? TEmitted + : never; + type ResolveEventType = ReturnTypeOrValue extends infer R ? R extends StateMachine< infer _TContext, @@ -2243,6 +2338,7 @@ type ResolveEventType = ReturnTypeOrValue extends infer R infer _TTag, infer _TInput, infer _TOutput, + infer _TEmitted, infer _TResolvedTypesMeta > ? TEvent @@ -2279,6 +2375,7 @@ export type ContextFrom = ReturnTypeOrValue extends infer R infer _TTag, infer _TInput, infer _TOutput, + infer _TEmitted, infer _TResolvedTypesMeta > ? TContext @@ -2303,6 +2400,7 @@ export type ContextFrom = ReturnTypeOrValue extends infer R infer _TTag, infer _TInput, infer _TOutput, + infer _TEmitted, infer _TResolvedTypesMeta > ? TContext diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 900e0e9265..3d7ef4c9b4 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -3,7 +3,7 @@ import { isMachineSnapshot } from './State.ts'; import type { StateNode } from './StateNode.ts'; import { TARGETLESS_KEY } from './constants.ts'; import type { - ActorLogic, + AnyActorLogic, AnyActorRef, AnyEventObject, AnyMachineSnapshot, @@ -197,7 +197,7 @@ export function resolveOutput< return mapper; } -export function isActorLogic(value: any): value is ActorLogic { +export function isActorLogic(value: any): value is AnyActorLogic { return ( value !== null && typeof value === 'object' && @@ -278,7 +278,7 @@ export function resolveReferencedActor(machine: AnyStateMachine, src: string) { return ( Array.isArray(invokeConfig) ? invokeConfig[indexStr as any] - : (invokeConfig as InvokeConfig) + : (invokeConfig as InvokeConfig) ).src; } diff --git a/packages/core/src/waitFor.ts b/packages/core/src/waitFor.ts index 3151618d9c..68b083e9f6 100644 --- a/packages/core/src/waitFor.ts +++ b/packages/core/src/waitFor.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { ActorRef, SnapshotFrom, Subscription } from './types.ts'; +import { ActorRef, AnyActorRef, SnapshotFrom, Subscription } from './types.ts'; interface WaitForOptions { /** @@ -36,7 +36,7 @@ const defaultWaitForOptions: WaitForOptions = { * @returns A promise that eventually resolves to the emitted value * that matches the condition */ -export function waitFor>( +export function waitFor( actorRef: TActorRef, predicate: (emitted: SnapshotFrom) => boolean, options?: Partial diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 8867bf824f..79e1cd8f2e 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -12,6 +12,7 @@ import { CallbackActorRef, fromCallback } from '../src/actors/callback.ts'; import { ActorRef, ActorRefFrom, + AnyActorRef, EventObject, Snapshot, assign, @@ -2463,7 +2464,7 @@ describe('forwardTo()', () => { const parent = createMachine({ types: {} as { - context: { child?: ActorRef }; + context: { child?: AnyActorRef }; events: { type: 'EVENT'; value: number } | { type: 'SUCCESS' }; }, id: 'parent', diff --git a/packages/core/test/actor.test.ts b/packages/core/test/actor.test.ts index 19def52827..e355e80f83 100644 --- a/packages/core/test/actor.test.ts +++ b/packages/core/test/actor.test.ts @@ -34,7 +34,7 @@ import { sleep } from '@xstate-repo/jest-utils'; describe('spawning machines', () => { const context = { - todoRefs: {} as Record> + todoRefs: {} as Record }; type TodoEvent = @@ -191,7 +191,7 @@ describe('spawning machines', () => { const parentMachine = createMachine( { context: { - ref: null! as ActorRef + ref: null! as AnyActorRef }, initial: 'waiting', states: { @@ -472,7 +472,7 @@ describe('spawning observables', () => { id: 'observable', initial: 'idle', context: { - observableRef: undefined! as ActorRef + observableRef: undefined! as AnyActorRef }, states: { idle: { @@ -719,7 +719,7 @@ describe('spawning event observables', () => { id: 'observable', initial: 'idle', context: { - observableRef: undefined! as ActorRef + observableRef: undefined! as AnyActorRef }, states: { idle: { diff --git a/packages/core/test/emit.test.ts b/packages/core/test/emit.test.ts new file mode 100644 index 0000000000..52fa0ddf26 --- /dev/null +++ b/packages/core/test/emit.test.ts @@ -0,0 +1,191 @@ +import { + AnyEventObject, + createActor, + createMachine, + enqueueActions, + setup +} from '../src'; +import { emit } from '../src/actions/emit'; + +describe('event emitter', () => { + it('only emits expected events if specified in setup', () => { + setup({ + types: { + emitted: {} as { type: 'greet'; message: string } + } + }).createMachine({ + // @ts-expect-error + entry: emit({ type: 'nonsense' }), + // @ts-expect-error + exit: emit({ type: 'greet', message: 1234 }), + + on: { + someEvent: { + actions: emit({ type: 'greet', message: 'hello' }) + } + } + }); + }); + + it('emits any events if not specified in setup (unsafe)', () => { + createMachine({ + entry: emit({ type: 'nonsense' }), + exit: emit({ type: 'greet', message: 1234 }), + on: { + someEvent: { + actions: emit({ type: 'greet', message: 'hello' }) + } + } + }); + }); + + it('emits events that can be listened to on actorRef.on(…)', async () => { + const machine = setup({ + types: { + emitted: {} as { type: 'emitted'; foo: string } + } + }).createMachine({ + on: { + someEvent: { + actions: emit({ type: 'emitted', foo: 'bar' }) + } + } + }); + + const actor = createActor(machine).start(); + setTimeout(() => { + actor.send({ + type: 'someEvent' + }); + }); + const event = await new Promise((res) => { + actor.on('emitted', res); + }); + + expect(event.foo).toBe('bar'); + }); + + it('enqueue.emit(…) emits events that can be listened to on actorRef.on(…)', async () => { + const machine = setup({ + types: { + emitted: {} as { type: 'emitted'; foo: string } + } + }).createMachine({ + on: { + someEvent: { + actions: enqueueActions(({ enqueue }) => { + enqueue.emit({ type: 'emitted', foo: 'bar' }); + + enqueue.emit({ + // @ts-expect-error + type: 'unknown' + }); + }) + } + } + }); + + const actor = createActor(machine).start(); + setTimeout(() => { + actor.send({ + type: 'someEvent' + }); + }); + const event = await new Promise((res) => { + actor.on('emitted', res); + }); + + expect(event.foo).toBe('bar'); + }); + + it('handles errors', async () => { + const machine = setup({ + types: { + emitted: {} as { type: 'emitted'; foo: string } + } + }).createMachine({ + on: { + someEvent: { + actions: emit({ type: 'emitted', foo: 'bar' }) + } + } + }); + + const actor = createActor(machine).start(); + actor.on('emitted', () => { + throw new Error('oops'); + }); + setTimeout(() => { + actor.send({ + type: 'someEvent' + }); + }); + const err = await new Promise((res) => + actor.subscribe({ + error: res + }) + ); + + expect(err).toBeInstanceOf(Error); + expect((err as Error).message).toEqual('oops'); + }); + + it('dynamically emits events that can be listened to on actorRef.on(…)', async () => { + const machine = createMachine({ + context: { count: 10 }, + on: { + someEvent: { + actions: emit(({ context }) => ({ + type: 'emitted', + count: context.count + })) + } + } + }); + + const actor = createActor(machine).start(); + setTimeout(() => { + actor.send({ + type: 'someEvent' + }); + }); + const event = await new Promise((res) => { + actor.on('emitted', res); + }); + + expect(event).toEqual({ + type: 'emitted', + count: 10 + }); + }); + + it('listener should be able to read the updated snapshot of the emitting actor', () => { + const spy = jest.fn(); + + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + ev: { + actions: emit({ type: 'someEvent' }), + target: 'b' + } + } + }, + b: {} + } + }); + + const actor = createActor(machine); + actor.on('someEvent', () => { + spy(actor.getSnapshot().value); + }); + + actor.start(); + actor.send({ type: 'ev' }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('b'); + }); +}); diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index 8bd2ac7528..eacb255e31 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -10,7 +10,8 @@ import { cancel, raise, stopChild, - log + log, + AnyActorRef } from '../src/index.ts'; import { interval, from } from 'rxjs'; import { fromObservable } from '../src/actors/observable'; @@ -1670,7 +1671,7 @@ describe('interpreter', () => { context: {} as { machineRef: ActorRefFrom; promiseRef: ActorRefFrom; - observableRef: ActorRef; + observableRef: AnyActorRef; }, entry: assign({ machineRef: ({ spawn }) => diff --git a/packages/core/test/typegenTypes.test.ts b/packages/core/test/typegenTypes.test.ts index 436d8898dd..aa523a5009 100644 --- a/packages/core/test/typegenTypes.test.ts +++ b/packages/core/test/typegenTypes.test.ts @@ -920,6 +920,7 @@ describe('typegen types', () => { any, any, any, + any, any > ) { diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 1b1efcc478..5f02489810 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -306,6 +306,32 @@ describe('output', () => { }); }); +describe('emitted', () => { + it('emitted type should be represented in actor.on(…)', () => { + const m = setup({ + types: { + emitted: {} as + | { type: 'onClick'; x: number; y: number } + | { type: 'onChange' } + } + }).createMachine({}); + + const actor = createActor(m); + + actor.on('onClick', (ev) => { + ev.x satisfies number; + + // @ts-expect-error + ev.x satisfies string; + }); + + actor.on('onChange', () => {}); + + // @ts-expect-error + actor.on('unknown', () => {}); + }); +}); + it('should infer context type from `config.context` when there is no `schema.context`', () => { createMachine( { @@ -353,7 +379,20 @@ it('should not use actions as possible inference sites', () => { it('should work with generic context', () => { function createMachineWithExtras( context: TContext - ): StateMachine { + ): StateMachine< + TContext, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any + > { return createMachine({ context }); } @@ -493,6 +532,7 @@ describe('events', () => { any, any, any, + any, any > ) {} diff --git a/packages/xstate-graph/src/actorScope.ts b/packages/xstate-graph/src/actorScope.ts index a81213c69d..a1783a3654 100644 --- a/packages/xstate-graph/src/actorScope.ts +++ b/packages/xstate-graph/src/actorScope.ts @@ -9,6 +9,7 @@ export function createMockActorScope(): AnyActorScope { sessionId: Math.random().toString(32).slice(2), defer: () => {}, system: emptyActor.system, // TODO: mock system? - stopChild: () => {} + stopChild: () => {}, + emit: () => {} }; } diff --git a/packages/xstate-graph/src/simplePaths.ts b/packages/xstate-graph/src/simplePaths.ts index 1824beea9b..6a8a19659d 100644 --- a/packages/xstate-graph/src/simplePaths.ts +++ b/packages/xstate-graph/src/simplePaths.ts @@ -1,13 +1,4 @@ -import { - EventObject, - AnyStateMachine, - StateFrom, - EventFrom, - ActorLogic, - AnyActorLogic, - EventFromLogic, - SnapshotFrom -} from 'xstate'; +import { AnyActorLogic, EventFromLogic } from 'xstate'; import { SerializedEvent, SerializedState, @@ -16,7 +7,7 @@ import { TraversalOptions, VisitedContext } from './types'; -import { resolveTraversalOptions, createDefaultMachineOptions } from './graph'; +import { resolveTraversalOptions } from './graph'; import { getAdjacencyMap } from './adjacency'; import { alterPath } from './alterPath'; import { createMockActorScope } from './actorScope'; diff --git a/packages/xstate-react/src/useSelector.ts b/packages/xstate-react/src/useSelector.ts index 96de665a2b..e7ffcb7030 100644 --- a/packages/xstate-react/src/useSelector.ts +++ b/packages/xstate-react/src/useSelector.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'; -import { ActorRef, SnapshotFrom } from 'xstate'; +import { AnyActorRef, SnapshotFrom } from 'xstate'; type SyncExternalStoreSubscribe = Parameters< typeof useSyncExternalStoreWithSelector @@ -10,12 +10,10 @@ function defaultCompare(a: T, b: T) { return a === b; } -export function useSelector | undefined, T>( +export function useSelector( actor: TActor, selector: ( - emitted: TActor extends ActorRef - ? SnapshotFrom - : undefined + emitted: TActor extends AnyActorRef ? SnapshotFrom : undefined ) => T, compare: (a: T, b: T) => boolean = defaultCompare ): T { diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 22876a0d3a..036958f868 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -40,10 +40,11 @@ export interface TestMachineConfig< TODO, TODO, TODO, - TODO, - TODO, TODO, // delays TODO, // tags + TODO, // input + TODO, // output + TODO, //emitted TTypesMeta >; } @@ -60,6 +61,7 @@ export interface TestStateNodeConfig< ParameterizedObject, TODO, TODO, + TODO, TODO >, | 'type' diff --git a/packages/xstate-vue/README.md b/packages/xstate-vue/README.md index 86b675dacf..d4060df051 100644 --- a/packages/xstate-vue/README.md +++ b/packages/xstate-vue/README.md @@ -49,7 +49,7 @@ const { snapshot: state, send } = useMachine(toggleMachine);