UNPKG

mobx-keystone

Version:

A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more

120 lines (107 loc) 3.75 kB
import { checkDecoratorContext } from "../utils/decorators" import type { ActionMiddlewareDisposer } from "../action/middleware" import type { AnyModel } from "../model/BaseModel" import { assertIsModel } from "../model/utils" import { addModelClassInitializer } from "../modelShared/modelClassInitializer" import { applyPatches } from "../patch" import { internalPatchRecorder, PatchRecorder } from "../patch/patchRecorder" import { assertIsObject, failure } from "../utils" import { actionTrackingMiddleware, ActionTrackingResult, SimpleActionContext, } from "./actionTrackingMiddleware" /** * Creates a transaction middleware, which reverts changes made by an action / child * actions when the root action throws an exception by applying inverse patches. * * @template M Model * @param target Object with the root target model object (`model`) and root action name (`actionName`). * @returns The middleware disposer. */ export function transactionMiddleware<M extends AnyModel>(target: { model: M actionName: keyof M }): ActionMiddlewareDisposer { assertIsObject(target, "target") const { model, actionName } = target assertIsModel(model, "target.model") if (typeof actionName !== "string") { throw failure("target.actionName must be a string") } const patchRecorderSymbol = Symbol("patchRecorder") function initPatchRecorder(ctx: SimpleActionContext) { ctx.rootContext.data[patchRecorderSymbol] = internalPatchRecorder(undefined, { recording: false, }) } function getPatchRecorder(ctx: SimpleActionContext): PatchRecorder { return ctx.rootContext.data[patchRecorderSymbol] } return actionTrackingMiddleware(model, { filter(ctx) { // the primary action must be on the root object const rootContext = ctx.rootContext return rootContext.target === model && rootContext.actionName === actionName }, onStart(ctx) { if (ctx === ctx.rootContext) { initPatchRecorder(ctx) } }, onResume(ctx) { getPatchRecorder(ctx).recording = true }, onSuspend(ctx) { getPatchRecorder(ctx).recording = false }, onFinish(ctx, ret) { if (ctx === ctx.rootContext) { const patchRecorder = getPatchRecorder(ctx) try { if (ret.result === ActionTrackingResult.Throw) { // undo changes (backwards for inverse patches) const { events } = patchRecorder for (let i = events.length - 1; i >= 0; i--) { const event = events[i] applyPatches(event.target, event.inversePatches, true) } } } finally { patchRecorder.dispose() } } }, }) } /** * Transaction middleware as a decorator. */ export function transaction(...args: any[]): void { if (typeof args[1] === "object") { // standard decorators const ctx = args[1] as ClassMethodDecoratorContext | ClassFieldDecoratorContext checkDecoratorContext("transaction", ctx.name, ctx.static) if (ctx.kind !== "method" && ctx.kind !== "field") { throw failure(`@transaction can only be used on fields or methods}`) } ctx.addInitializer(function (this: any) { const modelInstance = this transactionMiddleware({ model: modelInstance as AnyModel, actionName: ctx.name as any, }) }) } else { // non-standard decorators const target = args[0] const propertyKey: string | symbol = args[1] checkDecoratorContext("transaction", propertyKey, false) addModelClassInitializer(target.constructor, (modelInstance) => { transactionMiddleware({ model: modelInstance as AnyModel, actionName: propertyKey as any, }) }) } }