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