onActionMiddleware
This action middleware invokes a listener for all actions of a given tree.
Note that the listener will only be invoked for the topmost level actions, so it won't run for child actions or intermediary flow steps.
Also it won't trigger the listener for calls to hooks such as onAttachedToRootStore
or its returned disposer.
Its main use is to keep track of top-level actions that can be later replicated via applyAction
somewhere else (another machine, etc.).
There are two kinds of possible listeners, onStart
and onFinish
listeners.
onStart
listeners are called before the action executes and allow cancelation by returning a new return value (which might be a return or a throw).onFinish
listeners are called after the action executes, have access to the action's actual return value and allow overriding by returning a new return value (which might be a return or a throw).
The actions passed as arguments to the listener are not in a serializable format. If you want to ensure that the actual action calls are serializable you should use serializeActionCall
over the whole action before sending the action call over the wire / storing them and likewise use applySerializedActionAndTrackNewModelIds
(for the server) / applySerializedActionAndSyncNewModelIds
(for the clients) before applying it (as seen in the Client/Server Example).
It will return a disposer, which only needs to be called if you plan to early dispose of the middleware.
const disposer = onActionMiddleware(myTodoList, {
onStart(actionCall, actionContext) {
// we could serialize the action call and do something with it
const serializableActionCall = serializeActionCall(myTodoList, actionCall)
// optionally cancel the action by throwing something
return {
result: ActionTrackingResult.Throw,
value: new Error("whatever"),
}
// or by returning a different value
return {
result: ActionTrackingResult.Return,
value: 42,
}
// or do nothing / return `undefined` to continue it
},
onFinish(actionCall, actionContext, ret) {
if (ret.result === ActionTrackingResult.Return) {
// the action succeeded and `ret.value` has the return value
} else if (ret.result === ActionTrackingResult.Throw) {
// the action threw and `ret.value` has the thrown value
}
// as in above, we can either return an object with what to return / throw
// or do nothing / return `undefined` to continue the action
},
})
Action serialization with custom types as arguments
Action serialization (via serializeActionCall
and deserializeActionCall
) supports many cases by default:
- Primitives (including
undefined
,bigint
and specialnumber
valuesNaN
/+Infinity
/-Infinity
, but notsymbol
). - Tree nodes as paths if they are under the same root node as the model that holds the action being called.
- Tree nodes as snapshots if not.
- Arrays and observable arrays.
Date
objects as timestamps.- Maps and observable maps.
- Sets and observable sets.
- Plain objects, observable or not.
However, you might want to serialize an action that passes your custom type as an argument. In this case you can register a custom action serializer:
const myTypeSerializer: ActionCallArgumentSerializer<MyType, JsonCompatibleType> = {
id: "someSerializerUniqueId",
serialize(valueToSerialize, serializeChild, targetRoot) {
if (valueToSerialize instanceof MyType) {
return someJsonCompatibleValue
}
// let other serializer handle it
return cannotSerialize
},
deserialize(someJsonCompatibleValue, deserializeChild, targetRoot) {
// return back `MyType` from the JSON compatible value
},
}
registerActionCallArgumentSerializer(myTypeSerializer)
In this case, whenever an instance of MyType
is found as an action argument, then (after using serializeActionCall
on the action call) the action argument will be serialized as a SerializedActionCallArgument
:
{
$mobxKeystoneSerializer: "someSerializerUniqueId",
value: someJsonCompatibleValue
}
Likewise, using deserializeActionCall
will transform it back to an instance of MyType
.