Middlewares can be used to intercept any action on a subtree.
It is allowed to attach multiple middlewares to a node. The order in which middlewares are invoked is inside-out: This means that the middlewares are invoked in the order you attach them. The value returned by the action invoked/ the aborted value gets passed through the middleware chain and can be manipulated.
MST ships with a small set of pre-built / example middlewares.
Custom middleware example: SandBox example
Middlewares can be attached by using:
addMiddleware(target: IStateTreeNode, handler: IMiddlewareHandler, includeHooks: boolean = true) : void
the middleware will only be attached to actions of the target
and further sub nodes of such.
An example of this is as follows:
const store = SomeStore.create()
const disposer = addMiddleware(store, (call, next, abort) => {
console.log(`action ${call.name} was invoked`)
// runs the next middleware
// or the implementation of the targeted action
// if there is no middleware left to run
// the value returned from the next can be manipulated
next(call, value => value + 1);
});
const store = SomeStore.create()
const disposer = addMiddleware(store, (call, next, abort) => {
console.log(`action ${call.name} was invoked`)
// aborts running the middlewares and returns the 'value' instead.
// note that the targeted action won't be reached either.
return abort('value')
})
A middleware handler receives three arguments:
- the description of the the call,
- a function to invoke the next middleware in the chain and manipulate the returned value from the next middleware in the chain.
- a function to abort the middleware queue and return a value.
Note: You must call either next(call)
or abort(value)
within a middleware.
Note: If you abort, the action invoked will never be reached.
Note: The value from either abort('value')
or the returned value from the action
can be manipulated by previous middlewares.
Note: It is important to invoke next(call)
or abort(value)
synchronously.
Note: The value of the abort(value)
must be a promise in case of aborting a flow
.
export type IMiddleWareEvent = {
type: IMiddlewareEventType
name: string
id: number
parentId: number
rootId: number
tree: IStateTreeNode
context: IStateTreeNode
args: any[]
}
export type IMiddlewareEventType =
| "action"
| "flow_spawn"
| "flow_resume"
| "flow_resume_error"
| "flow_return"
| "flow_throw"
name
is the name of the actioncontext
is the object on which the action was defined & invokedtree
is the root of the MST tree in which the action was fired (tree === getRoot(context)
)args
are the original arguments passed to the actionid
is a number that is unique per external action invocation.parentId
is the number of the action / process that called this action.0
if it wasn't called by another action but directly from user coderootid
is the id of the action that spawned this action. If an action was not spawned by another action, but something external (user event etc),id
androotId
will be equal (andparentid
0
)
type
Indicates which kind of event this is
action
: this is a normal synchronous action invocationflow_spawn
: The invocation / kickoff of aprocess
block (see asynchronous actions)flow_resume
: a promise that was returned fromyield
earlier has resolved.args
contains the value it resolved to, and the action will now continue with that valueflow_resume_error
: a promise that was returned fromyield
earlier was rejected.args
contains the rejection reason, and the action will now continue throwing that error into the generatorflow_return
: the generator completed successfully. The promise returned by the action will resolve with the value found inargs
flow_throw
: the generator threw an uncatched exception. The promise returned by the action will reject with the exception found inargs
To see how a bunch of calls from an asynchronous process look, see the unit tests
A minimal, empty process will fire the following events if started as action:
action
: Anaction
event will always be emitted if a process is exposed as action on a model)flow_spawn
: This is just the notification that a new generator was startedflow_resume
: This will be emitted when the first "code block" is entered. (So, with zero yields there is oneflow_resume
still)flow_return
: The process has completed
use next to call the next middleware.
next(call: IMiddlewareEvent, callback?: (value: any) => any): void
-
call
Before passing the call middleware, feel free to (clone and) modify thecall.args
. Other properties should not be modified -
callback
can be used to manipulate values returned by later middlewares or the implementation of the targeted action.
use abort if you wan't kill the queue of middlewares and immediately return. the implementation of the targeted action won't be reached if you abort the queue.
abort(value: any) : void
value
is returned instead of the return value from the implementation of the targeted action.
set this flag to false
if you wan't to avoid having hooks passed to the middleware.
-
I alter a property and the change does not appear in the middleware.
-
If you alter a value of an unprotected node, the change won't reach the middleware. Only actions can be intercepted.