-
Notifications
You must be signed in to change notification settings - Fork 9
Home
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', {});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: {}
});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: {}
});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;
}
}
});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;
}
}
});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.
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.
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:
subscribeis a reserved word – it cannot be used for transitions, actions or event invocations.
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.
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.