Skip to content

Commit

Permalink
Remove State['changed'] (#4083)
Browse files Browse the repository at this point in the history
* Remove `State['changed']`

* avoid passing resolved actions around

* fixed type error

* fixed a thing

* Create light-spiders-attend.md
  • Loading branch information
Andarist committed Jun 20, 2023
1 parent 8ae5c34 commit 1635285
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 297 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-spiders-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"xstate": patch
---

Remove `State['changed']`. A new instance of `State` is being created if there are matching transitions for the received event. If there are no matching transitions then the current state is being returned.
9 changes: 0 additions & 9 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,6 @@ export class State<
public historyValue: Readonly<HistoryValue<TContext, TEvent>> = {};
public _internalQueue: Array<TEvent>;
public _initial: boolean = false;
/**
* Indicates whether the state has changed from the previous state. A state is considered "changed" if:
*
* - Its value is not equal to its previous value, or:
* - It has any new actions (side-effects) to execute.
*
* An initial state (with no history) will return `undefined`.
*/
public changed: boolean | undefined;
/**
* The enabled state nodes representative of the state value.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export class StateMachine<
preInitial._initial = true;

if (actorCtx) {
const [nextState] = resolveActionsAndContext(
const nextState = resolveActionsAndContext(
actions,
initEvent as TEvent,
preInitial,
Expand Down
59 changes: 19 additions & 40 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,13 +1039,10 @@ export function microstep<
const mutConfiguration = new Set(currentState.configuration);

if (!currentState._initial && !willTransition) {
const inertState = cloneState(currentState, {});

inertState.changed = false;
return inertState;
return currentState;
}

const [microstate, actions] = microstepProcedure(
const microstate = microstepProcedure(
currentState._initial
? [
{
Expand All @@ -1064,19 +1061,9 @@ export function microstep<
actorCtx
);

const { context } = microstate;

const nextState = cloneState(microstate, {
return cloneState(microstate, {
value: {} // TODO: make optional
});

nextState.changed = currentState._initial
? undefined
: !stateValuesEqual(nextState.value, currentState.value) ||
actions.length > 0 ||
context !== currentState.context;

return nextState;
}

function microstepProcedure(
Expand All @@ -1085,7 +1072,7 @@ function microstepProcedure(
mutConfiguration: Set<AnyStateNode>,
event: AnyEventObject,
actorCtx: AnyActorContext
): [typeof currentState, BaseActionObject[]] {
): typeof currentState {
const actions: BaseActionObject[] = [];
const historyValue = {
...currentState.historyValue
Expand Down Expand Up @@ -1130,7 +1117,7 @@ function microstepProcedure(
}

try {
const [nextState, resolvedActions] = resolveActionsAndContext(
const nextState = resolveActionsAndContext(
actions,
event,
currentState,
Expand All @@ -1143,18 +1130,15 @@ function microstepProcedure(

internalQueue.push(...nextState._internalQueue);

return [
cloneState(currentState, {
configuration: nextConfiguration,
historyValue,
_internalQueue: internalQueue,
context: nextState.context,
done,
output,
children: nextState.children
}),
resolvedActions
];
return cloneState(currentState, {
configuration: nextConfiguration,
historyValue,
_internalQueue: internalQueue,
context: nextState.context,
done,
output,
children: nextState.children
});
} catch (e) {
// TODO: Refactor this once proper error handling is implemented.
// See https://github.com/statelyai/rfcs/pull/4
Expand Down Expand Up @@ -1434,14 +1418,12 @@ export function resolveActionsAndContext<
event: TEvent,
currentState: State<TContext, TEvent, any>,
actorCtx: AnyActorContext | undefined
): [AnyState, BaseActionObject[]] {
): AnyState {
const { machine } = currentState;
const resolvedActions: BaseActionObject[] = [];
const raiseActions: Array<RaiseActionObject<TContext, TEvent>> = [];
let intermediateState = currentState;

function handleAction(action: BaseActionObject): void {
resolvedActions.push(action);
if (actorCtx?.self.status === ActorStatus.Running) {
action.execute?.(actorCtx!);
} else {
Expand Down Expand Up @@ -1494,12 +1476,9 @@ export function resolveActionsAndContext<
resolveAction(actionObject);
}

return [
cloneState(intermediateState, {
_internalQueue: raiseActions.map((a) => a.params.event)
}),
resolvedActions
];
return cloneState(intermediateState, {
_internalQueue: raiseActions.map((a) => a.params.event)
});
}

export function macrostep(
Expand All @@ -1519,7 +1498,7 @@ export function macrostep(

// Handle stop event
if (event.type === stopSignalType) {
nextState = stopStep(event, nextState, actorCtx)[0];
nextState = stopStep(event, nextState, actorCtx);
states.push(nextState);

return {
Expand Down
12 changes: 8 additions & 4 deletions packages/core/test/deterministic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,14 @@ describe('deterministic machine', () => {

const actor = interpret(machine).start();

const previousSnapshot = actor.getSnapshot();

actor.send({
type: 'FAKE'
});

expect(actor.getSnapshot().value).toBe('a');
expect(actor.getSnapshot().changed).toBe(false);
expect(actor.getSnapshot()).toBe(previousSnapshot);
});

it('should throw an error if not given an event', () => {
Expand Down Expand Up @@ -232,12 +234,14 @@ describe('deterministic machine', () => {

const actor = interpret(machine).start();

const previousSnapshot = actor.getSnapshot();

actor.send({
type: 'FAKE'
});

expect(actor.getSnapshot().value).toEqual({ a: 'b' });
expect(actor.getSnapshot().changed).toBe(false);
expect(actor.getSnapshot()).toBe(previousSnapshot);
});

it('should transition to the deepest initial state', () => {
Expand All @@ -254,7 +258,7 @@ describe('deterministic machine', () => {
});
});

it('should return the equivalent state if no transition occurs', () => {
it('should return the same state if no transition occurs', () => {
const initialState = lightMachine.transition(
lightMachine.getInitialState(
undefined as any // TODO: figure out the simulation API
Expand All @@ -273,7 +277,7 @@ describe('deterministic machine', () => {
);

expect(initialState.value).toEqual(nextState.value);
expect(nextState.changed).toBe(false);
expect(nextState).toBe(initialState);
});
});

Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/examples/6.16.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ describe('Example 6.16', () => {
});

const expected: Record<string, Record<string, StateValue | undefined>> = {
'{"A":"D", "B":"F"}': {
'{"A":"D","B":"F"}': {
1: { A: 'C', B: 'E' },
2: undefined,
'1, 5, 3': { A: 'C', B: 'F' }
},
'{"A":"C", "B":"E"}': {
'{"A":"C","B":"E"}': {
1: undefined,
2: { A: 'D', B: 'E' },
5: { A: 'C', B: 'G' }
},
'{"A":"C", "B":"G"}': {
'{"A":"C","B":"G"}': {
1: undefined,
2: undefined,
3: { A: 'C', B: 'F' }
Expand Down
83 changes: 49 additions & 34 deletions packages/core/test/interpreter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,54 +458,72 @@ describe('interpreter', () => {
});

describe('activities (deprecated)', () => {
let activityState = 'off';
it('should start activities', () => {
const spy = jest.fn();

const activityMachine = createMachine(
{
id: 'activity',
initial: 'on',
states: {
on: {
invoke: 'myActivity',
const activityMachine = createMachine(
{
id: 'activity',
initial: 'on',
states: {
on: {
TURN_OFF: 'off'
}
},
off: {}
}
},
{
actors: {
myActivity: fromCallback(() => {
activityState = 'on';
return () => (activityState = 'off');
})
invoke: 'myActivity',
on: {
TURN_OFF: 'off'
}
},
off: {}
}
},
{
actors: {
myActivity: fromCallback(spy)
}
}
}
);

it('should start activities', () => {
);
const service = interpret(activityMachine);

service.start();

expect(activityState).toEqual('on');
expect(spy).toHaveBeenCalled();
});

it('should stop activities', () => {
const spy = jest.fn();

const activityMachine = createMachine(
{
id: 'activity',
initial: 'on',
states: {
on: {
invoke: 'myActivity',
on: {
TURN_OFF: 'off'
}
},
off: {}
}
},
{
actors: {
myActivity: fromCallback(() => spy)
}
}
);
const service = interpret(activityMachine);

service.start();

expect(activityState).toEqual('on');
expect(spy).not.toHaveBeenCalled();

service.send({ type: 'TURN_OFF' });

expect(activityState).toEqual('off');
expect(spy).toHaveBeenCalled();
});

it('should stop activities upon stopping the service', () => {
let stopActivityState: string;
const spy = jest.fn();

const stopActivityMachine = createMachine(
{
Expand All @@ -523,21 +541,18 @@ describe('interpreter', () => {
},
{
actors: {
myActivity: fromCallback(() => {
stopActivityState = 'on';
return () => (stopActivityState = 'off');
})
myActivity: fromCallback(() => spy)
}
}
);

const stopActivityService = interpret(stopActivityMachine).start();

expect(stopActivityState!).toEqual('on');
expect(spy).not.toHaveBeenCalled();

stopActivityService.stop();

expect(stopActivityState!).toEqual('off');
expect(spy).toHaveBeenCalled();
});

it('should restart activities from a compound state', () => {
Expand Down
Loading

0 comments on commit 1635285

Please sign in to comment.