Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mobx-keystone

Package Overview
Dependencies
Maintainers
1
Versions
195
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mobx-keystone - npm Package Compare versions

Comparing version 1.3.0 to 1.4.0

6

CHANGELOG.md
# 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 @@

22

package.json
{
"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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc