Provides finite state machine DSL and implementation.
This FSM implementation is based on an entity model, where any entity in the db can be transitioned through allowed states given defined inputs. This means both domain entities as well as component entities can be used as FSMs.
Each FSM is defined declaratively. For example:
(f/reg-fsm ::review (fn [self user] {:ref self :stop #{::accepted ::cancelled} :fsm {nil {[:voted self] ::review} ::review {:* {:to ::decision :pull [self] :when ::review-threshold :dispatch [:notify user]}} ::decision {[::fsm/timeout self 1000] ::cancelled [:accepted self] {:to ::accepted} [:revisit self] ::review}}}))
This spec requires the following attributes:
:ref
A db reference to the FSM entity being advanced through
states.
:fsm
Defines the allowed states of the FSM, mapping each state
to a set of allowed transitions.
The following optional attributes are also supported:
:attr
The entity attribute where the FSM state is stored. If not
provided ::state
is used.
:stop
One or more states which when reached will stop the
FSM. The FSM interceptor will be removed, but the FSM
state will also be set to the relevant state.
:return
The result returned by the FSM subscription. This is
expressed as a pull spec that is run against the db with
the FSM :ref
as the root entity. The default pull spec
returns a single attribute, which is the FSM state
attribute (see the :attr
option above).
:or
Advance the FSM to some default state if there is no
state value in the db on init.
:dispatch
One or more events to dispatch immediately after starting
the FSM.
:dispatch-later
Same, but conforms to the Re-frame :dispatch-later
fx
syntax.
Like Re-frame events and subscriptions, FSMs are uniquely identified
by a vector that starts with their reg-fsm
id, followed by their
arguments: [::review self]
.
FSMs are implemented via interceptors that will advance the FSM on any received Re-frame event.
A running FSM will throw an error if it ever reaches a state that is
not defined in either the :fsm
state map, or the set of :stop
states. You can define a state with no transitions by mapping the
state to nil
:
{:fsm {::going-nowhere nil ::going-somewhere {[:event ref] ::going-nowhere}}}
Note this does NOT mean that ::going-nowhere
will transition to
the nil
state, but rather that ::going-nowhere
has no
transitions defined.
This will effectively pause the FSM, without actually turning off
its interceptor. To prevent the interceptor from running needlessly
you would normally declare ::going-nowhere
as a stop state
instead:
{:stop #{::going-nowhere} :fsm {::going-somewhere {[:event ref] ::going-nowhere}}}
The one use case for mapping a state to nil
, rather than declaring
it as a stop state, is if you want the FSM to sit and do nothing
until some other process has manually set the state attribute of the
FSM entity in the db. At this point the FSM would immediately
advance through the new state's defined transitions, starting with
the same event that manually set the FSM state. But this use case is
pretty niche.
Each transition for a given state is a map between an input event, and one or more output clauses.
There are two different types of event inputs currently defined:
Event transitions match a received event against a set of event prefixes. Each prefix either matches the event exactly, or an event with additional args. If more than one event prefix would match, then the longest matching prefix is chosen. For example, given the received event:
[:voted self first-pref second-pref]
and the set of input prefixes:
[:voted self] [:voted self first-pref]
Then matching prefix would be:
[:voted self first-pref]
You can match any event with the special keyword :*.
Timeout transitions are just like event transitions, except the first three positional elements of their event vector are:
[:reflet.fsm/timeout ref ms ...]
where ref
is an entity reference, and ms
is the timeout duration
in milliseconds.
If the FSM arrives at a state that has a timeout in its transition
map, and the timeout's ref
is the same as the FSM :ref
and
specifies a ms
duration, then the FSM will immediately set a
timeout for that state that will fire within the designated time. If
no other event causes the FSM to advance in this time, the timeout
event is fired via dispatch-sync
to advance the FSM through the
timeout transition. Any timeouts that do not fire are cleaned up
appropriately. If the timeout's ref
is not the same as the FSM
:ref
, or if the timeout does not define a ms
duration, then the
timeout will be matched just like a regular event. This way you can
listen for timeouts from other FSMs.
Only one wildcard or timeout transition is allowed in a state's transition map. Otherwise, a state's transition map can have an arbitrary number of event transitions. If a state defines both a wildcard and a set of event transitions, the event transitions will always be matched before the wildcard transition. As before: the most specific match will win.
All transitions define one or more output clauses. Each output clause be expressed in either simple or expanded form.
:to
The next state to transition to [required]
:when
A conditional clause that must return true for the
transition to match. This can be either, a) the id of a
Clojure spec that must return s/valid? true
, or b) a
Clojure function that returns a truthy value. By default,
the input to either the spec or the function will be the
received event that triggered the transition. If :pull
is specified, a list of entities will be provided as input
instead of the event. See the :pull
option for more
details. [optional]
:pull
A list of entity references. These entities are then
passed as inputs to the :when conditional, in place of the
trigger event. This allows you to build FSMs that use the
state of other FSMs as inputs to their transitions.
[optional]
:dispatch
An event vector to dispatch on a successful transition
[optional]
:dispatch-later
Same semantics as :dispatch-later
Re-frame fx.
Once an FSM has been declared using reg-fsm
, instances can be
created via subscriptions. When created this way, an FSM's lifecycle
is tied to the lifecycle of its subscription. When the subscription
is disposed, the FSM is also stopped. Subscriptions are the
preferred way to create FSMs.
If you really need to start or stop and FSM during the event/fx
phase of the application, you can do so using the ::start
and
::stop
event and fx handlers, with the caveat that an FSM can
never be started or stopped while mutating the db at the same
time. The FSM implementation actually has an interceptor that throws
an error if this ever happens.
When an FSM is started, its initial state will be whatever is
referenced by its FSM :ref
in the db. If no state exists, then it
will be nil
. You should always define a nil
transition, or set
the :to
option to advance the FSM on startup. Otherwise, the FSM
will never get out of the nil
state.
Because the FSM implementation is based on global interceptors that run every time, all the matching and lookup algorithms are written to be very fast.
Provides finite state machine DSL and implementation. This FSM implementation is based on an entity model, where any entity in the db can be transitioned through allowed states given defined inputs. This means both domain entities as well as component entities can be used as FSMs. Each FSM is defined declaratively. For example: (f/reg-fsm ::review (fn [self user] {:ref self :stop #{::accepted ::cancelled} :fsm {nil {[:voted self] ::review} ::review {:* {:to ::decision :pull [self] :when ::review-threshold :dispatch [:notify user]}} ::decision {[::fsm/timeout self 1000] ::cancelled [:accepted self] {:to ::accepted} [:revisit self] ::review}}})) This spec requires the following attributes: `:ref` A db reference to the FSM entity being advanced through states. `:fsm` Defines the allowed states of the FSM, mapping each state to a set of allowed transitions. The following optional attributes are also supported: `:attr` The entity attribute where the FSM state is stored. If not provided `::state` is used. `:stop` One or more states which when reached will stop the FSM. The FSM interceptor will be removed, but the FSM state will also be set to the relevant state. `:return` The result returned by the FSM subscription. This is expressed as a pull spec that is run against the db with the FSM `:ref` as the root entity. The default pull spec returns a single attribute, which is the FSM state attribute (see the `:attr` option above). `:or` Advance the FSM to some default state if there is no state value in the db on init. `:dispatch` One or more events to dispatch immediately after starting the FSM. `:dispatch-later` Same, but conforms to the Re-frame `:dispatch-later` fx syntax. Like Re-frame events and subscriptions, FSMs are uniquely identified by a vector that starts with their `reg-fsm` id, followed by their arguments: `[::review self]`. FSMs are implemented via interceptors that will advance the FSM on any received Re-frame event. A running FSM will throw an error if it ever reaches a state that is not defined in either the `:fsm` state map, or the set of `:stop` states. You can define a state with no transitions by mapping the state to `nil`: {:fsm {::going-nowhere nil ::going-somewhere {[:event ref] ::going-nowhere}}} Note this does NOT mean that `::going-nowhere` will transition to the `nil` state, but rather that `::going-nowhere` has no transitions defined. This will effectively pause the FSM, without actually turning off its interceptor. To prevent the interceptor from running needlessly you would normally declare `::going-nowhere` as a stop state instead: {:stop #{::going-nowhere} :fsm {::going-somewhere {[:event ref] ::going-nowhere}}} The one use case for mapping a state to `nil`, rather than declaring it as a stop state, is if you want the FSM to sit and do nothing until some other process has manually set the state attribute of the FSM entity in the db. At this point the FSM would immediately advance through the new state's defined transitions, starting with the same event that manually set the FSM state. But this use case is pretty niche. Each transition for a given state is a map between an input event, and one or more output clauses. There are two different types of event inputs currently defined: 1. Event transitions 2. Timeout transitions Event transitions match a received event against a set of event prefixes. Each prefix either matches the event exactly, or an event with additional args. If more than one event prefix would match, then the longest matching prefix is chosen. For example, given the received event: [:voted self first-pref second-pref] and the set of input prefixes: [:voted self] [:voted self first-pref] Then matching prefix would be: [:voted self first-pref] You can match any event with the special keyword :*. Timeout transitions are just like event transitions, except the first three positional elements of their event vector are: [:reflet.fsm/timeout ref ms ...] where `ref` is an entity reference, and `ms` is the timeout duration in milliseconds. If the FSM arrives at a state that has a timeout in its transition map, and the timeout's `ref` is the same as the FSM `:ref` and specifies a `ms` duration, then the FSM will immediately set a timeout for that state that will fire within the designated time. If no other event causes the FSM to advance in this time, the timeout event is fired via `dispatch-sync` to advance the FSM through the timeout transition. Any timeouts that do not fire are cleaned up appropriately. If the timeout's `ref` is not the same as the FSM `:ref`, or if the timeout does not define a `ms` duration, then the timeout will be matched just like a regular event. This way you can listen for timeouts from other FSMs. Only one wildcard or timeout transition is allowed in a state's transition map. Otherwise, a state's transition map can have an arbitrary number of event transitions. If a state defines both a wildcard and a set of event transitions, the event transitions will always be matched before the wildcard transition. As before: the most specific match will win. All transitions define one or more output clauses. Each output clause be expressed in either simple or expanded form. 1. Simple: Just a state keyword 2. Complex: A map containing the following attributes: `:to` The next state to transition to [required] `:when` A conditional clause that must return true for the transition to match. This can be either, a) the id of a Clojure spec that must return s/valid? `true`, or b) a Clojure function that returns a truthy value. By default, the input to either the spec or the function will be the received event that triggered the transition. If `:pull` is specified, a list of entities will be provided as input instead of the event. See the `:pull` option for more details. [optional] `:pull` A list of entity references. These entities are then passed as inputs to the :when conditional, in place of the trigger event. This allows you to build FSMs that use the state of other FSMs as inputs to their transitions. [optional] `:dispatch` An event vector to dispatch on a successful transition [optional] `:dispatch-later` Same semantics as `:dispatch-later` Re-frame fx. Once an FSM has been declared using `reg-fsm`, instances can be created via subscriptions. When created this way, an FSM's lifecycle is tied to the lifecycle of its subscription. When the subscription is disposed, the FSM is also stopped. Subscriptions are the preferred way to create FSMs. If you really need to start or stop and FSM during the event/fx phase of the application, you can do so using the `::start` and `::stop` event and fx handlers, with the caveat that an FSM can never be started or stopped while mutating the db at the same time. The FSM implementation actually has an interceptor that throws an error if this ever happens. When an FSM is started, its initial state will be whatever is referenced by its FSM `:ref` in the db. If no state exists, then it will be `nil`. You should always define a `nil` transition, or set the `:to` option to advance the FSM on startup. Otherwise, the FSM will never get out of the `nil` state. Because the FSM implementation is based on global interceptors that run every time, all the matching and lookup algorithms are written to be very fast.
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close