statelyai / xstate

Actor-based state management & orchestration for complex app logic.
https://stately.ai/docs
MIT License
27.18k stars 1.26k forks source link

start from a persisted state: actions runned abusively? #2109

Closed christophemacabiau closed 3 years ago

christophemacabiau commented 3 years ago

Description I dont know if it's a bug in xstate, or in me. I guess it's for me. When I restart a machine from a persisted state, the machine seems to restart from the initial state :

import { Machine } from 'xstate';

export interface OrderStateSchema {
  states: {
    start: {},
    created: {},
  }
};

export interface OrderContext {
  id: String,
};

const MachineTest = Machine<OrderContext, OrderStateSchema>(
  {
    initial: 'start',
    context: { id: null },
    states: {
      start: {
        entry: ['start'],
        always: { target: 'created' },
      },
      created: {
        entry: ['created'],
      },
    }
  },
  {
    actions: {
      start: function (context, event) { console.log('START'); },
      created: function (context, event) { console.log('CREATED'); },
    },
  },
);

export { MachineTest };

When I run it with :

const machine1 = interpret(MachineTest.withContext({ id: 0 }));
machine1.onTransition(function (state) {
  console.log('Machine 1, State:', state.value);
});
machine1.start();
const persistedState = JSON.stringify(machine1.state);

const machine2 = interpret(MachineTest);
machine2.onTransition(function (state) {
  console.log('Machine 2, State:', state.value);
});
machine2.start(MachineTest.resolveState(State.create(JSON.parse(persistedState))))

Expected Result I expect machine2 to start from 'created'

START
CREATED
Machine 1, State: created
CREATED
Machine 2, State: created

I am even not sure that the entry action of created should be run another time?

Actual Result but machine2 seems to start from 'start'

START
CREATED
Machine 1, State: created
START
CREATED
Machine 2, State: created

Reproduction

Additional context

bradwoods commented 3 years ago

Here is an example that might help - https://examples.bradwoods.io/xstate/persisting-state

christophemacabiau commented 3 years ago

Thanks @bradwoods, but it is not clear for me

With this machine:

import { interpret, Machine, State } from 'xstate';

const MachineTest = Machine(
  {
    initial: 'start',
    context: { id: null },
    states: {
      start: {
        entry: ['startentry'],
        always: { target: 'created', actions: ['starttransition'] },
        exit: ['startexit'],
      },
      created: {
        entry: ['created'],
        on: { 'end': 'end' }
      },
      end: { entry: ['end'] }
    }
  },
  {
    actions: {
      startentry: function (context, event) { console.log(`${context.id}, Action: start entry`); },
      startexit: function (context, event) { console.log(`${context.id}, Action: start exit`); },
      starttransition: function (context, event) { console.log(`${context.id}, Action: start transition`); },
      created: function (context, event) { console.log(`${context.id}, Action: created`); },
      end: function (context, event) { console.log(`${context.id}, Action: end`); },
    },
  },
);

let persistedState: string;

const machine1 = interpret(MachineTest.withContext({ id: 'Machine 1' }));
machine1.onTransition(function (state) {
  console.log('Machine 1, State:', state.value);
});

machine1.onStop(function () {
  console.log('Machine 1, onStop');
  console.log('Starting Machine2');
  const machine2 = interpret(MachineTest.withContext({ id: 'Machine 2' }));
  machine2.onTransition(function (state) {
    console.log('Machine 2, State:', state.value);
  });
  machine2.start(MachineTest.resolveState(State.create(JSON.parse(persistedState))));
});

machine1.start();
persistedState = JSON.stringify(machine1.state);
machine1.send('end');
machine1.stop();

The output is:

Machine 1, Action: start entry
Machine 1, Action: start exit
Machine 1, Action: start transition
Machine 1, Action: created
Machine 1, State: created
Machine 1, Action: end
Machine 1, State: end
Machine 1, onStop
Starting Machine2
Machine 1, Action: start entry
Machine 1, Action: start exit
Machine 1, Action: start transition
Machine 1, Action: created
Machine 2, State: created

It's very strange because all the actions of machine1, which is stopped, are run another time when running machine2. I don't understand why, and it is problematic, because these actions are side-effects, and I don't want to run them another time.

Does someone has an explanation? Is there something wrong in my code? A workaround?

Thanks for your help!

christophemacabiau commented 3 years ago

@davidkpiano @Andarist do you have some hint/lead?

christophemacabiau commented 3 years ago

Solution found here https://github.com/davidkpiano/xstate/discussions/1732 This might be integrated in the doc? I can do a PR if interested

davidkpiano commented 3 years ago

Solution found here #1732 This might be integrated in the doc? I can do a PR if interested

PRs always welcome!