mobx-keystone-mindreframer
Version:
A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more
160 lines (132 loc) • 4.84 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[]>()
interface ActionMiddlewaresIterator extends Iterable<PartialActionMiddleware> {}
const perObjectActionMiddlewaresIterator = new WeakMap<object, ActionMiddlewaresIterator>()
/**
* @ignore
* @internal
*
* Gets the current action middlewares to be run over a given object as an iterable object.
*
* @returns
*/
export function getActionMiddlewares(obj: object): ActionMiddlewaresIterator {
// 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 put
// the parent object ones at the end of the array
let iterable = perObjectActionMiddlewaresIterator.get(obj)
if (!iterable) {
iterable = {
[Symbol.iterator]() {
let current: any = obj
function getCurrentIterator() {
const objMwares = current ? perObjectActionMiddlewares.get(current) : undefined
if (!objMwares || objMwares.length <= 0) {
return undefined
}
return objMwares[Symbol.iterator]()
}
function findNextIterator() {
let nextIter
while (current && !nextIter) {
current = fastGetParent(current)
nextIter = getCurrentIterator()
}
return nextIter
}
let iter = getCurrentIterator()
if (!iter) {
iter = findNextIterator()
}
const iterator: Iterator<PartialActionMiddleware> = {
next() {
if (!iter) {
return { value: undefined, done: true } as any
}
let result = iter.next()
if (!result.done) {
return result
}
iter = findNextIterator()
return this.next()
},
}
return iterator
},
}
perObjectActionMiddlewaresIterator.set(obj, iterable)
}
return iterable
}
/**
* 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")
let { middleware, filter, subtreeRoot } = 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) {
filter = targetFilter
} else {
const customFilter = filter
filter = (ctx) => {
return targetFilter(ctx) && customFilter(ctx)
}
}
}
const actualMware = { middleware, filter }
let objMwares = perObjectActionMiddlewares.get(subtreeRoot)!
if (!objMwares) {
objMwares = [actualMware]
perObjectActionMiddlewares.set(subtreeRoot, objMwares)
} else {
objMwares.push(actualMware)
}
return () => {
deleteFromArray(objMwares, actualMware)
}
}