mobx-keystone
Advanced tools
Comparing version 1.3.0 to 1.4.0
# Change Log | ||
## 1.4.0 | ||
- Use a proxy to decorate model classes to reduce the inheritance level of models by one. | ||
- Create object children cache only when there are actually children to save some memory in objects/arrays that don't have children of type object. | ||
- Cache props/tProps to save some memory in model definitions. | ||
## 1.3.0 | ||
@@ -4,0 +10,0 @@ |
{ | ||
"name": "mobx-keystone", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"description": "A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more", | ||
@@ -61,3 +61,3 @@ "keywords": [ | ||
"test:perf:run": "cd perf_bench && NODE_ENV=production /usr/bin/time node --expose-gc --require ts-node/register ./report.ts", | ||
"memtest": "cd perf_bench &&node --expose-gc --require ts-node/register ./memtest.ts", | ||
"memtest": "cd perf_bench && node --expose-gc --require ts-node/register ./memtest.ts", | ||
"build-docs": "shx rm -rf api-docs && typedoc --options ./typedocconfig.js src/index.ts && shx rm -rf ../../apps/site/copy-to-build/api && shx mkdir -p ../../apps/site/copy-to-build/api && shx cp -R ./api-docs/* ../../apps/site/copy-to-build/api", | ||
@@ -70,13 +70,13 @@ "lint": "cd ../.. && yarn eslint \"packages/lib/src/**/*.ts\" \"packages/lib/test/**/*.ts\"" | ||
"devDependencies": { | ||
"@babel/core": "^7.20.12", | ||
"@babel/core": "^7.21.0", | ||
"@babel/plugin-proposal-class-properties": "^7.18.6", | ||
"@babel/plugin-proposal-decorators": "^7.20.13", | ||
"@babel/plugin-proposal-decorators": "^7.21.0", | ||
"@babel/preset-env": "^7.20.2", | ||
"@babel/preset-typescript": "^7.18.6", | ||
"@swc/core": "^1.3.35", | ||
"@babel/preset-typescript": "^7.21.0", | ||
"@swc/core": "^1.3.37", | ||
"@swc/jest": "^0.2.24", | ||
"@types/jest": "^29.4.0", | ||
"@types/node": "^18.13.0", | ||
"babel-jest": "^29.4.2", | ||
"jest": "^29.4.2", | ||
"@types/node": "^18.14.6", | ||
"babel-jest": "^29.4.3", | ||
"jest": "^29.4.3", | ||
"mobx": "^6.8.0", | ||
@@ -90,5 +90,5 @@ "mobx-v4": "npm:mobx@^4.15.7", | ||
"ts-node": "^10.9.1", | ||
"typedoc": "^0.23.25", | ||
"typedoc": "^0.23.26", | ||
"typescript": "^4.9.5", | ||
"vite": "^4.1.1" | ||
"vite": "^4.1.4" | ||
}, | ||
@@ -95,0 +95,0 @@ "dependencies": { |
@@ -0,0 +0,0 @@ import { observable } from "mobx" |
@@ -39,2 +39,59 @@ import { HookAction } from "../action/hookActions" | ||
const proxyClassHandlerTag = new WeakMap< | ||
ModelClass<AnyModel | AnyDataModel>, | ||
{ | ||
makeObservableFailed: boolean | ||
type: "class" | "data" | ||
} | ||
>() | ||
const proxyClassHandler: ProxyHandler<ModelClass<AnyModel | AnyDataModel>> = { | ||
construct(target, args) { | ||
const instance = new (target as any)(...args) | ||
runLateInitializationFunctions(instance, runAfterNewSymbol) | ||
// compatibility with mobx 6 | ||
const tag = proxyClassHandlerTag.get(target)! | ||
if (!tag.makeObservableFailed && getMobxVersion() >= 6) { | ||
try { | ||
mobx6.makeObservable(instance) | ||
} catch (e) { | ||
// sadly we need to use this hack since the PR to do this the proper way | ||
// was rejected on the mobx side | ||
tag.makeObservableFailed = true | ||
const err = e as Error | ||
if ( | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorator members have been found either" && | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorated members have been found either" | ||
) { | ||
throw err | ||
} | ||
} | ||
} | ||
// the object is ready | ||
addHiddenProp(instance, modelInitializedSymbol, true, false) | ||
runLateInitializationFunctions(instance, runBeforeOnInitSymbol) | ||
if (tag.type === "class" && instance.onInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onInit", HookAction.OnInit) | ||
instance.onInit() | ||
} | ||
if (tag.type === "data" && instance.onLazyInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onLazyInit", HookAction.OnLazyInit) | ||
instance.onLazyInit() | ||
} | ||
return instance | ||
}, | ||
} | ||
const internalModel = | ||
@@ -58,3 +115,3 @@ (name: string) => | ||
if (modelUnwrappedClassSymbol in clazz) { | ||
if (modelUnwrappedClassSymbol in clazz && clazz[modelUnwrappedClassSymbol] === clazz) { | ||
throw failure("a class already decorated with `@model` cannot be re-decorated") | ||
@@ -64,51 +121,7 @@ } | ||
// track if we fail so we only try it once per class | ||
let makeObservableFailed = false | ||
proxyClassHandlerTag.set(clazz, { makeObservableFailed: false, type }) | ||
// trick so plain new works | ||
const newClazz: any = function (this: any, initialData: any, modelConstructorOptions: any) { | ||
const instance = new (clazz as any)(initialData, modelConstructorOptions) | ||
const proxyClass = new Proxy<MC>(clazz, proxyClassHandler) | ||
runLateInitializationFunctions(instance, runAfterNewSymbol) | ||
// compatibility with mobx 6 | ||
if (!makeObservableFailed && getMobxVersion() >= 6) { | ||
try { | ||
mobx6.makeObservable(instance) | ||
} catch (e) { | ||
// sadly we need to use this hack since the PR to do this the proper way | ||
// was rejected on the mobx side | ||
makeObservableFailed = true | ||
const err = e as Error | ||
if ( | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorator members have been found either" && | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorated members have been found either" | ||
) { | ||
throw err | ||
} | ||
} | ||
} | ||
// the object is ready | ||
addHiddenProp(instance, modelInitializedSymbol, true, false) | ||
runLateInitializationFunctions(instance, runBeforeOnInitSymbol) | ||
if (type === "class" && instance.onInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onInit", HookAction.OnInit) | ||
instance.onInit() | ||
} | ||
if (type === "data" && instance.onLazyInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onLazyInit", HookAction.OnLazyInit) | ||
instance.onLazyInit() | ||
} | ||
return instance | ||
} | ||
clazz.toString = () => `class ${clazz.name}#${name}` | ||
@@ -119,17 +132,9 @@ if (type === "class") { | ||
// this also gives access to modelInitializersSymbol, modelPropertiesSymbol, modelDataTypeCheckerSymbol | ||
Object.setPrototypeOf(newClazz, clazz) | ||
newClazz.prototype = clazz.prototype | ||
// set or else it points to the undecorated class | ||
newClazz.prototype.constructor = newClazz | ||
proxyClass.prototype.constructor = proxyClass | ||
;(proxyClass as any)[modelUnwrappedClassSymbol] = clazz | ||
Object.defineProperty(newClazz, "name", { | ||
...Object.getOwnPropertyDescriptor(newClazz, "name"), | ||
value: clazz.name, | ||
}) | ||
newClazz[modelUnwrappedClassSymbol] = clazz | ||
const modelInfo = { | ||
name, | ||
class: newClazz, | ||
class: proxyClass, | ||
} | ||
@@ -139,3 +144,3 @@ | ||
modelInfoByClass.set(newClazz, modelInfo) | ||
modelInfoByClass.set(proxyClass, modelInfo) | ||
modelInfoByClass.set(clazz, modelInfo) | ||
@@ -145,3 +150,3 @@ | ||
return newClazz | ||
return proxyClass | ||
} | ||
@@ -148,0 +153,0 @@ |
@@ -23,3 +23,3 @@ import type { AnyDataModel } from "../dataModel/BaseDataModel" | ||
*/ | ||
export const modelInfoByClass = new Map<ModelClass<AnyModel | AnyDataModel>, ModelInfo>() | ||
export const modelInfoByClass = new WeakMap<ModelClass<AnyModel | AnyDataModel>, ModelInfo>() | ||
@@ -26,0 +26,0 @@ /** |
export const modelMetadataSymbol = Symbol("modelMetadata") | ||
export const modelUnwrappedClassSymbol = Symbol("modelUnwrappedClass") | ||
export const runAfterModelDecoratorSymbol = Symbol("runAfterModelDecorator") |
@@ -404,16 +404,26 @@ import type { SnapshotInOf, SnapshotOutOf } from "../snapshot/SnapshotOf" | ||
export function prop(def?: any): AnyModelProp { | ||
const obj: AnyModelProp = Object.create(baseProp) | ||
const hasDefaultValue = arguments.length >= 1 | ||
if (!hasDefaultValue) { | ||
return baseProp | ||
} | ||
const hasDefaultValue = arguments.length >= 1 | ||
if (hasDefaultValue) { | ||
let p = propCache.get(def) | ||
if (!p) { | ||
p = Object.create(baseProp) | ||
if (typeof def === "function") { | ||
obj._defaultFn = def | ||
p!._defaultFn = def | ||
} else { | ||
obj._defaultValue = def | ||
p!._defaultValue = def | ||
} | ||
propCache.set(def, p!) | ||
} | ||
return obj | ||
return p! | ||
} | ||
const propCache = new Map<unknown, AnyModelProp>() | ||
let cacheTransformResult = false | ||
@@ -420,0 +430,0 @@ const cacheTransformedValueFn = () => { |
@@ -29,3 +29,3 @@ import { applySet } from "../action/applySet" | ||
model: M, | ||
modelProp: AnyModelProp | undefined, | ||
modelProp: AnyModelProp, | ||
modelPropName: string | ||
@@ -36,11 +36,11 @@ ): any { | ||
if (modelProp?._transform) { | ||
return modelProp._transform.transform(value, model, modelPropName, (newValue) => { | ||
// use apply set instead to wrap it in an action | ||
// set the $ object to set the original value directly | ||
applySet(model.$, modelPropName, newValue) | ||
}) as any | ||
if (!modelProp._transform) { | ||
return value | ||
} | ||
return value | ||
return modelProp._transform.transform(value, model, modelPropName, (newValue) => { | ||
// use apply set instead to wrap it in an action | ||
// set the $ object to set the original value directly | ||
applySet(model.$, modelPropName, newValue) | ||
}) as any | ||
} | ||
@@ -50,3 +50,3 @@ | ||
model: M, | ||
modelProp: AnyModelProp | undefined, | ||
modelProp: AnyModelProp, | ||
modelPropName: string, | ||
@@ -62,3 +62,3 @@ value: any | ||
if (modelProp?._setter === "assign" && !getCurrentActionContext()) { | ||
if (modelProp._setter === "assign" && !getCurrentActionContext()) { | ||
// use apply set instead to wrap it in an action | ||
@@ -69,3 +69,3 @@ applySet(model, modelPropName as any, value) | ||
let untransformedValue = modelProp?._transform | ||
let untransformedValue = modelProp._transform | ||
? modelProp._transform.untransform(value, model, modelPropName) | ||
@@ -75,3 +75,3 @@ : value | ||
// apply default value if applicable | ||
if (modelProp && untransformedValue == null) { | ||
if (untransformedValue == null) { | ||
const defaultValue = getModelPropDefaultValue(modelProp) | ||
@@ -192,3 +192,3 @@ if (defaultValue !== noDefaultValue) { | ||
const base: any = baseModel ?? (type === "class" ? BaseModel : BaseDataModel) | ||
const basePropNames = base === BaseModel ? baseModelPropNames : baseDataModelPropNames | ||
const basePropNames = type === "class" ? baseModelPropNames : baseDataModelPropNames | ||
@@ -226,5 +226,2 @@ let propsToDeleteFromBase: string[] | undefined | ||
const newPrototype = Object.create(base.prototype) | ||
newPrototype.constructor = ThisModel | ||
const initializers = base[modelInitializersSymbol] | ||
@@ -251,2 +248,4 @@ if (initializers) { | ||
const newPrototype = Object.create(base.prototype) | ||
ThisModel.prototype = new Proxy(newPrototype, { | ||
@@ -263,2 +262,3 @@ get(target, p, receiver) { | ||
}, | ||
set(target, p, v, receiver) { | ||
@@ -277,2 +277,3 @@ if (receiver === ThisModel.prototype) { | ||
}, | ||
has(target, p) { | ||
@@ -284,2 +285,4 @@ const modelProp = !basePropNames.has(p as any) && composedModelProps[p as string] | ||
newPrototype.constructor = ThisModel | ||
// add setter actions to prototype | ||
@@ -290,12 +293,12 @@ for (const [propName, propData] of Object.entries(modelProps)) { | ||
newPrototype[setterName] = function (this: any, value: any) { | ||
this[propName] = value | ||
} | ||
const newPropDescriptor: any = modelAction(newPrototype, setterName, { | ||
value: function (this: any, value: any) { | ||
this[propName] = value | ||
}, | ||
writable: true, | ||
enumerable: false, | ||
configurable: true, | ||
}) | ||
const newPropDescriptor: any = modelAction( | ||
newPrototype, | ||
setterName, | ||
Object.getOwnPropertyDescriptor(newPrototype, setterName) | ||
) | ||
// we use define property to avoid the base proxy | ||
Object.defineProperty(newPrototype, setterName, newPropDescriptor) | ||
@@ -302,0 +305,0 @@ } |
@@ -1,14 +0,13 @@ | ||
import { action, createAtom, IAtom, observable, ObservableSet } from "mobx" | ||
import { action, createAtom, IAtom } from "mobx" | ||
import { fastGetParent } from "./path" | ||
const defaultObservableSetOptions = { deep: false } | ||
interface DeepObjectChildren { | ||
deep: Set<object> | ||
extensionsData: WeakMap<Symbol, any> | ||
extensionsData: WeakMap<object, any> | ||
} | ||
interface ObjectChildrenData extends DeepObjectChildren { | ||
shallow: ObservableSet<object> | ||
shallow: Set<object> | ||
shallowAtom: IAtom | ||
@@ -21,19 +20,20 @@ deepDirty: boolean | ||
/** | ||
* @internal | ||
*/ | ||
export function initializeObjectChildren(node: object) { | ||
if (objectChildren.has(node)) { | ||
return | ||
} | ||
function getObjectChildrenObject(node: object) { | ||
let obj = objectChildren.get(node) | ||
objectChildren.set(node, { | ||
shallow: observable.set(undefined, defaultObservableSetOptions), | ||
if (!obj) { | ||
obj = { | ||
shallow: new Set(), | ||
shallowAtom: createAtom("shallowChildrenAtom"), | ||
deep: new Set(), | ||
extensionsData: initExtensionsData(), | ||
deep: new Set(), | ||
deepDirty: true, | ||
deepAtom: createAtom("deepChildrenAtom"), | ||
deepDirty: true, | ||
deepAtom: createAtom("deepChildrenAtom"), | ||
}) | ||
extensionsData: initExtensionsData(), | ||
} | ||
objectChildren.set(node, obj) | ||
} | ||
return obj | ||
} | ||
@@ -45,3 +45,5 @@ | ||
export function getObjectChildren(node: object): ObjectChildrenData["shallow"] { | ||
return objectChildren.get(node)!.shallow | ||
const obj = getObjectChildrenObject(node) | ||
obj.shallowAtom.reportObserved() | ||
return obj.shallow | ||
} | ||
@@ -53,3 +55,3 @@ | ||
export function getDeepObjectChildren(node: object): DeepObjectChildren { | ||
const obj = objectChildren.get(node)! | ||
const obj = getObjectChildrenObject(node) | ||
@@ -73,3 +75,3 @@ if (obj.deepDirty) { | ||
const updateDeepObjectChildren = action((node: object): DeepObjectChildren => { | ||
const obj = objectChildren.get(node)! | ||
const obj = getObjectChildrenObject(node)! | ||
if (!obj.deepDirty) { | ||
@@ -84,3 +86,3 @@ return obj | ||
const childrenIter = getObjectChildren(node)!.values() | ||
const childrenIter = obj.shallow.values() | ||
let ch = childrenIter.next() | ||
@@ -113,4 +115,5 @@ while (!ch.done) { | ||
export const addObjectChild = action((node: object, child: object) => { | ||
const obj = objectChildren.get(node)! | ||
const obj = getObjectChildrenObject(node) | ||
obj.shallow.add(child) | ||
obj.shallowAtom.reportChanged() | ||
@@ -124,4 +127,5 @@ invalidateDeepChildren(node) | ||
export const removeObjectChild = action((node: object, child: object) => { | ||
const obj = objectChildren.get(node)! | ||
const obj = getObjectChildrenObject(node) | ||
obj.shallow.delete(child) | ||
obj.shallowAtom.reportChanged() | ||
@@ -132,3 +136,3 @@ invalidateDeepChildren(node) | ||
function invalidateDeepChildren(node: object) { | ||
const obj = objectChildren.get(node)! | ||
const obj = getObjectChildrenObject(node) | ||
@@ -146,3 +150,3 @@ if (!obj.deepDirty) { | ||
const extensions = new Map<Symbol, DeepObjectChildrenExtension<any>>() | ||
const extensions = new Map<object, DeepObjectChildrenExtension<any>>() | ||
@@ -158,3 +162,3 @@ interface DeepObjectChildrenExtension<D> { | ||
export function registerDeepObjectChildrenExtension<D>(extension: DeepObjectChildrenExtension<D>) { | ||
const dataSymbol = Symbol("deepObjectChildrenExtension") | ||
const dataSymbol = {} | ||
extensions.set(dataSymbol, extension) | ||
@@ -168,3 +172,3 @@ | ||
function initExtensionsData() { | ||
const extensionsData = new Map<Symbol, any>() | ||
const extensionsData = new WeakMap<object, any>() | ||
@@ -171,0 +175,0 @@ extensions.forEach((extension, dataSymbol) => { |
@@ -0,0 +0,0 @@ import { assertTweakedObject } from "../tweaker/core" |
@@ -0,0 +0,0 @@ import { assertTweakedObject } from "../tweaker/core" |
@@ -18,3 +18,3 @@ import { action } from "mobx" | ||
} from "./core" | ||
import { addObjectChild, initializeObjectChildren, removeObjectChild } from "./coreObjectChildren" | ||
import { addObjectChild, removeObjectChild } from "./coreObjectChildren" | ||
import { fastGetParentPath, fastGetRoot, ParentPath } from "./path" | ||
@@ -67,4 +67,2 @@ | ||
initializeObjectChildren(value) | ||
// make sure the new parent actually points to models when we give model data objs | ||
@@ -71,0 +69,0 @@ if (parentPath) { |
@@ -0,0 +0,0 @@ import { computed, IComputedValue } from "mobx" |
@@ -0,0 +0,0 @@ import { assertTweakedObject } from "../tweaker/core" |
@@ -88,3 +88,5 @@ import { action, createAtom, IAtom } from "mobx" | ||
frozenState.set(sn.untransformed, markAsFrozen) | ||
frozenState.set(sn.transformed, markAsFrozen) | ||
if (sn.transformed !== undefined) { | ||
frozenState.set(sn.transformed, markAsFrozen) | ||
} | ||
@@ -127,3 +129,5 @@ snapshots.set(value, sn) | ||
frozenState.set(sn.untransformed, false) | ||
frozenState.set(sn.transformed, false) | ||
if (sn.transformed !== undefined) { | ||
frozenState.set(sn.transformed, false) | ||
} | ||
@@ -130,0 +134,0 @@ sn.atom.reportChanged() |
@@ -0,0 +0,0 @@ import { runInAction } from "mobx" |
@@ -0,0 +0,0 @@ import { action } from "mobx" |
@@ -41,3 +41,3 @@ import { | ||
? originalArr | ||
: observable.array([], observableOptions) | ||
: observable.array(undefined, observableOptions) | ||
if (tweakedArr !== originalArr) { | ||
@@ -110,5 +110,13 @@ tweakedArr.length = originalArr.length | ||
function mutateSet(k: number, v: unknown, sn: unknown[]) { | ||
sn[k] = v | ||
} | ||
function mutateSplice(index: number, removedCount: number, addedItems: any[], sn: any[]) { | ||
sn.splice(index, removedCount, ...addedItems) | ||
} | ||
function arrayDidChange(change: any /*IArrayDidChange*/) { | ||
const arr = change.object | ||
let { untransformed: oldSnapshot } = getInternalSnapshot(arr as Array<any>)! | ||
let oldSnapshot = getInternalSnapshot(arr as Array<any>)!.untransformed | ||
@@ -121,208 +129,212 @@ const patchRecorder = new InternalPatchRecorder() | ||
case "splice": | ||
{ | ||
const index = change.index | ||
const addedCount = change.addedCount | ||
const removedCount = change.removedCount | ||
mutate = arrayDidChangeSplice(change, oldSnapshot, patchRecorder) | ||
break | ||
let addedItems: any[] = [] | ||
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)!.transformed | ||
} | ||
} | ||
case "update": | ||
mutate = arrayDidChangeUpdate(change, oldSnapshot, patchRecorder) | ||
break | ||
} | ||
const oldLen = oldSnapshot.length | ||
mutate = (newSnapshot) => { | ||
newSnapshot.splice(index, removedCount, ...addedItems) | ||
} | ||
runTypeCheckingAfterChange(arr, patchRecorder) | ||
const patches: Patch[] = [] | ||
const invPatches: Patch[] = [] | ||
if (!runningWithoutSnapshotOrPatches && mutate) { | ||
updateInternalSnapshot(arr, mutate) | ||
patchRecorder.emit(arr) | ||
} | ||
} | ||
// optimization: if we add as many as we remove then remove/readd instead | ||
const undefinedInsideArrayErrorMsg = | ||
"undefined is not supported inside arrays since it is not serializable in JSON, consider using null instead" | ||
// we cannot replace since we might end up in a situation where the same node | ||
// might attempt to be temporarily twice inside the same tree (e.g. sorting) | ||
function arrayDidChangeUpdate( | ||
change: any /*IArrayDidChange*/, | ||
oldSnapshot: any, | ||
patchRecorder: InternalPatchRecorder | ||
) { | ||
const k = change.index | ||
const val = change.newValue | ||
const oldVal = oldSnapshot[k] | ||
let newVal: any | ||
if (isPrimitive(val)) { | ||
newVal = val | ||
} else { | ||
const valueSn = getInternalSnapshot(val)! | ||
newVal = valueSn.transformed | ||
} | ||
const mutate = mutateSet.bind(undefined, k, newVal) | ||
// it would be faster to keep holes rather than remove/readd, but if we do that then | ||
// validation might fail | ||
const path = [k] | ||
if (addedCount === removedCount) { | ||
const readdPatches: Patch[] = [] | ||
const readdInvPatches: Patch[] = [] | ||
let removed = 0 | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(newVal), | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(oldVal), | ||
}, | ||
] | ||
) | ||
return mutate | ||
} | ||
for (let i = 0; i < addedCount; i++) { | ||
const realIndex = index + i | ||
function arrayDidChangeSplice( | ||
change: any /*IArrayDidChange*/, | ||
oldSnapshot: any, | ||
patchRecorder: InternalPatchRecorder | ||
) { | ||
const index = change.index | ||
const addedCount = change.addedCount | ||
const removedCount = change.removedCount | ||
const newVal = getValueAfterSplice( | ||
oldSnapshot, | ||
realIndex, | ||
index, | ||
removedCount, | ||
addedItems | ||
) | ||
const oldVal = oldSnapshot[realIndex] | ||
let addedItems: any[] = [] | ||
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)!.transformed | ||
} | ||
} | ||
if (newVal !== oldVal) { | ||
const removePath = [realIndex - removed] | ||
patches.push({ | ||
op: "remove", | ||
path: removePath, | ||
}) | ||
invPatches.push({ | ||
op: "remove", | ||
path: removePath, | ||
}) | ||
const oldLen = oldSnapshot.length | ||
const mutate = mutateSplice.bind(undefined, index, removedCount, addedItems) | ||
removed++ | ||
const patches: Patch[] = [] | ||
const invPatches: Patch[] = [] | ||
const readdPath = [realIndex] | ||
readdPatches.push({ | ||
op: "add", | ||
path: readdPath, | ||
value: freezeInternalSnapshot(newVal), | ||
}) | ||
// optimization: if we add as many as we remove then remove/readd instead | ||
readdInvPatches.push({ | ||
op: "add", | ||
path: readdPath, | ||
value: freezeInternalSnapshot(oldVal), | ||
}) | ||
} | ||
} | ||
// we cannot replace since we might end up in a situation where the same node | ||
// might attempt to be temporarily twice inside the same tree (e.g. sorting) | ||
patches.push(...readdPatches) | ||
invPatches.push(...readdInvPatches) | ||
// we need to reverse since inverse patches are applied in reverse | ||
invPatches.reverse() | ||
} else { | ||
const interimLen = oldLen - removedCount | ||
// it would be faster to keep holes rather than remove/readd, but if we do that then | ||
// validation might fail | ||
// 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, | ||
}) | ||
} | ||
if (addedCount === removedCount) { | ||
const readdPatches: Patch[] = [] | ||
const readdInvPatches: Patch[] = [] | ||
let removed = 0 | ||
for (let i = removedCount - 1; i >= 0; i--) { | ||
const realIndex = index + i | ||
const path = [realIndex] | ||
for (let i = 0; i < addedCount; i++) { | ||
const realIndex = index + i | ||
if (!removeUsingSetLength) { | ||
// remove ...2, 1, 0 | ||
patches.push({ | ||
op: "remove", | ||
path, | ||
}) | ||
} | ||
const newVal = getValueAfterSplice(oldSnapshot, realIndex, index, removedCount, addedItems) | ||
const oldVal = oldSnapshot[realIndex] | ||
// add 0, 1, 2... since inverse patches are applied in reverse | ||
invPatches.push({ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot(oldSnapshot[realIndex]), | ||
}) | ||
} | ||
} | ||
if (newVal !== oldVal) { | ||
const removePath = [realIndex - removed] | ||
patches.push({ | ||
op: "remove", | ||
path: removePath, | ||
}) | ||
invPatches.push({ | ||
op: "remove", | ||
path: removePath, | ||
}) | ||
// 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, | ||
}) | ||
} | ||
removed++ | ||
for (let i = 0; i < addedCount; i++) { | ||
const realIndex = index + i | ||
const path = [realIndex] | ||
const readdPath = [realIndex] | ||
readdPatches.push({ | ||
op: "add", | ||
path: readdPath, | ||
value: freezeInternalSnapshot(newVal), | ||
}) | ||
// add 0, 1, 2... | ||
patches.push({ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot( | ||
getValueAfterSplice(oldSnapshot, realIndex, index, removedCount, addedItems) | ||
), | ||
}) | ||
readdInvPatches.push({ | ||
op: "add", | ||
path: readdPath, | ||
value: freezeInternalSnapshot(oldVal), | ||
}) | ||
} | ||
} | ||
// remove ...2, 1, 0 since inverse patches are applied in reverse | ||
if (!restoreUsingSetLength) { | ||
invPatches.push({ | ||
op: "remove", | ||
path, | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
patches.push(...readdPatches) | ||
invPatches.push(...readdInvPatches) | ||
// we need to reverse since inverse patches are applied in reverse | ||
invPatches.reverse() | ||
} else { | ||
const interimLen = oldLen - removedCount | ||
patchRecorder.record(patches, invPatches) | ||
// 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, | ||
}) | ||
} | ||
break | ||
case "update": | ||
{ | ||
const k = change.index | ||
const val = change.newValue | ||
const oldVal = oldSnapshot[k] | ||
let newVal: any | ||
if (isPrimitive(val)) { | ||
newVal = val | ||
} else { | ||
const valueSn = getInternalSnapshot(val)! | ||
newVal = valueSn.transformed | ||
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, | ||
}) | ||
} | ||
mutate = (newSnapshot) => { | ||
newSnapshot[k] = newVal | ||
} | ||
const path = [k] | ||
// add 0, 1, 2... since inverse patches are applied in reverse | ||
invPatches.push({ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot(oldSnapshot[realIndex]), | ||
}) | ||
} | ||
} | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(newVal), | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(oldVal), | ||
}, | ||
] | ||
) | ||
// 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, | ||
}) | ||
} | ||
break | ||
} | ||
runTypeCheckingAfterChange(arr, patchRecorder) | ||
for (let i = 0; i < addedCount; i++) { | ||
const realIndex = index + i | ||
const path = [realIndex] | ||
if (!runningWithoutSnapshotOrPatches && mutate) { | ||
updateInternalSnapshot(arr, mutate) | ||
patchRecorder.emit(arr) | ||
// add 0, 1, 2... | ||
patches.push({ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot( | ||
getValueAfterSplice(oldSnapshot, realIndex, index, removedCount, addedItems) | ||
), | ||
}) | ||
// remove ...2, 1, 0 since inverse patches are applied in reverse | ||
if (!restoreUsingSetLength) { | ||
invPatches.push({ | ||
op: "remove", | ||
path, | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
patchRecorder.record(patches, invPatches) | ||
return mutate | ||
} | ||
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 | ||
@@ -337,66 +349,72 @@ function interceptArrayMutation( | ||
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) | ||
} | ||
} | ||
} | ||
interceptArrayMutationSplice(change) | ||
break | ||
for (let i = 0; i < change.removedCount; i++) { | ||
const removedValue = change.object[change.index + i] | ||
tweak(removedValue, undefined) | ||
tryUntweak(removedValue) | ||
} | ||
case "update": | ||
interceptArrayMutationUpdate(change, array) | ||
break | ||
} | ||
return change | ||
} | ||
for (let i = 0; i < change.added.length; i++) { | ||
change.added[i] = tweak(change.added[i], { | ||
parent: change.object, | ||
path: change.index + i, | ||
}) | ||
} | ||
function interceptArrayMutationUpdate(change: IArrayWillChange, array: IObservableArray) { | ||
if ( | ||
inDevMode() && | ||
!getGlobalConfig().allowUndefinedArrayElements && | ||
change.newValue === undefined | ||
) { | ||
throw failure(undefinedInsideArrayErrorMsg) | ||
} | ||
// 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 | ||
// 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) | ||
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 | ||
change.newValue = tweak(change.newValue, { parent: array, path: change.index }) | ||
} | ||
case "update": | ||
if ( | ||
inDevMode() && | ||
!getGlobalConfig().allowUndefinedArrayElements && | ||
change.newValue === undefined | ||
) { | ||
function interceptArrayMutationSplice(change: IArrayWillSplice) { | ||
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) | ||
} | ||
} | ||
} | ||
// 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) | ||
for (let i = 0; i < change.removedCount; i++) { | ||
const removedValue = change.object[change.index + i] | ||
tweak(removedValue, undefined) | ||
tryUntweak(removedValue) | ||
} | ||
change.newValue = tweak(change.newValue, { parent: array, path: change.index }) | ||
break | ||
for (let i = 0; i < change.added.length; i++) { | ||
change.added[i] = tweak(change.added[i], { | ||
parent: change.object, | ||
path: change.index + i, | ||
}) | ||
} | ||
return change | ||
// 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, | ||
}) | ||
} | ||
} | ||
} | ||
@@ -403,0 +421,0 @@ |
@@ -134,6 +134,14 @@ import { | ||
function mutateSet(k: PropertyKey, v: unknown, sn: Record<PropertyKey, unknown>) { | ||
sn[k] = v | ||
} | ||
function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>) { | ||
delete sn[k] | ||
} | ||
function objectDidChange(change: IObjectDidChange): void { | ||
const obj = change.object | ||
const actualNode = dataToModelNode(obj) | ||
let { untransformed: oldUntransformedSn } = getInternalSnapshot(actualNode)! | ||
let oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed | ||
@@ -147,84 +155,7 @@ const patchRecorder = new InternalPatchRecorder() | ||
case "update": | ||
{ | ||
const k = change.name | ||
const val = change.newValue | ||
const oldVal = oldUntransformedSn[k] | ||
let newVal: any | ||
if (isPrimitive(val)) { | ||
newVal = val | ||
} else { | ||
const valueSn = getInternalSnapshot(val)! | ||
newVal = valueSn.transformed | ||
} | ||
mutate = (sn) => { | ||
sn[k] = newVal | ||
} | ||
const path = [k as string] | ||
if (change.type === "add") { | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot(newVal), | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "remove", | ||
path, | ||
}, | ||
] | ||
) | ||
} else { | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(newVal), | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(oldVal), | ||
}, | ||
] | ||
) | ||
} | ||
} | ||
mutate = objectDidChangeAddOrUpdate(change, oldUntransformedSn, patchRecorder) | ||
break | ||
case "remove": | ||
{ | ||
const k = change.name | ||
const oldVal = oldUntransformedSn[k] | ||
mutate = (sn) => { | ||
delete sn[k] | ||
} | ||
const path = [k as string] | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "remove", | ||
path, | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot(oldVal), | ||
}, | ||
] | ||
) | ||
} | ||
mutate = objectDidChangeRemove(change, oldUntransformedSn, patchRecorder) | ||
break | ||
@@ -241,2 +172,90 @@ } | ||
function objectDidChangeRemove( | ||
change: IObjectDidChange & { type: "remove" }, | ||
oldUntransformedSn: any, | ||
patchRecorder: InternalPatchRecorder | ||
) { | ||
const k = change.name | ||
const oldVal = oldUntransformedSn[k] | ||
const mutate = mutateDelete.bind(undefined, k) | ||
const path = [k as string] | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "remove", | ||
path, | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot(oldVal), | ||
}, | ||
] | ||
) | ||
return mutate | ||
} | ||
function objectDidChangeAddOrUpdate( | ||
change: IObjectWillChange & { type: "add" | "update" }, | ||
oldUntransformedSn: any, | ||
patchRecorder: InternalPatchRecorder | ||
) { | ||
const k = change.name | ||
const val = change.newValue | ||
const oldVal = oldUntransformedSn[k] | ||
let newVal: any | ||
if (isPrimitive(val)) { | ||
newVal = val | ||
} else { | ||
const valueSn = getInternalSnapshot(val)! | ||
newVal = valueSn.transformed | ||
} | ||
const mutate = mutateSet.bind(undefined, k, newVal) | ||
const path = [k as string] | ||
if (change.type === "add") { | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "add", | ||
path, | ||
value: freezeInternalSnapshot(newVal), | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "remove", | ||
path, | ||
}, | ||
] | ||
) | ||
} else { | ||
patchRecorder.record( | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(newVal), | ||
}, | ||
], | ||
[ | ||
{ | ||
op: "replace", | ||
path, | ||
value: freezeInternalSnapshot(oldVal), | ||
}, | ||
] | ||
) | ||
} | ||
return mutate | ||
} | ||
function interceptObjectMutation(change: IObjectWillChange) { | ||
@@ -243,0 +262,0 @@ assertCanWrite() |
@@ -14,2 +14,26 @@ import { AnyModelProp, MaybeOptionalModelProp, OptionalModelProp, prop } from "../modelShared/prop" | ||
const noDefaultValueSymbol = Symbol("noDefaultValue") | ||
const tPropCache = new WeakMap<TypeChecker | LateTypeChecker, Map<unknown, AnyModelProp>>() | ||
function getOrCreateTProp( | ||
type: TypeChecker | LateTypeChecker, | ||
defKey: unknown, | ||
createTProp: () => AnyModelProp | ||
): AnyModelProp { | ||
let defValueCache = tPropCache.get(type) | ||
if (!defValueCache) { | ||
defValueCache = new Map() | ||
tPropCache.set(type, defValueCache) | ||
} | ||
let prop = defValueCache.get(defKey) | ||
if (!prop) { | ||
prop = createTProp() | ||
defValueCache.set(defKey, prop) | ||
} | ||
return prop | ||
} | ||
/** | ||
@@ -129,4 +153,2 @@ * Defines a string model property with a default value. | ||
const newProp = hasDefaultValue ? prop(def) : prop() | ||
const typeChecker = resolveStandardType(typeOrDefaultValue) as unknown as | ||
@@ -136,21 +158,36 @@ | TypeChecker | ||
const fromSnapshotTypeChecker = hasDefaultValue | ||
? typesOr(typeChecker as unknown as AnyType, typesUndefined, typesNull) | ||
: typeChecker | ||
return getOrCreateTProp(typeChecker, hasDefaultValue ? def : noDefaultValueSymbol, () => { | ||
const fromSnapshotTypeChecker = hasDefaultValue | ||
? typesOr(typeChecker as unknown as AnyType, typesUndefined, typesNull) | ||
: typeChecker | ||
Object.assign(newProp, { | ||
_typeChecker: typeChecker, | ||
// we use Object.create to avoid messing up with the prop cache | ||
const newProp = Object.create(hasDefaultValue ? prop(def) : prop()) | ||
_fromSnapshotProcessor: (sn) => { | ||
const fsnp = resolveTypeChecker(fromSnapshotTypeChecker).fromSnapshotProcessor | ||
return fsnp ? fsnp(sn) : sn | ||
}, | ||
Object.assign(newProp, { | ||
_typeChecker: typeChecker, | ||
_toSnapshotProcessor: (sn) => { | ||
const tsnp = resolveTypeChecker(typeChecker).toSnapshotProcessor | ||
return tsnp ? tsnp(sn) : sn | ||
}, | ||
} satisfies Partial<AnyModelProp>) | ||
_fromSnapshotProcessor: tPropFromSnapshotProcessor.bind(undefined, fromSnapshotTypeChecker), | ||
return newProp | ||
_toSnapshotProcessor: tPropToSnapshotProcessor.bind(undefined, typeChecker), | ||
} satisfies Partial<AnyModelProp>) | ||
return newProp | ||
}) | ||
} | ||
function tPropFromSnapshotProcessor( | ||
fromSnapshotTypeChecker: AnyType | TypeChecker | LateTypeChecker, | ||
sn: unknown | ||
): unknown { | ||
const fsnp = resolveTypeChecker(fromSnapshotTypeChecker).fromSnapshotProcessor | ||
return fsnp ? fsnp(sn) : sn | ||
} | ||
function tPropToSnapshotProcessor( | ||
typeChecker: AnyType | TypeChecker | LateTypeChecker, | ||
sn: unknown | ||
): unknown { | ||
const tsnp = resolveTypeChecker(typeChecker).toSnapshotProcessor | ||
return tsnp ? tsnp(sn) : sn | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4189730
55896