Skip to content
Ken Kunz edited this page Sep 8, 2022 · 11 revisions

Svelte FSM Documentation

Creating a state machine object

Svelte FSM exports a single default function. Import this as fsm, svelteFsm, or whatever seems appropriate in your project.

import fsm from 'svelte-fsm'

This function expects two arguments: initialState and states. The following is technically a valid but completely useless state machine:

const myFsm = fsm('initial', {});

States

Each state is a top-level property of the states object. A state's key can be any valid object property name (string or Symbol) except the wildcard '*' (see Fallback Actions below). A state's value should be an object with transition and action properties. The simplest state definition is just an empty object (you might use this for a final state where no further transitions or actions are possible).

const myFsm = fsm('initial', {
  initial: {
    finish: 'final'
  },

  final: {}
});

Transitions

As shown in the example above, a simple transition is defined by a key representing an event that can be invoked on the FSM object, and a value indicating the state to be transitioned to. In addition, action methods can optionally return a state value to be transitioned to. The following simple action-based transition is equivalent to the example above:

const myFsm = fsm('initial', {
  initial: {
    finish() { return 'final'; }
  },

  final: {}
});

Actions

States can include methods called actions, which optionally may return a transition. Actions are useful for side-effects (requesting data, modifying context, generating output, etc.) as well as for conditional or (guarded) transitions. Since an action optionally returns a transition state, a single action might result in a transition in some circumstances and not others, or may result in different transitions. Actions can also optionally receive arguments.

const max = 10;
let level = 0;

const bucket = fsm('notFull', {
  notFull: {
    add(amount) {
      level += amount;
      if (level === max) {
        return 'full';
      } else if (level > max) {
        return 'overflowing';
      }
    }
  },

  full: {
    add(amount) {
      level += amount;
      return 'overflowing';
    }
  },

  overflowing: {
    add(amount) {
      level += amount;
    }
  }
});

Lifecycle Actions

States can also include two special lifecycle actions: _enter and _exit. These actions are only invoked when a transition occurs – _exit is invoked first on the state being exited, followed by _enter on the new state being entered.

Unlike normal actions, lifecycle methods cannot return a state transition (return values are ignored). These methods are called during a transition and cannot modify the outcome of it.

Lifecycle methods receive a single lifecycle metadata argument with the following properties:

{
  from: 'peviousState',  // the state prior to the transition.
  to: 'newState',        // the new state being transitioned to
  event: 'eventName',    // the name of the invoked event that triggered the transition
  args: [ ... ]          // any arguments that were passed to the event
}

A somewhat special case is when a new state machine object is initialized. The _enter action is called on the initial state with a value of null for both the from and event properties, and an empty args array. This can be useful in case you want different entry behavior on initialization vs. when the state is re-entered.

const max = 10;
let level = 0;
let spillage = 0;

const bucket = fsm('notFull', {
  notFull: {
    add(amount) {
      level += amount;
      if (level === max) {
        return 'full';
      } else if (level > max) {
        return 'overflowing';
      }
    }
  },

  full: {
    add(amount) {
      level += amount;
      return 'overflowing';
    }
  },

  overflowing: {
    _enter({ from, to, event, args }) {
      spillage = level - max;
      level = max;
    }

    add(amount) {
      spillage += amount;
    }
  }
});

Fallback Actions

Actions may also be defined on a special fallback wildcard state '*'. Actions defined on the wildcard state will be invoked when no matching property exists on the FSM object's current state. This is true for both normal and lifecycle actions. This is useful for defining default behavior, which can be overridden within specific states.

Event invocation

Conceptually, invoking an event on an FSM object is asking it to do something. The object decides what to do based on what state it's in. The most natural syntax for asking an object to do something is simply a method call. Event invocations can include arguments, which are passed to matching actions (and also also forwarded to Lifecycle Actions as the args parameter).

myFsm.finish(); // => 'final'
bucket.add(10); // => 'full'

The resulting state of the object is returned from invocations. In addition, subscribers are notified if the state changes.

Subscribing to state changes

Svelte FSM adheres to Svelte's store contract. You can use this outside of Svelte components by calling subscribe with a callback (which returns an unsubscribe function).

const unsub = bucket.subscribe(console.log);
bucket.add(5); // [console] => 'notFull'
bucket.add(5); // [console] => 'full'
bucket.add(5); // [console] => 'overflowing'

Within a Svelte Component, you can use the $ syntactic sugar to access the current value of the store. Svelte FSM does not implement a set method, so you can't assign it directly. (This is intentional – finite state machines only change state based on the defined transitions and event invocations).

<div class={$bucket}>
  The bucket is {$bucket === 'notFull' ? 'not full' : $bucket}
</div>
<input type="number" bind:value>
<button type="button" on:click={() => bucket.add(value)}>

NOTE: subscribe is a reserved word – it cannot be used for transitions, actions or event invocations.

Action method binding

Action methods are called with the this keyword bound to the FSM object. This enables you to invoke events from within the FSM's action methods, even before the resulting FSM object has been assigned to a variable.

When is it useful to invoke events within action methods? A common pattern is to initiate an asynchronous event from within a state's _enter action (e.g., a timed event using debounce, or a web request using fetch). The event invokes an action on the same state – e.g., success() or error(), resulting in an appropriate transition. The Traffic Light and Svelte Form States examples illustrate this scenario.

Making synchronous event calls within an action method is not recommended (this.someEvent()). Doing so may yield surprising results – e.g., if you invoke an event from an action that returns a state transition, and the invoked action also returns a transition, you are essentially making a nested transition. The outer transition (original action return value) will have the final say.

Note that arrow function expressions do not have their own this binding. You may use arrow functions as action properties, just don't expect this to reference the FSM object.

Debounced invocation

Events can be invoked with a delay by appending .debounce to any invocation. The first argument to debounce should be the wait time in milliseconds; subsequent arguments are forwarded to the action. If debounce is called again before the timer has completed, the original timer is canceled and replaced with the new one (even if the delay time is different).

bucket.add.debounce(2); // => Promise that resolves with 'overflowing'

debounce invocations return a Promise that resolves with the resulting state if the invocation executes. Canceled invocations (due to a subsequent debounce call) never resolve. Calling debounce(null) cancels prior invocations without scheduling a new one, and resolves immediately.