mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
135 lines (115 loc) • 4.82 kB
text/typescript
import { remove } from "mobx"
import type { AnyModel } from "../model/BaseModel"
import { getModelIdPropertyName } from "../model/getModelMetadata"
import { isReservedModelKey, modelIdKey, modelTypeKey } from "../model/metadata"
import { isModel, isModelSnapshot } from "../model/utils"
import type { ModelClass } from "../modelShared/BaseModelShared"
import { getModelInfoForName } from "../modelShared/modelInfo"
import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
import { AnyModelProp, getModelPropDefaultValue, noDefaultValue } from "../modelShared/prop"
import { deepEquals } from "../treeUtils/deepEquals"
import { runTypeCheckingAfterChange } from "../tweaker/typeChecking"
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
import { failure, isArray } from "../utils"
import type { ModelPool } from "../utils/ModelPool"
import { setIfDifferent } from "../utils/setIfDifferent"
import type { SnapshotInOfModel } from "./SnapshotOf"
import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
import { fromSnapshot } from "./fromSnapshot"
import { getSnapshot } from "./getSnapshot"
import { detachIfNeeded, reconcileSnapshot, registerReconciler } from "./reconcileSnapshot"
function reconcileModelSnapshot(
value: any,
sn: SnapshotInOfModel<AnyModel>,
modelPool: ModelPool,
parent: any
): AnyModel {
const type = sn[modelTypeKey]!
const modelInfo = getModelInfoForName(type)
if (!modelInfo) {
throw failure(`model with name "${type}" not found in the registry`)
}
// try to use model from pool if possible
const modelInPool = modelPool.findModelForSnapshot(sn)
if (modelInPool) {
value = modelInPool
}
// we don't check by actual instance since the class might be a different one due to hot reloading
if (!isModel(value) || value[modelTypeKey] !== type) {
// different kind of model type, no reconciliation possible
return fromSnapshot<AnyModel>(sn)
}
const modelClass = modelInfo.class as ModelClass<AnyModel>
const modelProps = getInternalModelClassPropsInfo(modelClass)
const modelIdPropertyName = getModelIdPropertyName(modelClass)
if (modelIdPropertyName) {
const id = sn[modelIdPropertyName]
if (value[modelIdKey] !== id) {
// different id, no reconciliation possible
return fromSnapshot<AnyModel>(sn)
}
} else if (isArray(parent)) {
// no id and inside an array? no reconciliation possible,
// unless the snapshots are equivalent (note deep equals will use the snapshot of value auto)
if (!deepEquals(value, sn)) {
return fromSnapshot<AnyModel>(sn)
}
}
const modelObj: AnyModel = value
const snapshotBeforeChanges = getSnapshot(modelObj)
withoutTypeChecking(() => {
const modelClass: ModelClass<AnyModel> = (modelObj as any).constructor
const processedSn = modelClass.fromSnapshotProcessor ? modelClass.fromSnapshotProcessor(sn) : sn
const data = modelObj.$
// remove excess props
const dataKeys = Object.keys(data)
const dataKeysLen = dataKeys.length
for (let i = 0; i < dataKeysLen; i++) {
const k = dataKeys[i]
if (!(k in processedSn)) {
// use default value if applicable
const modelProp = modelProps[k] as AnyModelProp | undefined
const defaultValue = modelProp ? getModelPropDefaultValue(modelProp) : noDefaultValue
if (defaultValue === noDefaultValue) {
remove(data, k)
} else {
setIfDifferent(data, k, defaultValue)
}
}
}
// reconcile the rest
const processedSnKeys = Object.keys(processedSn)
const processedSnKeysLen = processedSnKeys.length
for (let i = 0; i < processedSnKeysLen; i++) {
const k = processedSnKeys[i]
if (!isReservedModelKey(k)) {
const v = processedSn[k]
const oldValue = data[k]
let newValue = reconcileSnapshot(oldValue, v, modelPool, modelObj)
// use default value if applicable
if (newValue == null) {
const modelProp = modelProps[k] as AnyModelProp | undefined
const defaultValue = modelProp ? getModelPropDefaultValue(modelProp) : noDefaultValue
if (defaultValue !== noDefaultValue) {
newValue = defaultValue
}
}
detachIfNeeded(newValue, oldValue, modelPool)
setIfDifferent(data, k, newValue)
}
}
})
runTypeCheckingAfterChange(modelObj, undefined, snapshotBeforeChanges)
return modelObj
}
/**
* @internal
*/
export function registerModelSnapshotReconciler() {
registerReconciler(SnapshotterAndReconcilerPriority.Model, (value, sn, modelPool, parent) => {
if (isModelSnapshot(sn)) {
return reconcileModelSnapshot(value, sn, modelPool, parent)
}
return undefined
})
}