mobx-bonsai
Version:
A fast lightweight alternative to MobX-State-Tree + Y.js two-way binding
102 lines (95 loc) • 3.4 kB
text/typescript
import { action, isObservableArray, isObservableObject, remove, set } from "mobx"
import { failure } from "../error/failure"
import { assertIsNode } from "../node/node"
import { reconcileData } from "../node/reconcileData"
import { resolvePath } from "../node/tree/resolvePath"
import type { UndoableChange } from "./types"
/**
* Applies a single change in forward or reverse mode.
* Used by UndoManager during undo/redo operations.
*
* @param rootNode - The tracked root node
* @param change - The change to apply
* @param mode - Whether to apply the change forward (redo) or reverse (undo)
*/
export const applyChange = action(
(rootNode: object, change: UndoableChange, mode: "forward" | "reverse"): void => {
// Resolve the target object/array using the path
const result = resolvePath(rootNode, change.path)
if (!result.resolved) {
throw failure(`cannot resolve path: ${change.path.join(".")}`)
}
const target = result.value as any
// Ensure target is a node
assertIsNode(target, "target")
// Validate target type matches operation type
if (change.operation.startsWith("object-")) {
if (!isObservableObject(target)) {
throw failure(
`cannot apply ${change.operation} to non-object target at path: ${change.path.join(".")}`
)
}
} else if (change.operation.startsWith("array-")) {
if (!isObservableArray(target)) {
throw failure(
`cannot apply ${change.operation} to non-array target at path: ${change.path.join(".")}`
)
}
}
if (mode === "reverse") {
// Apply changes in reverse (undo)
switch (change.operation) {
case "object-add":
remove(target, change.propertyName)
break
case "object-remove":
set(target, change.propertyName, reconcileData(undefined, change.oldValue, target))
break
case "object-update":
set(
target,
change.propertyName,
reconcileData(target[change.propertyName], change.oldValue, target)
)
break
case "array-splice":
target.splice(
change.index,
change.added.length,
...change.removed.map((val: any) => reconcileData(undefined, val, target))
)
break
case "array-update":
set(target, change.index, reconcileData(target[change.index], change.oldValue, target))
break
}
} else {
// Apply changes forward (redo)
switch (change.operation) {
case "object-add":
set(target, change.propertyName, reconcileData(undefined, change.newValue, target))
break
case "object-remove":
remove(target, change.propertyName)
break
case "object-update":
set(
target,
change.propertyName,
reconcileData(target[change.propertyName], change.newValue, target)
)
break
case "array-splice":
target.splice(
change.index,
change.removed.length,
...change.added.map((val: any) => reconcileData(undefined, val, target))
)
break
case "array-update":
set(target, change.index, reconcileData(target[change.index], change.newValue, target))
break
}
}
}
)