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
364 lines (314 loc) • 10.2 kB
text/typescript
import {
IArrayWillChange,
IArrayWillSplice,
intercept,
IObservableArray,
isObservableArray,
observable,
observe,
set,
} from "mobx"
import { assertCanWrite } from "../action/protection"
import { getGlobalConfig } from "../globalConfig"
import type { ParentPath } from "../parent/path"
import { setParent } from "../parent/setParent"
import { InternalPatchRecorder } from "../patch/emitPatch"
import type { Patch } from "../patch/Patch"
import { getInternalSnapshot, setInternalSnapshot } from "../snapshot/internal"
import { failure, inDevMode, isArray, 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 tweakArray<T extends any[]>(
value: T,
parentPath: ParentPath<any> | undefined,
doNotTweakChildren: boolean
): T {
const originalArr: ReadonlyArray<any> = value
const arrLn = originalArr.length
const tweakedArr = isObservableArray(originalArr)
? originalArr
: observable.array([], observableOptions)
if (tweakedArr !== originalArr) {
tweakedArr.length = originalArr.length
}
let interceptDisposer: () => void
let observeDisposer: () => void
const untweak = () => {
interceptDisposer()
observeDisposer()
}
tweakedObjects.set(tweakedArr, untweak)
setParent({
value: tweakedArr,
parentPath,
indexChangeAllowed: false,
isDataObject: false,
// arrays shouldn't be cloned anyway
cloneIfApplicable: false,
})
const standardSn: any[] = []
standardSn.length = arrLn
// substitute initial values by proxied values
for (let i = 0; i < arrLn; i++) {
const v = originalArr[i]
if (isPrimitive(v)) {
if (!doNotTweakChildren) {
set(tweakedArr, i, v)
}
standardSn[i] = v
} else {
const path = { parent: tweakedArr, path: i }
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(tweakedArr, i, tweakedValue)
}
const valueSn = getInternalSnapshot(tweakedValue)!
standardSn[i] = valueSn.standard
}
}
setInternalSnapshot(tweakedArr, standardSn)
interceptDisposer = intercept(tweakedArr, interceptArrayMutation.bind(undefined, tweakedArr))
observeDisposer = observe(tweakedArr, arrayDidChange)
return tweakedArr as any
}
function arrayDidChange(change: any /*IArrayDidChange*/) {
const arr = change.object
let { standard: oldSnapshot } = getInternalSnapshot(arr as Array<any>)!
const patchRecorder = new InternalPatchRecorder()
const newSnapshot = oldSnapshot.slice()
switch (change.type) {
case "splice":
{
const index = change.index
const addedCount = change.addedCount
const removedCount = change.removedCount
let addedItems = []
addedItems.length = addedCount
for (let i = 0; i < addedCount; i++) {
const v = change.added[i]
if (isPrimitive(v)) {
addedItems[i] = v
} else {
addedItems[i] = getInternalSnapshot(v)!.standard
}
}
const oldLen = oldSnapshot.length
newSnapshot.splice(index, removedCount, ...addedItems)
const patches: Patch[] = []
const invPatches: Patch[] = []
// optimization: if we add as many as we remove then replace instead
if (addedCount === removedCount) {
for (let i = 0; i < addedCount; i++) {
const realIndex = index + i
const newVal = newSnapshot[realIndex]
const oldVal = oldSnapshot[realIndex]
if (newVal !== oldVal) {
const path = [realIndex]
// replace 0, 1, 2...
patches.push({
op: "replace",
path,
value: newVal,
})
// replace ...2, 1, 0 since inverse patches are applied in reverse
invPatches.push({
op: "replace",
path,
value: oldVal,
})
}
}
} else {
const interimLen = oldLen - removedCount
// first remove items
if (removedCount > 0) {
// optimization, when removing from the end set the length instead
const removeUsingSetLength = index >= interimLen
if (removeUsingSetLength) {
patches.push({
op: "replace",
path: ["length"],
value: interimLen,
})
}
for (let i = removedCount - 1; i >= 0; i--) {
const realIndex = index + i
const path = [realIndex]
if (!removeUsingSetLength) {
// remove ...2, 1, 0
patches.push({
op: "remove",
path,
})
}
// add 0, 1, 2... since inverse patches are applied in reverse
invPatches.push({
op: "add",
path,
value: oldSnapshot[realIndex],
})
}
}
// then add items
if (addedCount > 0) {
// optimization, for inverse patches, when adding from the end set the length to restore instead
const restoreUsingSetLength = index >= interimLen
if (restoreUsingSetLength) {
invPatches.push({
op: "replace",
path: ["length"],
value: interimLen,
})
}
for (let i = 0; i < addedCount; i++) {
const realIndex = index + i
const path = [realIndex]
// add 0, 1, 2...
patches.push({
op: "add",
path,
value: newSnapshot[realIndex],
})
// remove ...2, 1, 0 since inverse patches are applied in reverse
if (!restoreUsingSetLength) {
invPatches.push({
op: "remove",
path,
})
}
}
}
}
patchRecorder.record(patches, invPatches)
}
break
case "update":
{
const k = change.index
const val = change.newValue
const oldVal = newSnapshot[k]
if (isPrimitive(val)) {
newSnapshot[k] = val
} else {
const valueSn = getInternalSnapshot(val)!
newSnapshot[k] = valueSn.standard
}
const path = [k]
patchRecorder.record(
[
{
op: "replace",
path,
value: newSnapshot[k],
},
],
[
{
op: "replace",
path,
value: oldVal,
},
]
)
}
break
}
runTypeCheckingAfterChange(arr, patchRecorder)
if (!runningWithoutSnapshotOrPatches) {
setInternalSnapshot(arr, newSnapshot)
patchRecorder.emit(arr)
}
}
const undefinedInsideArrayErrorMsg =
"undefined is not supported inside arrays since it is not serializable in JSON, consider using null instead"
// TODO: remove array parameter and just use change.object once mobx update event is fixed
function interceptArrayMutation(
array: IObservableArray,
change: IArrayWillChange | IArrayWillSplice
) {
assertCanWrite()
switch (change.type) {
case "splice":
{
if (inDevMode() && !getGlobalConfig().allowUndefinedArrayElements) {
const len = change.added.length
for (let i = 0; i < len; i++) {
const v = change.added[i]
if (v === undefined) {
throw failure(undefinedInsideArrayErrorMsg)
}
}
}
for (let i = 0; i < change.removedCount; i++) {
const removedValue = change.object[change.index + i]
tweak(removedValue, undefined)
tryUntweak(removedValue)
}
for (let i = 0; i < change.added.length; i++) {
change.added[i] = tweak(change.added[i], {
parent: change.object,
path: change.index + i,
})
}
// we might also need to update the parent of the next indexes
const oldNextIndex = change.index + change.removedCount
const newNextIndex = change.index + change.added.length
if (oldNextIndex !== newNextIndex) {
for (let i = oldNextIndex, j = newNextIndex; i < change.object.length; i++, j++) {
setParent({
value: change.object[i],
parentPath: {
parent: change.object,
path: j,
},
indexChangeAllowed: true,
isDataObject: false,
// just re-indexing
cloneIfApplicable: false,
})
}
}
}
break
case "update":
if (
inDevMode() &&
!getGlobalConfig().allowUndefinedArrayElements &&
change.newValue === undefined
) {
throw failure(undefinedInsideArrayErrorMsg)
}
// TODO: should be change.object, but mobx is bugged and doesn't send the proxy
const oldVal = array[change.index]
tweak(oldVal, undefined) // set old prop obj parent to undefined
tryUntweak(oldVal)
change.newValue = tweak(change.newValue, { parent: array, path: change.index })
break
}
return change
}
registerTweaker(TweakerPriority.Array, (value, parentPath) => {
if (isArray(value)) {
return tweakArray(value, parentPath, false)
}
return undefined
})
const observableOptions = {
deep: false,
}