UNPKG

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

251 lines (218 loc) 6.12 kB
import { intercept, IObjectDidChange, IObjectWillChange, isObservableObject, observable, observe, set, } from "mobx" import { assertCanWrite } from "../action/protection" import { modelTypeKey } from "../model/metadata" import { dataToModelNode } from "../parent/core" import type { ParentPath } from "../parent/path" import { setParent } from "../parent/setParent" import { InternalPatchRecorder } from "../patch/emitPatch" import { getInternalSnapshot, setInternalSnapshot } from "../snapshot/internal" import { failure, isPlainObject, isPrimitive } from "../utils" import { runningWithoutSnapshotOrPatches, tweakedObjects } from "./core" import { registerTweaker, tryUntweak, tweak } from "./tweak" import { TweakerPriority } from "./TweakerPriority" import { runTypeCheckingAfterChange } from "./typeChecking" /** * @ignore */ export function tweakPlainObject<T>( value: T, parentPath: ParentPath<any> | undefined, snapshotModelType: string | undefined, doNotTweakChildren: boolean, isDataObject: boolean ): T { const originalObj: { [k: string]: any } = value const tweakedObj = isObservableObject(originalObj) ? originalObj : observable.object({}, undefined, observableOptions) let interceptDisposer: () => void let observeDisposer: () => void const untweak = () => { interceptDisposer() observeDisposer() } tweakedObjects.set(tweakedObj, untweak) setParent({ value: tweakedObj, parentPath, indexChangeAllowed: false, isDataObject, // an object shouldn't be cloned cloneIfApplicable: false, }) const standardSn: any = {} // substitute initial values by tweaked values const originalObjKeys = Object.keys(originalObj) const originalObjKeysLen = originalObjKeys.length for (let i = 0; i < originalObjKeysLen; i++) { const k = originalObjKeys[i] const v = originalObj[k] if (isPrimitive(v)) { if (!doNotTweakChildren) { set(tweakedObj, k, v) } standardSn[k] = v } else { const path = { parent: tweakedObj, path: k } let tweakedValue if (doNotTweakChildren) { tweakedValue = v setParent({ value: tweakedValue, parentPath: path, indexChangeAllowed: false, isDataObject: false, // the value is already a new value (the result of a fromSnapshot) cloneIfApplicable: false, }) } else { tweakedValue = tweak(v, path) set(tweakedObj, k, tweakedValue) } const valueSn = getInternalSnapshot(tweakedValue)! standardSn[k] = valueSn.standard } } if (snapshotModelType) { standardSn[modelTypeKey] = snapshotModelType } setInternalSnapshot(isDataObject ? dataToModelNode(tweakedObj) : tweakedObj, standardSn) interceptDisposer = intercept(tweakedObj, interceptObjectMutation) observeDisposer = observe(tweakedObj, objectDidChange) return tweakedObj as any } const observableOptions = { deep: false, } function objectDidChange(change: IObjectDidChange): void { const obj = change.object const actualNode = dataToModelNode(obj) let { standard: standardSn } = getInternalSnapshot(actualNode)! const patchRecorder = new InternalPatchRecorder() standardSn = Object.assign({}, standardSn) switch (change.type) { case "add": case "update": { const k = change.name const val = change.newValue const oldVal = standardSn[k] if (isPrimitive(val)) { standardSn[k] = val } else { const valueSn = getInternalSnapshot(val)! standardSn[k] = valueSn.standard } const path = [k as string] if (change.type === "add") { patchRecorder.record( [ { op: "add", path, value: standardSn[k], }, ], [ { op: "remove", path, }, ] ) } else { patchRecorder.record( [ { op: "replace", path, value: standardSn[k], }, ], [ { op: "replace", path, value: oldVal, }, ] ) } } break case "remove": { const k = change.name const oldVal = standardSn[k] delete standardSn[k] const path = [k as string] patchRecorder.record( [ { op: "remove", path, }, ], [ { op: "add", path, value: oldVal, }, ] ) } break } runTypeCheckingAfterChange(obj, patchRecorder) if (!runningWithoutSnapshotOrPatches) { setInternalSnapshot(actualNode, standardSn) patchRecorder.emit(actualNode) } } function interceptObjectMutation(change: IObjectWillChange) { assertCanWrite() if (typeof change.name === "symbol") { throw failure("symbol properties are not supported") } switch (change.type) { case "add": change.newValue = tweak(change.newValue, { parent: change.object, path: "" + (change.name as any), }) break case "remove": { const oldVal = change.object[change.name] tweak(oldVal, undefined) tryUntweak(oldVal) break } case "update": { const oldVal = change.object[change.name] tweak(oldVal, undefined) tryUntweak(oldVal) change.newValue = tweak(change.newValue, { parent: change.object, path: "" + (change.name as any), }) break } } return change } registerTweaker(TweakerPriority.PlainObject, (value, parentPath) => { // plain object if (isObservableObject(value) || isPlainObject(value)) { return tweakPlainObject(value, parentPath, undefined, false, false) } return undefined })