UNPKG

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
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) } }