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

179 lines (159 loc) 4.83 kB
import { remove } from "mobx" import { BuiltInAction } from "../action/builtInActions" import { ActionContextActionType } from "../action/context" import { wrapInAction } from "../action/wrapInAction" import { modelToDataNode } from "../parent/core" import type { PathElement } from "../parent/pathTypes" import type { Patch } from "../patch/Patch" import { reconcileSnapshot } from "../snapshot/reconcileSnapshot" import { assertTweakedObject } from "../tweaker/core" import { failure, inDevMode, isArray, lazy } from "../utils" import { ModelPool } from "../utils/ModelPool" import { setIfDifferent } from "../utils/setIfDifferent" /** * Applies the given patches to the given target object. * * @param node Target object. * @param patches List of patches to apply. * @param reverse Whether patches are applied in reverse order. */ export function applyPatches( node: object, patches: ReadonlyArray<Patch> | ReadonlyArray<ReadonlyArray<Patch>>, reverse = false ): void { assertTweakedObject(node, "node") if (patches.length <= 0) { return } wrappedInternalApplyPatches().call(node, patches, reverse) } /** * @internal */ export function internalApplyPatches( this: object, patches: ReadonlyArray<Patch> | ReadonlyArray<ReadonlyArray<Patch>>, reverse = false ): void { const obj = this const modelPool = new ModelPool(obj) if (reverse) { let i = patches.length while (i--) { const p = patches[i] if (isArray(p)) { let j = p.length while (j--) { applySinglePatch(obj, p[j], modelPool) } } else { applySinglePatch(obj, p as Patch, modelPool) } } } else { const len = patches.length for (let i = 0; i < len; i++) { const p = patches[i] if (isArray(p)) { const len2 = p.length for (let j = 0; j < len2; j++) { applySinglePatch(obj, p[j], modelPool) } } else { applySinglePatch(obj, p as Patch, modelPool) } } } } const wrappedInternalApplyPatches = lazy(() => wrapInAction({ nameOrNameFn: BuiltInAction.ApplyPatches, fn: internalApplyPatches, actionType: ActionContextActionType.Sync, }) ) function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void { const { target, prop } = pathArrayToObjectAndProp(obj, patch.path) if (isArray(target)) { switch (patch.op) { case "add": { const index = +prop! // reconcile from the pool if possible const newValue = reconcileSnapshot(undefined, patch.value, modelPool, target) if (index < 0) { // extension needed by mobx-keystone-yjs target.push(newValue) } else { target.splice(index, 0, newValue) } break } case "remove": { const index = +prop! // no reconciliation, removing target.splice(index, 1) break } case "replace": { if (prop === "length") { target.length = patch.value } else { const index = +prop! // try to reconcile const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target) setIfDifferent(target, index as any, newValue) } break } default: throw failure(`unsupported patch operation: ${(patch as any).op}`) } } else { switch (patch.op) { case "add": { // reconcile from the pool if possible const newValue = reconcileSnapshot(undefined, patch.value, modelPool, target) setIfDifferent(target, prop!, newValue) break } case "remove": { // no reconciliation, removing remove(target, prop) break } case "replace": { // try to reconcile // we don't need to tweak the pool since reconcileSnapshot will do that for us const newValue = reconcileSnapshot(target[prop!], patch.value, modelPool, target) setIfDifferent(target, prop!, newValue) break } default: throw failure(`unsupported patch operation: ${(patch as any).op}`) } } } function pathArrayToObjectAndProp( obj: object, path: Patch["path"] ): { target: any; prop?: PathElement } { if (inDevMode) { if (!isArray(path)) { throw failure(`invalid path: ${path}`) } } let target: any = modelToDataNode(obj) if (path.length === 0) { return { target, } } for (let i = 0; i <= path.length - 2; i++) { target = modelToDataNode(target[path[i]]) } return { target, prop: path[path.length - 1], } }