diff --git a/.changeset/twelve-dingos-fold.md b/.changeset/twelve-dingos-fold.md new file mode 100644 index 0000000000..bd93b6bdc2 --- /dev/null +++ b/.changeset/twelve-dingos-fold.md @@ -0,0 +1,5 @@ +--- +'xstate': patch +--- + +Fixed an issue with actors not being reinstantiated correctly when an actor with the same ID was first stopped and then invoked/spawned again in the same microstep. diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 5c7c4491cc..c620fc9fbc 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -13,7 +13,8 @@ import { resolveMicroTransition, resolveStateValue, toState, - transitionNode + transitionNode, + setChildren } from './stateUtils'; import type { AreAllImplementationsAssumedToBeProvided, @@ -305,6 +306,8 @@ export class StateMachine< preInitial._initial = true; preInitial.actions.unshift(...actions); + setChildren(preInitial.children, actions); + return preInitial; } diff --git a/packages/core/src/actions/invoke.ts b/packages/core/src/actions/invoke.ts index f3c9c80297..67aa98aa18 100644 --- a/packages/core/src/actions/invoke.ts +++ b/packages/core/src/actions/invoke.ts @@ -62,10 +62,7 @@ export function invoke< type, params: { ...params, - id: params.id, - src: params.src, - ref: new ObservableActorRef(behavior, id), - meta + ref: new ObservableActorRef(behavior, id) } } as InvokeActionObject; } diff --git a/packages/core/src/actions/stop.ts b/packages/core/src/actions/stop.ts index 587c3bb4e3..b80a1a762e 100644 --- a/packages/core/src/actions/stop.ts +++ b/packages/core/src/actions/stop.ts @@ -32,14 +32,14 @@ export function stop< { actor }, - ({ params, type }, context, _event) => { - const actorRefOrString = isFunction(params.actor) + ({ params, type }, context, _event, { state }) => { + const actorRef = isFunction(params.actor) ? params.actor(context, _event.data) - : params.actor; + : state.children[params.actor]; return { type, - params: { actor: actorRefOrString } + params: { actor: actorRef } } as StopActionObject; } ); diff --git a/packages/core/src/interpreter.ts b/packages/core/src/interpreter.ts index 1a0abb76cb..07a8ac3a77 100644 --- a/packages/core/src/interpreter.ts +++ b/packages/core/src/interpreter.ts @@ -632,7 +632,7 @@ export class Interpreter< [actionTypes.cancel]: (_ctx, _e, { action }) => { this.cancel((action as CancelActionObject).params.sendId); }, - [actionTypes.invoke]: (_ctx, _e, { action, state }) => { + [actionTypes.invoke]: (_ctx, _e, { action }) => { const { id, autoForward, @@ -651,16 +651,9 @@ export class Interpreter< return; } ref._parent = this; // TODO: fix - // If the actor will be stopped right after it's started - // (such as in transient states) don't bother starting the actor. - if ( - state.actions.find((otherAction) => { - return ( - otherAction.type === actionTypes.stop && - (otherAction as StopActionObject).params.actor === id - ); - }) - ) { + // If the actor didn't end up being in the state + // (eg. going through transient states could stop it) don't bother starting the actor. + if (!this.state.children[id]) { return; } try { @@ -668,9 +661,6 @@ export class Interpreter< this.forwardTo.add(id); } - // TODO: determine how this can be immutably updated - this.state.children[id] = ref; - ref.start?.(); } catch (err) { this.send(error(id, err)); @@ -679,11 +669,9 @@ export class Interpreter< }, [actionTypes.stop]: (_ctx, _e, { action }) => { const { actor } = (action as StopActionObject).params; - const actorRef = - typeof actor === 'string' ? this.state.children[actor] : actor; - if (actorRef) { - this.stopChild(actorRef.name); + if (actor) { + this.stopChild(actor); } }, [actionTypes.log]: (_ctx, _e, { action }) => { @@ -746,16 +734,8 @@ export class Interpreter< return undefined; } - private stopChild(childId: string): void { - const child = this.state.children[childId]; - if (!child) { - return; - } - - this.forwardTo.delete(childId); - // TODO: determine how this can be immutably updated - delete this.state.children[childId]; - + private stopChild(child: ActorRef): void { + this.forwardTo.delete(child.name); if (isFunction(child.stop)) { child.stop(); } diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 8d58d4197a..ccbae36a53 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -16,6 +16,7 @@ import { BaseActionObject, EventObject, InvokeActionObject, + StopActionObject, StateValue, TransitionConfig, TransitionDefinition, @@ -1588,8 +1589,6 @@ export function resolveMicroTransition< return inertState as any; } - const children = { ...currentState.children }; - const resolvedConfiguration = willTransition ? Array.from(resolved.configuration) : !currentState._initial @@ -1605,6 +1604,9 @@ export function resolveMicroTransition< const { context, actions: nonRaisedActions } = resolved; + const children = { ...currentState.children }; + setChildren(children, nonRaisedActions); + const nextState = machine.createState({ value: getStateValue(machine.root, resolved.configuration), context, @@ -1627,7 +1629,14 @@ export function resolveMicroTransition< context !== currentState.context; nextState._internalQueue = resolved.internalQueue; - nextState.actions.forEach((action) => { + return nextState; +} + +export function setChildren< + TContext extends MachineContext, + TEvent extends EventObject +>(children: State['children'], actions: BaseActionObject[]) { + actions.forEach((action) => { if ( action.type === actionTypes.invoke && (action as InvokeActionObject).params.ref @@ -1636,10 +1645,13 @@ export function resolveMicroTransition< if (ref) { children[ref.name] = ref; } + } else if (action.type === actionTypes.stop) { + const ref = (action as StopActionObject).params.actor; + if (ref) { + delete children[ref.name]; + } } }); - - return nextState; } function resolveActionsAndContext< diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index aeb7266b7d..dece249e31 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1209,7 +1209,7 @@ export interface DynamicStopActionObject< export interface StopActionObject { type: ActionTypes.Stop; params: { - actor: string | ActorRef; + actor: ActorRef; }; } diff --git a/packages/core/test/invoke.test.ts b/packages/core/test/invoke.test.ts index ef3489b1a4..dd25d19e45 100644 --- a/packages/core/test/invoke.test.ts +++ b/packages/core/test/invoke.test.ts @@ -3117,6 +3117,35 @@ describe('invoke', () => { done(); }, 100); }); + + it('should get reinstantiated after reentering the invoking state in a microstep', () => { + let invokeCount = 0; + + const machine = createMachine({ + initial: 'a', + states: { + a: { + invoke: { + src: () => + fromCallback(() => { + invokeCount++; + }) + }, + on: { + GO_AWAY_AND_REENTER: 'b' + } + }, + b: { + always: 'a' + } + } + }); + const service = interpret(machine).start(); + + service.send({ type: 'GO_AWAY_AND_REENTER' }); + + expect(invokeCount).toBe(2); + }); }); describe('actors option', () => {