jakesgordon / javascript-state-machine

A javascript finite state machine library
MIT License
8.69k stars 964 forks source link

save a state machine to json #120

Open qianL93 opened 7 years ago

qianL93 commented 7 years ago

save a state machine to json, and recover it from json. it will be useful.

mikkelking commented 7 years ago

I think this would be useful, I am looking for a way to run multiple state machines that have long life times and will need to be persisted to the database.

So an incoming event would first retrieve the state machine from the database, and then the event handler would be invoked, actions taken, state updated, and then saved to the database again.

I think this is something achievable, and I would be willing to either make the changes or help with them (subject to some discussion of course). Please let me know if you think this is a useful feature.

I used v2 of the FSM on a previous project and am familiar with the v2 code, because I made a couple of changes for my own purposes.

dooreelko commented 6 years ago

(Thinking out loud) If you don't need history, then the solution is rather simple: on each transition persist jsm.state. After restoring the state machine, use that state from the database as the 'init' one. A little more work if you want to have interactive history, but i believe still possible.

What do you think?

aitorllj93 commented 6 years ago

Hi, here's the solution I did to work with history without jumping (jump triggers events and breaks history).

First, store your machine state json. I use concat to clone array and remove references.

function storeState(fsm) {
  return  {
    currentState: fsm.state,
    history: fsm.history.concat([]),
    future: fsm.future.concat([])
  };
}

For restoring the machine you must initialize it with currentState and populate history and future arrays:

function restoreFromState(state) {
  const fsm = new StateMachine({
    init: state.currentState || 'defaultState',
    ...,
    plugins: [ new stateMachineHistory() ]
  };

  if (state.history) {
    // clean history
    fsm.history.splice(0, fsm.history.length);
    // add items from json
    state.history.forEach((item: string) => fsm.history.push(item));
  }

  if (state.future) {
    // clean future
    fsm.future.splice(0, fsm.future.length);
    // add items from json
    state.future.forEach((item: string) => fsm.future.push(item));
  }

  return fsm;
}
rickbsgu commented 6 years ago

Nice. What about application data that needs to be present for a given state?

Something I had thought about was recording state transitions with app data, and then playing back that recording from the start state. I implemented an undo once that way that seemed to work pretty well.

Looks like you're doing that in the last .forEach loop, which guarantees that state transitions are preserved. But app data doesn't come along?

aitorllj93 commented 6 years ago

Hi, @rickbsgu

I have not used data or methods yet, but you should be able to use data like this:

function restoreFromState(state) {
  const fsm = new StateMachine({
    init: state.currentState || 'defaultState',
    data: state.data // keep reference or
    data: JSON.parse(JSON.stringify(state.data)) // make a clone with JSON or
    data: _.cloneDeep(state.data) // make a deep clone with lodash
    ...,
    plugins: [ new stateMachineHistory() ]
  };

  if (state.history) {
    // clean history
    fsm.history.splice(0, fsm.history.length);
    // add items from json
    state.history.forEach((item: string) => fsm.history.push(item));
  }

  if (state.future) {
    // clean future
    fsm.future.splice(0, fsm.future.length);
    // add items from json
    state.future.forEach((item: string) => fsm.future.push(item));
  }

  return fsm;
}

function storeState(fsm) {
  return  {
    currentState: fsm.state,
    data: {
      color: fsm.color
    },
    history: fsm.history.concat([]),
    future: fsm.future.concat([])
  };
}

The cloning strategy it's up to you and your needs.

If you're working with factories and data as method you should do something like this

function restoreFromState(state) {
  const FSM = new StateMachine({
    init: state.currentState || 'defaultState',
    data: function(color) {
     return {
        color
     }
   },
    ...,
    plugins: [ new stateMachineHistory() ]
  };

  const fsm = new FSM(state.data.color);

  if (state.history) {
    // clean history
    fsm.history.splice(0, fsm.history.length);
    // add items from json
    state.history.forEach((item: string) => fsm.history.push(item));
  }

  if (state.future) {
    // clean future
    fsm.future.splice(0, fsm.future.length);
    // add items from json
    state.future.forEach((item: string) => fsm.future.push(item));
  }

  return fsm;
}

I've not worked with factories and I'm sure it could be done not creating a factory every time you call restoreFromState, but I think the example solves your question. In JavaScript almost everything can be overwritten, just keep in mind the cloneOrKeepReference paradigm and make sure you are cloning when you need throw away references and you are not overriding references when you need to keep them (like in history & future restore process)

cilindrox commented 4 years ago

Leaving this here for future reference - but using the factory approach you can configure the _fsm property before initializing your object, so there's no need to return multiple factory wrappers, eg:

// factory.js
const internals = {};

internals.MyClass = class {

    constructor(options = {}) {

        this._fsm.config.configureInitTransition(options.state); // Set the state to an arbitrary init state
        this._fsm();
        this.createdAt = this.updatedAt = new Date();
    }
};

module.exports = internals.Factory = Fsm.factory(internals.MyClass, {
    init: 'someState',
    transitions: [ ... ],
    methods: { ... }
});

// foo.js

const MyClass = require('factory');

const instance = new MyClass({ state: 'somethingElse' });