Liking cljdoc? Tell your friends :D

reflet.fsm

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.

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.
raw docstring

advancecljs

source

distribute-transitionscljs

(distribute-transitions fsm)
source

fsm-interceptorscljs

source

lifecycle-interceptorcljs

source

reg-fsmcljs

(reg-fsm id fsm-fn)
source

running?cljs

(running? fsm-v)
source

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close