intermatic

state machine for NodeJS and the browser

Usage no npm install needed!

<script type="module">
  import intermatic from 'https://cdn.skypack.dev/intermatic';
</script>

README

πŸ„ΈπŸ„½πŸ…ƒπŸ„΄πŸ…πŸ„ΌπŸ„°πŸ…ƒπŸ„ΈπŸ„²

Table of Contents generated with DocToc

work in progress

state machine for NodeJS and the browser

  • fsm.goto = ( to_sname ) -> ... will be present when an entry goto: '*' is present in the top level of the FSMD.

  • FSMs can be nested, with a sub-FSM lamp declared in the FSM description as fsmd.subs.lamp, and referred to in the FSM instance simply by fsm.lamp.

  • The parent FSM can be referred to from the sub-FSM via its attrbute up, so that e.g. button.lamp.up is identical to button.

  • NOTE in the future, some of the details of declaring and referring to sub-FSMs may change.

  • Nested FSMs thus provide namespaces. For example, an appliance with three buttons alpha_btn, beta_btn, gamma_btn can have one lamp for each button which will be referred to as alpha_btn.lamp, beta_btn.lamp, gamma_btn.lamp. The definition of each lamp can be identical (or variants along the same pattern), yet act independently of the other lamps.

  • Nested FSMs are also a measure to deal with the combinatorial state explosion.

  • The state of an FSM is

    • represented locally as a JS value / string ??? from 'inside';
    • for the outside, it is a key/value pair, implemented as an object with the FSM's name as sole attribute, so if a lamp is lit, that gives { lamp: 'lit', }
alpha_btn:
  ...
  my:
    lamp:
      cyclers:
        toggle: [ 'lit', 'dark', ]
    label:
      values:
        id:     {
          G: { text: 'go',    color: 'green', },
          W: { text: 'wait',  color: 'amber', },
          S: { text: 'stop',  color: 'red',   }, }
      entering:
        id:
          G: { }

Finite State Machine Description Objects (FSMDs)

  • In order to instantiate an FSM, use new Intermatic fsmd where fsmd is an object that describes the details of the state machineβ€”a Finite State Machine Description.

  • The fields of an FSMD are:

    • Declaring triggers:

      • triggers
      • cyclers (Not Implemented)
    • Lifecycle Attributes:

      • LAs concerning triggers:

        • before
        • after
      • LAs concerning states:

        • entering
        • keeping
        • leaving
        • change
    • specials:

      • goto
      • name
    • custom:

      • all attributes and properties except those mentioned above will be copied from the FSMD to the resulting FSM, preserving their property descriptors, meaning that things like computed properties, proxies, read-only values &c. will be preserved.
  • An Intermatic compound FSM (cFSM) has a tree structure

  • which implies that there must be exactly one root object.

  • The root object is always an FSM; however,

  • like any FSM, the root FSM can be 'empty' (i.e. have no other functionality than a start() method which transitions from the implicit void to the same void state);

  • the root FSM may or may not have a name (be named or anonymous)

  • and may contain zero or more sub-FSMs

  • all of which must be named.

  • Compound and simple FSMs are instances of Intermatic,

  • Sub-FSMs may be defined as sub-objects of their parent FSMD provided

    • their name is not a reserved key (after before cascades cstate data entering EXP_dstate fsm_names has_subfsms history_length keeping leaving lstate lstates moves up)
    • their value is a plain JS objects ({})
    • their value is a valid FSMD
  • Unreachable states are states that can not be reached by any kind of proper (named) trigger;

  • these make sense only for FSMs that have a goto() method.

  • Unreachable states cause an error on instantiation unless licensed in the configuration (FSMD) by setting unreachable: true.

  • departures (dpar), destinations (dest) are the local states where a transition─a move─starts and ends, respectively;

  • verbs are what triggers an FSM to change state.

  • Specifically, the methods that are compiled from the verbs found as keys in an FSMD's moves object are called triggers because they are used to trigger a single transition from one state to another state.

  • Triggers accept any number of arguments; these will be passed into the state and trigger actions.

  • State Actions are methods that are called when a state is entered or left.

  • Trigger Actions are methods that are called before or after a trigger has caused a transition.

  • Actions are associated with tuples ( stage, cause ), where a cause is either a verb or a local state. The stages associated with trigger actions are 'before' and 'after'; the stages associated with state actions are entering, leaving, and keeping. Thus an action associated with ( 'before', 'start' ) will be called (as implicit) before the transition to be caused by calling the trigger start is performed; an action associated with ( 'leaving', 'green' ) will be called whenever the local state is 'green' and a transition is about to change that.

  • A trajectory is a (possibly empty) list of local states. It must satisfy a number of constraints:

    • A trajectory list must have either no elements or more than one element.
    • The elements in a trajectory list are interpreted in a pair-wise fashion such that the ith element becomes the departure and the i + 1th element the destination of an elementary trajectory a.k.a. a transition. For example, the trajectory [ 'a', 'b', 'c', ] contains the transitions from departure a to destination b, and the transition from b to c.
    • It is not allowed to repeat any element of a trajectory except for the case of circular trajectory (cycles) where the last element repeats the first element. For example, [ 'a', 'b', 'c', 'a', ] denotes a trajectory from a through b through c, and then from c back to a.
    • The first element of a trajectory list (and only the first one) may be the catch-all symbol (written as '*' or 'any'); this signifies that the verb may be called in any state and will then transition to the second element in the list.
  • A move is a key/value pair whose key is a verb and whose value is a trajectory.

  • A given verb may connect a number of departures and destinations, and a given verb may connect several departures with several destinations; however, given a verb and a departure state, there can only be up to one destination state.

  • actions are (synchronous or asynchronous) functions that are called in response to actions having taken or about to take place

  • Verbs mentioned in the fsmd.cascades attributes will be called on all sub-FSMs.

  • Root FSM (the uppermost object reachable through recursively retrieving the up attribute) is available as attribute root_fsm; the value of fsm.root_fsm.root_fsm is always null.

  • Experimental poor man's event bubbling: if the root FSM has a method after.EXP_any_change(), it will be called after any change in any (direct or indirect) sub-FSM; the first argument will be the sub-FSM whose state has changed; rest of arguments as with all other actions.

  • path_separator and omit_root_name may be set (only at the root FSM for the time being) to control whether the value returned by fsm.path should include the root FSM's name as first element, and waht string should be used to separate path components. Defaults are omit_root_name: false and path_separator: '/'. Observe that the result of root_fsm.path will always be the root FSM's name, regardless of the value of omit_root_name. (Currently, when one of these attributes is set on any child FSM, the behavior of fsm.path is considered undefined.)

  • fsm.breadcrumbs returns a list with the path elements that are also seen in fsm.path. Observe though that in case fsm.omit_root_name is set, the path for the root FSM will still be its name, but its breadcrumbs property will be an empty list.

    • This is in keeping with the intended use case for these properties: configure and use path to obtain a suitable and readable unique ID for (the vents coming from) each FSM; use breadcrumbs or a derivative of it to show 'how to get there', optionally omitting the root object which may be seen as a technical necessity, as the case may be.
  • fsm.history

  • Multiple terminal states are not a problem.

fsm = {
  foobar: {
    triggers: [ ... ],
    before:   { ... },
    entering:    { ... },
    ... }
fsm = {
  name:     'foobar',
  triggers: [ ... ],
  before:   { ... },
  entering:    { ... },
  ... }

or an object with

or

fsm = {
  foobar: {
    name:     'foobar',
    triggers: [ ... ],
    before:   { ... },
    entering:    { ... },
    ... }
fsm_1 = new Intermatic { subs: { foo: { ... }, bar: { ... }, }  }
fsm_1 = new Intermatic { foo: { ... },              }
fsm_2 = new Intermatic { foo: { ... }, bar: { ... } }
  • { alpha_btn: { lamp: 'lit', color: 'green', label: 'go', } }
fsmd =
  name: 'meta_lamp'
  triggers: [
    [ 'void',   'start',  'lit',  ] # trigger β„– 1
    [ '*',      'reset',  'void', ] # trigger β„– 2
    [ 'lit',    'toggle', 'dark', ] # trigger β„– 3
    [ 'dark',   'toggle', 'lit',  ] # trigger β„– 4
  cyclers:
    toggle: [ 'lit', 'dark', ] # not yet implemented, alternative to triggers β„–s 3, 4
  after:
    change:     ( s ) -> register "after change:  #{rpr s}"
  entering:
    dark:       ( s ) -> register "entering dark:    #{rpr s}"
  leaving:
    lit:        ( s ) -> register "leave lit      #{rpr s}"
  goto:         '*'
  fail:         ( s ) -> register "failed: #{rpr s}"
#---------------------------------------------------------------------------------------------------------
{ Intermatic, } = require '../../../apps/intermatic'
fsmd            = fsmd
fsm             = new Intermatic fsmd
fsm.start()
fsm.toggle()
fsm.reset()
fsm.toggle()
fsm.goto 'lit'

Lifecycle of Intermatic FSMs

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•₯───────────────────────────────────────────────────────┐
 1 β”‚      called β•‘ called by FSM                                         β”‚
 2 β”‚     by User ║────────────────────┬──────────────────────────────────│
 3 β”‚             β•‘                    β”‚fsm.  β”‚ fsm.move.                 β”‚
 4 β”‚             β•‘                    β”‚lstate│──────┬──────┬──────┬──────│
 5 β”‚             β•‘            actions β”‚      β”‚ stageβ”‚ verb β”‚ dpar β”‚ dest β”‚
 6 β”‚             β•‘                    β”‚      β”‚      β”‚      β”‚      β”‚      β”‚
 7 │═════════════║════════════════════β•ͺ══════β•ͺ══════β•ͺ══════β•ͺ══════β•ͺ══════│
 8 β”‚             β•‘                    β”‚ void β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
 9 │─────────────║────────────────────│ void β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
10 β”‚   Ο†.start() β•‘                    β”‚ void β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
11 β”‚             β•‘   Ο†.before.any[]() β”‚ void β”‚ bfr. β”‚ startβ”‚ void β”‚ a    β”‚
12 β”‚             β•‘Ο†.before.change[]() β”‚ void β”‚ bfr. β”‚ startβ”‚ void β”‚ a    β”‚
13 β”‚             β•‘ Ο†.before.start[]() β”‚ void β”‚ bfr. β”‚ startβ”‚ void β”‚ a    β”‚
14 β”‚             β•‘  Ο†.leaving.any[]() β”‚ void β”‚ lvg. β”‚ startβ”‚ void β”‚ a    β”‚
15 β”‚             β•‘ Ο†.leaving.void[]() β”‚ void β”‚ lvg. β”‚ startβ”‚ void β”‚ a    β”‚
16 β”‚             ║────────────────────│──────│ lvg. β”‚ startβ”‚ void β”‚ a    β”‚
17 β”‚             β•‘ Ο†.entering.any[]() β”‚ a    β”‚ ent. β”‚ startβ”‚ void β”‚ a    β”‚
18 β”‚             β•‘   Ο†.entering.a[]() β”‚ a    β”‚ ent. β”‚ startβ”‚ void β”‚ a    β”‚
19 β”‚             β•‘    Ο†.after.any[]() β”‚ a    β”‚ aftr.β”‚ startβ”‚ void β”‚ a    β”‚
20 β”‚             β•‘ Ο†.after.change[]() β”‚ a    β”‚ aftr.β”‚ startβ”‚ void β”‚ a    β”‚
21 β”‚             β•‘  Ο†.after.start[]() β”‚ a    β”‚ aftr.β”‚ startβ”‚ void β”‚ a    β”‚
22 │─────────────║────────────────────│ a    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
23 β”‚    Ο†.step() β•‘                    β”‚ a    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
24 β”‚             β•‘   Ο†.before.any[]() β”‚ a    β”‚ bfr. β”‚ step β”‚ a    β”‚ b    β”‚
25 β”‚             β•‘Ο†.before.change[]() β”‚ a    β”‚ bfr. β”‚ step β”‚ a    β”‚ b    β”‚
26 β”‚             β•‘  Ο†.before.step[]() β”‚ a    β”‚ bfr. β”‚ step β”‚ a    β”‚ b    β”‚
27 β”‚             β•‘  Ο†.leaving.any[]() β”‚ a    β”‚ lvg. β”‚ step β”‚ a    β”‚ b    β”‚
28 β”‚             β•‘    Ο†.leaving.a[]() β”‚ a    β”‚ lvg. β”‚ step β”‚ a    β”‚ b    β”‚
29 β”‚             ║────────────────────│──────│ lvg. β”‚ step β”‚ a    β”‚ b    β”‚
30 β”‚             β•‘ Ο†.entering.any[]() β”‚ b    β”‚ ent. β”‚ step β”‚ a    β”‚ b    β”‚
31 β”‚             β•‘   Ο†.entering.b[]() β”‚ b    β”‚ ent. β”‚ step β”‚ a    β”‚ b    β”‚
32 β”‚             β•‘    Ο†.after.any[]() β”‚ b    β”‚ aftr.β”‚ step β”‚ a    β”‚ b    β”‚
33 β”‚             β•‘ Ο†.after.change[]() β”‚ b    β”‚ aftr.β”‚ step β”‚ a    β”‚ b    β”‚
34 β”‚             β•‘   Ο†.after.step[]() β”‚ b    β”‚ aftr.β”‚ step β”‚ a    β”‚ b    β”‚
35 │─────────────║────────────────────│ b    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
36 β”‚    Ο†.step() β•‘                    β”‚ b    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
37 β”‚             β•‘   Ο†.before.any[]() β”‚ b    β”‚ bfr. β”‚ step β”‚ b    β”‚ c    β”‚
38 β”‚             β•‘Ο†.before.change[]() β”‚ b    β”‚ bfr. β”‚ step β”‚ b    β”‚ c    β”‚
39 β”‚             β•‘  Ο†.before.step[]() β”‚ b    β”‚ bfr. β”‚ step β”‚ b    β”‚ c    β”‚
40 β”‚             β•‘  Ο†.leaving.any[]() β”‚ b    β”‚ lvg. β”‚ step β”‚ b    β”‚ c    β”‚
41 β”‚             β•‘    Ο†.leaving.b[]() β”‚ b    β”‚ lvg. β”‚ step β”‚ b    β”‚ c    β”‚
42 β”‚             ║────────────────────│──────│ lvg. β”‚ step β”‚ b    β”‚ c    β”‚
43 β”‚             β•‘ Ο†.entering.any[]() β”‚ c    β”‚ ent. β”‚ step β”‚ b    β”‚ c    β”‚
44 β”‚             β•‘   Ο†.entering.c[]() β”‚ c    β”‚ ent. β”‚ step β”‚ b    β”‚ c    β”‚
45 β”‚             β•‘    Ο†.after.any[]() β”‚ c    β”‚ aftr.β”‚ step β”‚ b    β”‚ c    β”‚
46 β”‚             β•‘ Ο†.after.change[]() β”‚ c    β”‚ aftr.β”‚ step β”‚ b    β”‚ c    β”‚
47 β”‚             β•‘   Ο†.after.step[]() β”‚ c    β”‚ aftr.β”‚ step β”‚ b    β”‚ c    β”‚
48 │─────────────║────────────────────│ c    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
49 β”‚    Ο†.step() β•‘                    β”‚ c    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
50 β”‚             β•‘   Ο†.before.any[]() β”‚ c    β”‚ bfr. β”‚ step β”‚ c    β”‚ c    β”‚
51 β”‚             β•‘  Ο†.before.step[]() β”‚ c    β”‚ bfr. β”‚ step β”‚ c    β”‚ c    β”‚ # NOTE that `before.change`
52 β”‚             β•‘  Ο†.keeping.any[]() β”‚ c    β”‚ keep.β”‚ step β”‚ c    β”‚ c    β”‚ # and `after.change` are
53 β”‚             β•‘    Ο†.keeping.c[]() β”‚ c    β”‚ keep.β”‚ step β”‚ c    β”‚ c    β”‚ # missing here b/c lstate
54 β”‚             β•‘    Ο†.after.any[]() β”‚ c    β”‚ aftr.β”‚ step β”‚ c    β”‚ c    β”‚ # is kept at `c`
55 β”‚             β•‘   Ο†.after.step[]() β”‚ c    β”‚ aftr.β”‚ step β”‚ c    β”‚ c    β”‚
56 │─────────────║────────────────────│ c    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
57 β”‚    Ο†.stop() β•‘                    β”‚ c    β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
58 β”‚             β•‘   Ο†.before.any[]() β”‚ c    β”‚ bfr. β”‚ stop β”‚ c    β”‚ void β”‚
59 β”‚             β•‘Ο†.before.change[]() β”‚ c    β”‚ bfr. β”‚ stop β”‚ c    β”‚ void β”‚
60 β”‚             β•‘  Ο†.before.stop[]() β”‚ c    β”‚ bfr. β”‚ stop β”‚ c    β”‚ void β”‚
61 β”‚             β•‘  Ο†.leaving.any[]() β”‚ c    β”‚ lvg. β”‚ stop β”‚ c    β”‚ void β”‚
62 β”‚             β•‘    Ο†.leaving.c[]() β”‚ c    β”‚ lvg. β”‚ stop β”‚ c    β”‚ void β”‚
63 β”‚             ║────────────────────│──────│ lvg. β”‚ stop β”‚ c    β”‚ void β”‚
64 β”‚             β•‘    Ο†.after.any[]() β”‚ void β”‚ aftr.β”‚ stop β”‚ c    β”‚ void β”‚
65 β”‚             β•‘ Ο†.after.change[]() β”‚ void β”‚ aftr.β”‚ stop β”‚ c    β”‚ void β”‚
66 β”‚             β•‘   Ο†.after.stop[]() β”‚ void β”‚ aftr.β”‚ stop β”‚ c    β”‚ void β”‚
67 β”‚             β•‘                    β”‚ void β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚β•³β•³β•³β•³β•³β•³β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•¨β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜

Note: in the above, x[]() denotes a call to all the functions in the list of functions identified by x. x[]() corresponds to x: [(->)] in the below FSMD.

fsmd =
  name: 'Ο†'
  moves: [
    start:    [ 'void', 'a', ]
    step:     [ 'a', 'b', 'c', 'c', ]
    stop:     [ 'c', 'void', ]
  start:
    before:   [(->)]
    after:    [(->)]
  step:
    before:   [(->)]
    after:    [(->)]
  stop:
    before:   [(->)]
    after:    [(->)]
  a:
    entering:    [(->)]
    leaving:     [(->)]
  b:
    entering:    [(->)]
    leaving:     [(->)]
  c:
    entering:    [(->)]
    keeping:     [(->)]
    leaving:     [(->)]

NOTE: in the above, [(->)] denotes a value consisting of either single function or a (possibly empty) list of functions.

To Do

  • implement fsm.tryto 't' to call trigger t only when allowed, avoiding calls to fail()
  • implement fsm.can 't' to test whether trigger t may be emitted from current state
  • implement attribute-access (cf. Multimix) for goto, tryto such that fsm.goto 's', fsm.tryto 't' is equivalent to fsm.goto.s(), fsm.tryto.t()
  • remove s/trigger argument from event handlers
  • Implement computed property move as { verb, dpar, dest, }

  • use lists of functions when compiling actions (allowing FSMDs to define either a list of functions or else a single function that compiles into a list with one element)

  • REJECTED should we unify before and entering, after and leaving? Possible setup uses 4 categories as opposed to the 5 now in use (before, after, entering, leaving, keeping):

    • beforeβ€”for trigger actions, called before move is started
    • keepingβ€”for state actions, only called when dpar equals dest
    • entering, leavingβ€”for state actions, only called when dpar is different from dest
    • afterβ€”for trigger actions, called after move has finished
  • implement goto with list of target (or source and target?) states

  • implement toggle

  • implement trigger cancellation (using API call, not return value)

  • discuss namespaces: trigger names and names of sub-FSMs originate in different parts of an FSMD but end up sharing one namespace when the FSM is constructed

  • percolate/bubble triggers (from sub to up? both directions? all FSMs in tree?)

  • when one trigger bubbles through the FSMs, how to tell when that trigger has been processed? Two consecutive events could have same name. Use ID?

  • implement cascading events, such that top.start() implicitly calls start() on all sub-FSMs

  • asynchronous moves

  • equivalents to setTimeout(), setInterval()?

  • make symbolic '*' equivalent to 'any'

  • rename FSMD attribute triggers to moves

  • one of the following:

    • use { verb, dpar, dest, } format
    • use lists with optionally more than three elements; a step action that goes from a to b to c, then stays at c would be [ 'step', 'a', 'b', 'c', 'c' ]; a cycler would be [ 'cycle', 'a', 'b', 'c', 'a', ]; this would obsolete FSMD attribute cyclers
  • state to be separated into three computed properties:

    • lstate(?) for local state: just the text (value) indicating the state of that component
    • clstate(?) for compound state with local states: object with lstate attributes for FSM and sub-FSMs
    • ccstate(?) more complete state including history (?)
  • make fsm.history return list of @move objects, do not construct new data type

  • remove index.* as those files are no longer needed

  • make all computed properties enumerable (use decorator/factory)

  • terminology/prefabs: 'pushtoggle' (a momentary switch that toggles between a number of states)