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

145 lines (126 loc) 3.76 kB
import { action } from "mobx" import type { O } from "ts-toolbelt" import type { AnyDataModel } from "../dataModel/BaseDataModel" import type { AnyModel } from "../model/BaseModel" import { ActionContext, ActionContextActionType, getCurrentActionContext, setCurrentActionContext, } from "./context" import { isModelAction, modelActionSymbol } from "./isModelAction" import { getActionMiddlewares } from "./middleware" import type { FlowFinisher } from "./modelFlow" import { tryRunPendingActions } from "./pendingActions" /** * @ignore */ export type WrapInActionOverrideContextFn = (ctx: O.Writable<ActionContext>, self: any) => void /** * @ignore */ export function wrapInAction<T extends Function>({ nameOrNameFn, fn, actionType, overrideContext, isFlowFinisher = false, }: { nameOrNameFn: string | (() => string) fn: T actionType: ActionContextActionType overrideContext?: WrapInActionOverrideContextFn isFlowFinisher?: boolean }): T { let fnInAction = false const wrappedAction = function (this: any) { const name = typeof nameOrNameFn === "function" ? nameOrNameFn() : nameOrNameFn if (!fnInAction) { fnInAction = true // we need to make only inner actions actual mobx actions // so reactions (e.g. reference detaching) are picked up in the // right context fn = action(name, fn) } const target = this const parentContext = getCurrentActionContext() const context: O.Writable<ActionContext> = { actionName: name, type: actionType, target, args: Array.from(arguments), parentContext, data: {}, rootContext: undefined as any, // will be set after the override } if (overrideContext) { overrideContext(context, this) } if (!context.rootContext) { if (context.previousAsyncStepContext) { context.rootContext = context.previousAsyncStepContext.rootContext } else if (context.parentContext) { context.rootContext = context.parentContext.rootContext } else { context.rootContext = context } } setCurrentActionContext(context) let mwareFn: () => any = fn.bind(target, ...arguments) const mwareIter = getActionMiddlewares(context.target)[Symbol.iterator]() let mwareCur = mwareIter.next() while (!mwareCur.done) { const mware = mwareCur.value const filterPassed = mware.filter ? mware.filter(context) : true if (filterPassed) { mwareFn = mware.middleware.bind(undefined, context, mwareFn) } mwareCur = mwareIter.next() } try { const ret = mwareFn() if (isFlowFinisher) { const flowFinisher = ret as FlowFinisher const value = flowFinisher.value if (flowFinisher.resolution === "accept") { flowFinisher.accepter(value) } else { flowFinisher.rejecter(value) } return value // not sure if this is even needed } else { return ret } } finally { setCurrentActionContext(context.parentContext) tryRunPendingActions() } } ;(wrappedAction as any)[modelActionSymbol] = true return wrappedAction as any } /** * @ignore */ export function wrapModelMethodInActionIfNeeded<M extends AnyModel | AnyDataModel>( model: M, propertyKey: keyof M, name: string ): void { const fn = model[propertyKey] as any if (isModelAction(fn)) { return } const wrappedFn = wrapInAction({ nameOrNameFn: name, fn, actionType: ActionContextActionType.Sync, }) const proto = Object.getPrototypeOf(model) const protoFn = proto[propertyKey] if (protoFn === fn) { proto[propertyKey] = wrappedFn } else { model[propertyKey] = wrappedFn } }