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