Xstate: Support delayed events

Created on 5 Jan 2018  Â·  25Comments  Â·  Source: davidkpiano/xstate

Hi. One of the things I like about statecharts is to deal with delaying events and so on, introducing small wait states that process events differently.

Since xstate is a pure functional thing it doesn't make sense to support delayed events per se, but it would be mighty useful if it were possible to

  1. annotate a state with a delayed event
  2. show (with an example) how one might code it up (using setTimeout)

One thing is that one would have to know if an event actually exited a state, since exiting a state should cancel the associated timer.

I suggest something a lot simpler than scxml (which doesn't have this built in) (yaml format)

states:
  searching:
    on:
      results: displaying_results
    onEntry: startHttpRequest
    onExit: cancelHttpRequest
    states:
      silent:
        after: 2s warning
      warning:
        onEntry: showWarning

or something like that. It's important that the timeout doesn't need a name, just a target.

As a user of this I would have to know that a transition _to_ searching state should start a timer, so I can act accordingly, but I also then need to know when I handle an event if I actually exited the searching state at all. Today this might be visible in the returned state's "history", but I would have to inspect the machine myself to know if the new state / historical state have a common ancestor.

E.g. in the state machine above, if I'm in the "searching" "silent" state then an event might _exit_ silent, and I would have to know that this happened, and I think it's hard to infer from the current response

enhancement

Most helpful comment

I think "regular events" should be declared as part of the statechart itself:

clock: {
  onEntry: {
    type: 'xstate.raise', value: 'TICK'
  },
  after: {
    1000: 'clock'
  }
}

Declare some state that upon exit has a delayed transition to itself, and upon entry raises an event.

Benefits:

  • It becomes apparent from the statechart where the "tick" events come from
  • It also allows different "clocks" that tick around the statechart
  • It allows clocks to be active and stop being active if they ever leave the state
  • It allows synchronizing the start of the "ticks" with external events, so e.g. you can start ticking when an event occurs (by entering the clock state).

All 25 comments

Currently, if you have something that understands some timer_* action, the above can be translated to: (contrived example)

# ...
states:
  silent:
    onEntry: timer_2s_from_silent
    on:
      timer_2s_from_silent_elapsed: warning
      foo: somethingElse
  warning:
    onEntry: showWarning
  somethingElse:
    # ...

That way, if you transition to silent, the timer_2s_from_silent action will set a timer that fires timer_2s_from_silent_elapsed. If still on the silent state, it will transition to warning. If not (e.g., somethingElse), that state doesn't handle timer_2s_from_silent_elapsed anyway, so nothing will happen.

That definitely gets complicated though, especially if you want to be able to cancel the timer or handle multiple timers.

Another idea (which I'm planning on doing) is adding the feature of specifying actions as _objects_ instead of just strings. This can make it possible to easily model <send> in SCXML:

{
  silent: {
    onEntry: { type: 'SEND', event: 'TIMER', delay: 2000 }
    // ...
  },
  // ...
}

Which of course can be abstracted into a function:

{
  silent: {
    onEntry: send('TIMER', 2000)
  }
}

This would fit best into reactive scenarios, and I'm planning on making demos with RxJS to show how this can be achieved.

The workaround indeed becomes more verbose because it is in fact important to cancel the timeout when it exits, otherwise we'd end up with the same bugs as normal timeouts. My suggested notation was wirtten at 1am and with not nearly enough coffee. My reason for making it a first class citizen is that even with the syntactic sugar, you end up with this, which IMHO is pretty verbose:

{
  silent: {
    onEntry: send('TIMER', 2000)
    onExit: cancel_send('TIMER')
    on {
      TIMER: 'elsewhere'
    }
  }
}

It also means that if you want to write tests, you bind yourself to the names of these "timer" events which seems even more brittle.

I would much rather have something closer to the harel statechart notation, which is simply an arrow to another state and a time specification (and optional guards, I imagine):

{
  silent: {
    after: {
      "4s": "elsewhere"
    }
  }
}

This also can be expanded to e.g. an object rather than a string, especially when you think about adding support for guards. e.g. "after 4 seconds if x > 3" and "after 5 seconds if x <= 3". It should of course also support arrays, since (like onEntry) many things can happen at the same time. The syntax here is of course not what I have the strongest preference for.

The client code that executes the xstate pure function would need to be told:

  • which delayed events should be sent, and when
  • which previously delayed event that had delayed events that were exited

The client code also needs a simple way to tell the state machine that "The elapsed time you told me about has elapsed, do something".

These would basically be mapped to a calling program's list of currently outstanding timeouts.

Let's say we _entered_ a state that had a "after 4s transition to elsewhere", and we _exited_ a state called "other" that had an "after 3 seconds transition to something"

newstate: {
  effects: {
    entry: [ ... ],
    exit: [ ... ],
    timeouts: [ { state: "silent", target: "elsewhere", delay: 4000 } ],
    clearTimeouts: [ { state: "other", target: "something", delay: 3000 } ]
  }
}

These "effects" can then be used in a simple, homogenous manner by hooking into the setTimeout / clearTimeout. We would of course clear the old timeouts before we set up new timeouts, just like we call exit handlers before entry handlers.

newstate = machine......(oldstate, "foo");
// clear each timeout of the states that were exited that have after x s
newstate.effects.canceledTimers.forEach(event => clearTimeout(timeouts[event.id]))
// set new timeouts of the states that were entered that have after x s
newstate.effects.setTimers.forEach(event => timeouts[event.id] = setTimeout(...., event.delay))

We need a new API then to basically tell the state machine that "some time has passed, and that the after x seconds should trigger". What would be nice would be to be able to avoid giving these transitions names, since it just adds clutter and makes them even harder to use. They don't need names, they just happen after a certain amount of time.

Today we have machine.transition(state, event), but time is just time, so you could either base it purely on how much time has elapsed, and have e.g. machine.elapse(state, duration) and then the state machine itself will figure out what should happen based on the current state. It would also make it easy to test too:

it('should show the login page after 0.4 seconds', () {
  state = machine.transition(state, 'login')
  state = machine.elapse(state, 0.4);
  // assert that state.effects contains the "show login page" action
});

I'm not sure I understand how the statechart can let you know to "perform action X after 4 seconds", if the developer has to call .transition() manually.

Seems to me the only option is to make .transition() return some kind of promise/observable?

stateMachine.transition(currentState, 'TIMER')
  .then(newStateMachine => newStateMachine.actions) // do something with actions

Unless I'm missing something 🤔 ?

Also having xstate handle time will it make it unpure by definition - unless we find a way of outsourcing the timing logic outside of xstate.

The idea was indeed to handle this in the same manner as actions are handled today. xstate would only _tell you_ that "By the way, you ought to start a timer that fires in 4 seconds, and tell me about it when it happens":

state = machine.transition(state, "something");

state.timers.start would be an array of timeouts that need to be started, each containing a unique ID (generated by the statechart), and a time.
state.timers.start[0].id perhaps abcd1234
state.timers.start[0].duration perhaps. 2.0

This is enough for me to (in my controller) write something like this:

myTimers[state.timers.start[0].id] = setTimeout(() => machine.timeout(state.timers.start[0].id), state.timers.start[0].duration * 1000)

i.e. "At 2000ms in the future, call back the machine with the ID provided, saying that the time has elapsed". Of course you'd use Array.forEach or something to loop over all of the timers to start, and there would be a similar loop for _canceling_ timers that the statechart has asked for:

state.timers.stop  //  [ "abcd1234" ]
clearTimeout(state.timerst.stop[0]);

xstate is still pure function.

Thanks for clarification. Perhaps simply having the statechart return something like "you should transition to A in 4 seconds" would be ok I guess.

However, I still think that's asking a lot for the user to implement.

@lmatteis I'm going to open an issue regarding adding a runner that will act as an event emitter/stream of values, which will mitigate the implementation concerns for timers, activities, etc. This is going to be very similar to an Observable stream, and since it will have no side effects (since it's only used for observing emitted values) it is still functional, similar to Elm subscriptions.

@lmatteis I think such a wrapper is needed for "normal operation" — when you're doing timeouts and somehing, there's no way around it being stateful. But the inner core of xstate should remain stateless. Having a stateless core is really cool when it comes to writing test cases that verify certain behaviour. e.g. "after 0.5 s after the login failed message, the error message must be shown"

Having a stateless core already requires you to maintain the "current state" and alredy you need to deal with any actions "emitted", so I think timers is just a natural extension. If your state machine doesn't use timers then you don't have to use them. But a de facto _runner_ would be super to have, implementing the required outside behaviour _correctly_.

Agree, having runners, perhaps even as separate projects, would make sense.

Here's the runner/interpreter proposal: https://github.com/davidkpiano/xstate/issues/50

Lots of use cases to consider for this one (do we want different types of events? how do we start the interpreter? etc.)

For the tentative syntax, I think that this can be implemented without having to introduce new syntax, through "special" actions that can describe how to invoke events (I put "special" in quotes because they can be interpreted in any way, up to the developer).

{
  someState: {
    onEntry: [
      {
        type: 'send', // "special" <send> action
        event: 'doSomethingLater',
        delay: 2000, // 2s
      },
      { type: 'someNormalAction' }
    ],
    onExit: [
      {
        type: 'cancel', // "special" <cancel> action
        event: 'doSomethingLater'
      }
    ]
  }
}

This keeps the exact same structure as currently exists, allows for full <send> and <cancel> semantics (in converting to SCXML, all onEntry/onExit actions will be <send> actions anyway).

With activities, we may also be able to simplify the above, if cancellation is desired:

{
  someState: {
    activities: [
      { type: 'send', event: 'doSomethingLater', delay: 2000 },
      'anotherActivity'
    ]
  }
}

The above will automatically cancel the activity of sending that event when the state is exited, and is much more succinct.

This also gives the developer the full flexibility to handle their own "delay" strategy in their interpreter. This is important (and why xstate remains pure), because for testing, you want to simulate delays instead of having the actual delays occur over time.

Thoughts @mogsie @lmatteis ?

I'm a bit confused. Don't we need the pure transition function to just output "please transition to state B in 5 seconds"? Then of course the "interpreter" will actually implement that.

To describe this, why do we need 'event' and 'type'?

To me this is simply an activity "goToStateBin5seconds" which could be described in the statechart as:

{ target: 'B', delay: 5000 }

And the output of the transition would contain an object inside start/stop/active instead of the string?

Can you provide the reasoning behind the event and type properties?

You're talking about delayed transitions; I'm talking about delayed events.

Ah my bad! How would you represent these visually, on the graph?

Ah number 2. I stupidly used the term "delayed events" throughout this issue description, but what I think I'm talking about is actually a delayed transition. I don't see the need for an event to trigger the transitions that I'm thinking about.

Specifically, I'd like to be able to (all within the statechart, i.e. xstate + runner) declare succinctly that:

If the machine is in this state for X amount of time (and if, at time X, the guard condition holds true), please transition to this state.

I don't actually see much of a use case for "delayed events" per se. Has this caused much confusion?

@mogsie How would this be represented with SCXML? I want to be cautious of adding too many extra ad-hoc properties. I know that SCXML has the notion of an eventless transition, but not one that occurs over time, nor is immediately cancelled once the state exits. That more fits the semantics of activities, which I think is a much more natural fit for this use case.

By definition, a transition should be (at least conceptually) be considered zero-time, so the only way a transition can happen over time is if an event caused that transition, even if it's an internal event.

If you can, let's include here how other libraries handle automatic transitions. We can be open to adding an extra property as long as it can somehow be represented (even just as a slightly more verbose equivalence without the sugar) in SCXML.

On second (third?) thought, a separate property might be the best solution here. Bikeshedding, "after" implies a "before" and can be confusing with other common uses of the term (Mocha, etc). Also we need to enforce the constraint that there is at most one "delayed transition" (because that's how time works and we haven't discovered wormholes yet).

How about "onDelay" or just "delay" or "timeout"?

In SCXML, a delayed transition is only possible by way of an entry <send>, an exit <cancel>, and a handler for the event. I don't know if they discussed it in their 10 year history.

I've described it (SCXML syntax) in https://statecharts.github.io/glossary/delayed-transition.html

When it comes to _only one_ such timed transition, I do see the use case for a guarded, delayed transition, which _does_ complicate things.

I don't think this delayed transition as being something that happens "over time" — I think it simply doesn't happen _until_ a specific point in time, relative to the entry of a state. A delayed transition is still instantaneous.

WRT syntax: After giving it some more thought, I wonderi f we could avoid putting this into the core xstate but instead make a wrapper or pre-processor to the machine definition. It shouldn't be too hard, and this will give us some freedom in trying various syntaxes out without committing to them by adding it to xstate proper.

Imagine a TimeoutPreprocessor or some more aptly named function which transforms:

{
  "initial": "silent",
  "states": {
    "silent": {
      "after": {
        "4s": "elsewhere"
      }
    }
    "elsewhere": {}
  }
}

Into this — when it sees an "after: 4s elsewhere" syntax, it invents a unique ID (here abcd1234) and desugars it (e.g. using the activity spec)

{
  "initial": "silent",
  "states": {
    "silent": {
      "activity": {
        "xstate.timeout.abcd1234:4s"
      }
      "on": {
        "xstate.timeout.abcd1234": "elsewhere"
      }
    }
    "elsewhere": {}
  }
}

or even to

{
  "initial": "silent",
  "states": {
    "silent": {
      "onEntry": {
        "xstate.setTimeout xstate.timeout.abcd1234:4s"
      }
      "onExit": {
        "xstate.clearTimeout xstate.timeout.abcd1234"
      }
      "on": {
        "xstate.timeout.abcd1234": "elsewhere"
      }
    }
    "elsewhere": {}
  }
}

I.e. convert a simple-to-author "after 4s" transition into a normal xstate with some additional constraints when it comes to valid event names (perhaps reserving xstate. prefixed events)

Then a _runner_ could complement this that implements setTimeout, clearTimeout and sends these "system generated events" to a standard xstate Machine that is none-the-wiser.

Reopening to further nail down syntax for after:

green: {
  on: { EMERGENCY: 'red' },
  after: {
    1000: 'yellow'
  }
}
loading: {
  after: {
    1000: [
      { target: 'stillLoadingSorry', cond: xs => xs.userIsImpatient },
      { target: 'stillLoading' } // default
    },
    2000: 'giveUp' // never called, unless all < 2000 delays don't meet their conditions
  }
}
after: [
  { delay: 1000, target: 'yellow' },
  // possibly like delayexpr:
  // { delay: xs => xs.customDelay, target: 'yellow' }
]

This would be sugar for starting/stopping the delay activities while in the given state.

Do you plan to support delayed and/or regular events?

The case for regular events - ticks - feels stronger than for delayed events. I can imagine an outer state ticking out events where the effect is determined by the current inner state. It would I’m sure be possible to achieve the same thing with delayed transitions within the relevant set of inner states, but this might obscure the intent.

I’m not sure, just thinking out loud.

I think "regular events" should be declared as part of the statechart itself:

clock: {
  onEntry: {
    type: 'xstate.raise', value: 'TICK'
  },
  after: {
    1000: 'clock'
  }
}

Declare some state that upon exit has a delayed transition to itself, and upon entry raises an event.

Benefits:

  • It becomes apparent from the statechart where the "tick" events come from
  • It also allows different "clocks" that tick around the statechart
  • It allows clocks to be active and stop being active if they ever leave the state
  • It allows synchronizing the start of the "ticks" with external events, so e.g. you can start ticking when an event occurs (by entering the clock state).

after syntax and delayed events are in master and will be released in 4.0.

Should it be possible to have onEntry accept a function to access context of a dynamic delay?

  • this machine is an Actor withContext({ delay: 4000 })
  • I want to slow down time and draw some points on a canvas in random locations
  • I want to self transition in a loop until all my pointsRemaining have been depleted.

It doesn't look like this is possible with a dynamic value or wrapping the whole machine in a factory function which to me is not ideal.

   ...
    adding: {
        onEntry: (context, event) =>  send("DRAW_SITES", { delay: context.delay }),
        on: {
          DRAW_SITES: [
            {
              target: "adding",
              actions: ["decrementPoint"],
              cond: { type: "pointsRemaining" }
            },
            { target: "sleeping" }
          ]
        }
      },
      ...

@adam-cyclones It's possible:

// ...
entry: send('DRAW_SITES', {
  delay: (context) => context.delay
})
// ...

@adam-cyclones It's possible:

// ...
entry: send('DRAW_SITES', {
  delay: (context) => context.delay
})
// ...

I blame the late nights, thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

carlbarrdahl picture carlbarrdahl  Â·  3Comments

amelon picture amelon  Â·  3Comments

pke picture pke  Â·  3Comments

kurtmilam picture kurtmilam  Â·  3Comments

bradwoods picture bradwoods  Â·  3Comments