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
text/typescript
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
}
}