javascript-state-machine icon indicating copy to clipboard operation
javascript-state-machine copied to clipboard

save a state machine to json

Open qianL93 opened this issue 7 years ago • 6 comments

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

qianL93 avatar Jun 22 '17 02:06 qianL93

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.

mikkelking avatar Sep 17 '17 12:09 mikkelking

(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?

dooreelko avatar Nov 10 '17 21:11 dooreelko

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;
}

aitorllj93 avatar Mar 13 '18 12:03 aitorllj93

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?

rickbsgu avatar Mar 13 '18 14:03 rickbsgu

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)

aitorllj93 avatar Mar 16 '18 11:03 aitorllj93

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' });

cilindrox avatar Mar 26 '20 19:03 cilindrox