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

101 lines (89 loc) 2.82 kB
import { isHookAction } from "../action/hookActions" import type { ActionMiddlewareDisposer } from "../action/middleware" import { assertTweakedObject } from "../tweaker/core" import { failure } from "../utils" import { actionTrackingMiddleware, ActionTrackingResult, SimpleActionContext, } from "./actionTrackingMiddleware" /** * Return type for readonly middleware. */ export interface ReadonlyMiddlewareReturn { allowWrite: <R>(fn: () => R) => R dispose: ActionMiddlewareDisposer } /** * Attaches an action middleware that will throw when any action is started * over the node or any of the child nodes, thus effectively making the subtree * readonly. * * It will return an object with a `dispose` function to remove the middleware and a `allowWrite` function * that will allow actions to be started inside the provided code block. * * Example: * ```ts * // given a model instance named todo * const { dispose, allowWrite } = readonlyMiddleware(todo) * * // this will throw * todo.setDone(false) * await todo.setDoneAsync(false) * * // this will work * allowWrite(() => todo.setDone(false)) * // note: for async always use one action invocation per allowWrite! * await allowWrite(() => todo.setDoneAsync(false)) * ``` * * @param subtreeRoot Subtree root target object. * @returns An object with the middleware disposer (`dispose`) and a `allowWrite` function. */ export function readonlyMiddleware(subtreeRoot: object): ReadonlyMiddlewareReturn { assertTweakedObject(subtreeRoot, "subtreeRoot") let writable = false const writableSymbol = Symbol("writable") const disposer = actionTrackingMiddleware(subtreeRoot, { filter(ctx) { // skip hooks if (isHookAction(ctx.actionName)) { return false } // if we are inside allowWrite it is writable let currentlyWritable = writable if (!currentlyWritable) { // if a parent context was writable then the child should be as well let currentCtx: SimpleActionContext | undefined = ctx while (currentCtx && !currentlyWritable) { currentlyWritable = !!currentCtx.data[writableSymbol] currentCtx = currentCtx.parentContext } } if (currentlyWritable) { ctx.data[writableSymbol] = true return false } return true }, onStart(ctx) { // if we get here (wasn't filtered out) it is not writable return { result: ActionTrackingResult.Throw, value: failure(`tried to invoke action '${ctx.actionName}' over a readonly node`), } }, }) return { dispose: disposer, allowWrite(fn) { const oldWritable = writable writable = true try { return fn() } finally { writable = oldWritable } }, } }