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

Calling a new transition from inside a lifecycle method

Open reubenab opened this issue 7 years ago • 8 comments

If I want to decide between two transitions on entering a state, I am unable to call a transition from inside any lifecycle event. Say I have a very simple FSM with 3 states: A, B, and C. There is a transition A->B called step1() and another transition B->C called step2(). Say I only want to print hello when we get to B then call step2(). How would this be achieved? If I write an onEnterB or onAfterStep1 function, I get an error saying that a new transition cannot be called while the old one is still in effect.

reubenab avatar Nov 08 '17 22:11 reubenab

You can't transition while in a transition. What i do is pass an object as a param, and the current transition sets a 'nextTransition' value in it, which i feed back to the state machine after the first transition is over.

On 9 Nov 2017 0:19, "Reuben Abraham" [email protected] wrote:

If I want to decide between two transitions on entering a state, I am unable to call a transition from inside any lifecycle event. Say I have a very simple FSM with 3 states: A, B, and C. There is a transition A->B called step1() and another transition B->C called step2(). Say I only want to print hello when we get to B then call step2(). How would this be achieved? If I write an onEnterB or onAfterStep1 function, I get an error saying that a new transition cannot be called while the old one is still in effect.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/jakesgordon/javascript-state-machine/issues/143, or mute the thread https://github.com/notifications/unsubscribe-auth/ACE68CjEmCed4ebsAwRRypxU94Ll9cTvks5s0ikAgaJpZM4QXIcP .

adami avatar Nov 08 '17 23:11 adami

Pretty much any event system will choke if you try to invoke an event within an event.

The typical solution is to back out to the event loop (let the current event complete) before invoking the internal event. You can do this with either a custom event, or a timeout.

A 0 length timeout is the easiest:

setTimeout(function() {
  fsm.newTrans() } , 0
)}

rickbsgu avatar Nov 09 '17 00:11 rickbsgu

I've been doing process.nextTick(() => transition()) and it seems to be working fine.

adamworrall avatar Dec 29 '17 08:12 adamworrall

Even though that the workaround will do it`s job, if before the execution of process.nextTick you go for some reason into another transition you will have a race condition and undefined state in the end.

For example A(init) -> calls (B) -> process.nextTick to (C) still on same tick but another piece of the code checks if Current State (B) -> process.nextTick move to (D)

That move to D should fail because you are already scheduled async transition to (C), but there is no way for the fsm to know this. Ideally you want to replicate the lock mechanism...

telemmaite avatar Feb 05 '18 10:02 telemmaite

Update 11-7-2018

Reads even cleaner with async / await

const fsm = new StateMachine({
      init: "init",
      transitions: [
        { name: "step", from: "init", to: "dbWrite" },
        { name: "step", from: "dbWrite", to: "submitToApi" },
        { name: "step", from: "submitToApi", to: "imageUpload" },
        { name: "step", from: "imageUpload", to: "success" }
      ],
      methods: {
        onDbWrite: async () => {
          await this.onDbWrite();
          setTimeout(() => this.fsm.step(), 0);
        },
        onSubmitToApi: async () => {
          await this.onSubmitTo311Api();
          setTimeout(() => this.fsm.step(), 0);
        },
        onImageUpload: async () => {
          await this.onImageUpload();
          setTimeout(() => this.fsm.step(), 0);
        },
        onSuccess: async () => {
          await this.onSuccess();
        }
      }
    });

Got multiple state transitions working async.

Could just be my ignorance but it doesn't seem like it would be too crazy to support in the library

const fsm = new StateMachine({
      init: "init",
      transitions: [
        { name: "step", from: "init", to: "dbWrite" },
        { name: "step", from: "dbWrite", to: "submitToApi" },
        { name: "step", from: "submitToApi", to: "imageUpload" },
        { name: "step", from: "imageUpload", to: "success" }
      ],
      methods: {
        onTransition: lifecycle => this.onTransition(lifecycle),
        onDbWrite: () =>
          this.onDbWrite().then(() => setTimeout(() => this.fsm.step(), 0)),
        onSubmitToApi: () =>
          this.onSubmitToApi().then(() => setTimeout(() => this.fsm.step(), 0)),
        onImageUpload: () =>
          this.onImageUpload().then(() => setTimeout(() => this.fsm.step(), 0)),
        onSuccess: () => this.onSuccess()
      }
    });

My fsm lives inside of a class with member functions that are async await or return promises.

Seems like a really neat way of implementing lifecycle hooks

williscool avatar Oct 16 '18 02:10 williscool

Update 11-7-2018

Reads even cleaner with async / await

const fsm = new StateMachine({
      init: "init",
      transitions: [
        { name: "step", from: "init", to: "dbWrite" },
        { name: "step", from: "dbWrite", to: "submitToApi" },
        { name: "step", from: "submitToApi", to: "imageUpload" },
        { name: "step", from: "imageUpload", to: "success" }
      ],
      methods: {
        onDbWrite: async () => {
          await this.onDbWrite();
          setTimeout(() => this.fsm.step(), 0);
        },
        onSubmitToApi: async () => {
          await this.onSubmitTo311Api();
          setTimeout(() => this.fsm.step(), 0);
        },
        onImageUpload: async () => {
          await this.onImageUpload();
          setTimeout(() => this.fsm.step(), 0);
        },
        onSuccess: async () => {
          await this.onSuccess();
        }
      }
    });

Got multiple state transitions working async.

Could just be my ignorance but it doesn't seem like it would be too crazy to support in the library

const fsm = new StateMachine({
      init: "init",
      transitions: [
        { name: "step", from: "init", to: "dbWrite" },
        { name: "step", from: "dbWrite", to: "submitToApi" },
        { name: "step", from: "submitToApi", to: "imageUpload" },
        { name: "step", from: "imageUpload", to: "success" }
      ],
      methods: {
        onTransition: lifecycle => this.onTransition(lifecycle),
        onDbWrite: () =>
          this.onDbWrite().then(() => setTimeout(() => this.fsm.step(), 0)),
        onSubmitToApi: () =>
          this.onSubmitToApi().then(() => setTimeout(() => this.fsm.step(), 0)),
        onImageUpload: () =>
          this.onImageUpload().then(() => setTimeout(() => this.fsm.step(), 0)),
        onSuccess: () => this.onSuccess()
      }
    });

My fsm lives inside of a class with member functions that are async await or return promises.

Seems like a really neat way of implementing lifecycle hooks

Will this approach have any other effects or disadvantages?

pmmcoder avatar Nov 27 '18 10:11 pmmcoder

Hi,

I could solve this problem calling the transtition inside the method writing:

setTimeout(() => fsm.step(),0);

If I wrote setTimeout(fsm.step(),0) I get the error. I don´t know the difference, but using => solves the problem.

germancmartinez avatar Mar 08 '19 13:03 germancmartinez

Maybe creator of the lib can provide an example how to make state machine which changes its state from first to last (without setTimeout) when previous transition is finished? A -> B -> C. I guess this is pretty often case.

vsorrokin avatar Jan 21 '21 12:01 vsorrokin