-
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:
subscribe
is 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.