Skip to content

Commit

Permalink
Fixed an infinite loop when initially spawned actor responded synchro…
Browse files Browse the repository at this point in the history
…nously to its parent
  • Loading branch information
Andarist committed Jan 17, 2022
1 parent 56842cb commit e9f3f07
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-onions-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': patch
---

Fixed an infinite loop when initially spawned actor (in an initial context) responded synchronously to its parent.
24 changes: 14 additions & 10 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,12 @@ class StateNode<

stateTransition.configuration = [...resolvedConfig];

return this.resolveTransition(stateTransition, currentState, _event);
return this.resolveTransition(
stateTransition,
currentState,
currentState.context,
_event
);
}

private resolveRaisedTransition(
Expand All @@ -1151,9 +1156,9 @@ class StateNode<

private resolveTransition(
stateTransition: StateTransition<TContext, TEvent>,
currentState?: State<TContext, TEvent, any, any>,
_event: SCXML.Event<TEvent> = initEvent as SCXML.Event<TEvent>,
context: TContext = this.machine.context
currentState: State<TContext, TEvent, any, any> | undefined,
context: TContext,
_event: SCXML.Event<TEvent> = initEvent as SCXML.Event<TEvent>
): State<TContext, TEvent, TStateSchema, TTypestate> {
const { configuration } = stateTransition;
// Transition will "apply" if:
Expand All @@ -1171,10 +1176,9 @@ class StateNode<
? (this.machine.historyValue(currentState.value) as HistoryValue)
: undefined
: undefined;
const currentContext = currentState ? currentState.context : context;
const actions = this.getActions(
stateTransition,
currentContext,
context,
_event,
currentState
);
Expand All @@ -1192,7 +1196,7 @@ class StateNode<
const [resolvedActions, updatedContext] = resolveActions(
this,
currentState,
currentContext,
context,
_event,
actions,
this.machine.config.preserveActionOrder
Expand Down Expand Up @@ -1275,7 +1279,7 @@ class StateNode<
machine: this
});

const didUpdateContext = currentContext !== updatedContext;
const didUpdateContext = context !== updatedContext;

nextState.changed = _event.name === actionTypes.update || didUpdateContext;

Expand Down Expand Up @@ -1554,8 +1558,8 @@ class StateNode<
actions: []
},
undefined,
undefined,
context
context ?? this.machine.context,
undefined
);
}

Expand Down
118 changes: 78 additions & 40 deletions packages/core/test/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,84 @@ describe('actors', () => {
.start();
});

it('should only spawn an actor in an initial state of a child that gets invoked in the initial state of a parent when the parent gets started', () => {
let spawnCounter = 0;

interface TestContext {
promise?: ActorRefFrom<Promise<string>>;
}

const child = Machine<TestContext>({
initial: 'bar',
context: {},
states: {
bar: {
entry: assign<TestContext>({
promise: () => {
return spawn(() => {
spawnCounter++;
return Promise.resolve('answer');
});
}
})
}
}
});

const parent = Machine({
initial: 'foo',
states: {
foo: {
invoke: {
src: child,
onDone: 'end'
}
},
end: { type: 'final' }
}
});
interpret(parent).start();
expect(spawnCounter).toBe(1);
});

// https://github.com/statelyai/xstate/issues/2565
it('should only spawn an initial actor once when it synchronously responds with an event', () => {
let spawnCalled = 0;
const anotherMachine = createMachine({
initial: 'hello',
states: {
hello: {
entry: sendParent('ping')
}
}
});

const testMachine = createMachine<{ ref: ActorRef<any> }>({
initial: 'testing',
context: () => {
spawnCalled++;
// throw in case of an infinite loop
expect(spawnCalled).toBe(1);
return {
ref: spawn(anotherMachine)
};
},
states: {
testing: {
on: {
ping: {
target: 'done'
}
}
},
done: {}
}
});

const service = interpret(testMachine).start();
expect(service.state.value).toEqual('done');
});

it('should spawn null actors if not used within a service', () => {
interface TestContext {
ref?: ActorRef<any>;
Expand Down Expand Up @@ -846,46 +924,6 @@ describe('actors', () => {
})
.start();
});

it('should only spawn an actor in an initial state of a child that gets invoked in the initial state of a parent when the parent gets started', () => {
let spawnCounter = 0;

interface TestContext {
promise?: ActorRefFrom<Promise<string>>;
}

const child = Machine<TestContext>({
initial: 'bar',
context: {},
states: {
bar: {
entry: assign<TestContext>({
promise: () => {
return spawn(() => {
spawnCounter++;
return Promise.resolve('answer');
});
}
})
}
}
});

const parent = Machine({
initial: 'foo',
states: {
foo: {
invoke: {
src: child,
onDone: 'end'
}
},
end: { type: 'final' }
}
});
interpret(parent).start();
expect(spawnCounter).toBe(1);
});
});
});

Expand Down

0 comments on commit e9f3f07

Please sign in to comment.