mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
138 lines (117 loc) • 4.3 kB
text/typescript
import { fastGetParent } from "../parent/path"
import { isChildOfParent } from "../parent/path2"
import { assertTweakedObject } from "../tweaker/core"
import { assertIsFunction, assertIsObject, deleteFromArray, failure } from "../utils"
import type { ActionContext } from "./context"
/**
* An action middleware.
*/
export interface ActionMiddleware {
/**
* Subtree root object (object and child objects) this middleware will run for.
* This target "filter" will be run before the custom filter.
*/
readonly subtreeRoot: object
/**
* A filter function to decide if an action middleware function should be run or not.
*/
filter?: (ctx: ActionContext) => boolean
/**
* An action middleware function.
* Rember to `return next()` if you want to continue the action or throw if you want to cancel it.
*/
middleware: (ctx: ActionContext, next: () => any) => any
}
/**
* The disposer of an action middleware.
*/
export type ActionMiddlewareDisposer = () => void
type PartialActionMiddleware = Pick<ActionMiddleware, "filter" | "middleware">
const perObjectActionMiddlewares = new WeakMap<object, PartialActionMiddleware[]>()
/**
* @internal
*
* Runs a callback for each action middlewares to be run over a given object.
*
* @returns
*/
export function forEachActionMiddleware(
obj: object,
callback: (middleware: PartialActionMiddleware) => void
): void {
// when we call a middleware we will call the middlewares of that object plus all parent objects
// the parent object middlewares are run last
// since an array like [a, b, c] will be called like c(b(a())) this means that we need to call
// the parent object ones at the end of the array
let current: unknown = obj
while (current) {
const objMwares = perObjectActionMiddlewares.get(current)
if (objMwares && objMwares.length > 0) {
objMwares.forEach(callback)
}
current = fastGetParent(current, false)
}
}
/**
* @internal
*
* Returns the action middlewares to be run over a given object.
*
* @returns
*/
export function getPerObjectActionMiddlewares(obj: object): PartialActionMiddleware[][] {
const result: PartialActionMiddleware[][] = []
let current: unknown = obj
while (current) {
const objMwares = perObjectActionMiddlewares.get(current)
if (objMwares && objMwares.length > 0) {
result.push(objMwares)
}
current = fastGetParent(current, false)
}
return result
}
/**
* Adds a global action middleware to be run when an action is performed.
* It is usually preferable to use `onActionMiddleware` instead to limit it to a given tree and only to topmost level actions
* or `actionTrackingMiddleware` for a simplified middleware.
*
* @param mware Action middleware to be run.
* @returns A disposer to cancel the middleware. Note that if you don't plan to do an early disposal of the middleware
* calling this function becomes optional.
*/
export function addActionMiddleware(mware: ActionMiddleware): ActionMiddlewareDisposer {
assertIsObject(mware, "middleware")
const { middleware, subtreeRoot } = mware
let { filter } = mware
assertTweakedObject(subtreeRoot, "middleware.subtreeRoot")
assertIsFunction(middleware, "middleware.middleware")
if (filter && typeof filter !== "function") {
throw failure("middleware.filter must be a function or undefined")
}
// reminder: never turn middlewares into actions or else
// reactions will not be picked up by the undo manager
if (subtreeRoot) {
const targetFilter = (ctx: ActionContext) =>
ctx.target === subtreeRoot || isChildOfParent(ctx.target, subtreeRoot)
if (filter) {
const customFilter = filter
filter = (ctx) => {
return targetFilter(ctx) && customFilter(ctx)
}
} else {
filter = targetFilter
}
}
const actualMware = { middleware, filter }
let objMwares = perObjectActionMiddlewares.get(subtreeRoot)
if (objMwares) {
objMwares.push(actualMware)
} else {
objMwares = [actualMware]
perObjectActionMiddlewares.set(subtreeRoot, objMwares)
}
return () => {
deleteFromArray(objMwares, actualMware)
}
}