mobx-keystone 1.25.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/dist/mobx-keystone.esm.js +3627 -1540
- package/dist/mobx-keystone.esm.mjs +3627 -1540
- package/dist/mobx-keystone.umd.js +3638 -1543
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +3 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
- package/dist/types/ref/nodesById.d.ts +1 -0
- package/dist/types/ref/refIdIndex.d.ts +1 -0
- package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
- package/dist/types/snapshot/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
- package/dist/types/utils/index.d.ts +13 -0
- package/dist/types/utils/mapUtils.d.ts +2 -2
- package/dist/types/utils/setLike.d.ts +1 -0
- package/package.json +2 -2
- package/src/action/applyAction.ts +1 -1
- package/src/action/middleware.ts +17 -36
- package/src/action/modelFlow.ts +4 -5
- package/src/action/mutationBatch.ts +10 -10
- package/src/action/pendingActions.ts +3 -4
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
- package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
- package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
- package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
- package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
- package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
- package/src/actionMiddlewares/undoMiddleware.ts +112 -59
- package/src/actionMiddlewares/utils.ts +2 -1
- package/src/computedTree/computedTree.ts +14 -3
- package/src/context/context.ts +17 -45
- package/src/dataModel/BaseDataModel.ts +20 -16
- package/src/dataModel/dataModelInstanceCache.ts +8 -0
- package/src/deepChange/onDeepChange.ts +179 -24
- package/src/frozen/Frozen.ts +50 -33
- package/src/model/BaseModel.ts +10 -13
- package/src/model/newModel.ts +21 -24
- package/src/model/utils.ts +2 -2
- package/src/modelShared/modelClassInitializer.ts +3 -2
- package/src/modelShared/modelDecorator.ts +39 -39
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +157 -55
- package/src/parent/path.ts +15 -5
- package/src/parent/setParent.ts +34 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +12 -101
- package/src/patch/applyPatches.ts +74 -34
- package/src/patch/emitPatch.ts +142 -26
- package/src/patch/jsonPatch.ts +5 -0
- package/src/patch/patchRecorder.ts +18 -14
- package/src/redux/connectReduxDevTools.ts +24 -18
- package/src/redux/redux.ts +5 -21
- package/src/ref/core.ts +81 -29
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/attachDetach.ts +27 -16
- package/src/rootStore/rootStore.ts +10 -6
- package/src/snapshot/SnapshotOf.ts +2 -2
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +89 -19
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +6 -6
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/standardActions/arrayActions.ts +12 -2
- package/src/standardActions/objectActions.ts +3 -2
- package/src/transforms/ImmutableDate.ts +4 -0
- package/src/transforms/asMap.ts +3 -1
- package/src/treeUtils/deepEquals.ts +15 -13
- package/src/treeUtils/draft.ts +8 -0
- package/src/treeUtils/sandbox.ts +31 -27
- package/src/tweaker/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -22
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +183 -19
- package/src/tweaker/tweakArray.ts +176 -33
- package/src/tweaker/tweakPlainObject.ts +117 -39
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +25 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +541 -88
- package/src/types/objectBased/typesDataModelData.ts +4 -2
- package/src/types/objectBased/typesModel.ts +4 -2
- package/src/types/objectBased/typesObject.ts +20 -16
- package/src/types/objectBased/typesRecord.ts +8 -18
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesEnum.ts +4 -2
- package/src/types/primitiveBased/typesPrimitive.ts +9 -4
- package/src/types/typeCheckErrorUtils.ts +1 -0
- package/src/types/utility/typesCodecSupport.ts +97 -56
- package/src/types/utility/typesMap.ts +24 -41
- package/src/types/utility/typesRefinement.ts +3 -2
- package/src/types/utility/typesSet.ts +24 -0
- package/src/utils/ModelPool.ts +78 -22
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +1 -1
- package/src/utils/errorDiagnostics.ts +6 -2
- package/src/utils/forEachWithDelayedThrow.ts +83 -0
- package/src/utils/index.ts +71 -2
- package/src/utils/mapUtils.ts +3 -7
- package/src/utils/setIfDifferent.ts +3 -2
- package/src/utils/setLike.ts +134 -0
- package/src/wrappers/ArraySet.ts +5 -9
- package/src/wrappers/ObjectMap.ts +15 -11
- package/src/wrappers/asMap.ts +30 -18
- package/src/wrappers/asSet.ts +36 -12
- package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
- package/src/action/modelFlowPromiseGenerator.ts +0 -127
|
@@ -69,13 +69,13 @@ export type SnapshotInOfModel<M extends AnyModel> = ModelFromSnapshot<M>
|
|
|
69
69
|
export interface SnapshotInOfFrozen<F extends Frozen<any>> extends FrozenData<F["data"]> {}
|
|
70
70
|
|
|
71
71
|
export interface SnapshotInOfObjectMap<V> {
|
|
72
|
-
items?: { [k: string]:
|
|
72
|
+
items?: { [k: string]: SnapshotInOf<V> }
|
|
73
73
|
[modelTypeKey]?: string
|
|
74
74
|
[modelIdKey]: string
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface SnapshotInOfArraySet<V> {
|
|
78
|
-
items?:
|
|
78
|
+
items?: SnapshotInOf<V>[]
|
|
79
79
|
[modelTypeKey]?: string
|
|
80
80
|
[modelIdKey]: string
|
|
81
81
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getModelNotRegisteredErrorMessage,
|
|
14
14
|
modelInfoByClass,
|
|
15
15
|
} from "../modelShared/modelInfo"
|
|
16
|
+
import { withChangeBatch } from "../tweaker/changeRollback"
|
|
16
17
|
import { assertTweakedObject } from "../tweaker/core"
|
|
17
18
|
import { assertIsObject, inDevMode, isArray, isMap, isPlainObject, isSet, lazy } from "../utils"
|
|
18
19
|
import { runWithErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
@@ -24,6 +25,7 @@ import { SnapshotProcessingError } from "./SnapshotProcessingError"
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Applies a full snapshot over an object, reconciling it with the current contents of the object.
|
|
28
|
+
* If it throws, the changes it made are rolled back (errors thrown by listeners do not roll them back).
|
|
27
29
|
*
|
|
28
30
|
* @template T Object type.
|
|
29
31
|
* @param node Target object (model object, object or array).
|
|
@@ -52,27 +54,33 @@ export function internalApplySnapshot<T extends object>(
|
|
|
52
54
|
): void {
|
|
53
55
|
const obj = this
|
|
54
56
|
|
|
55
|
-
const reconcile = () =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
const reconcile = () =>
|
|
58
|
+
withChangeBatch(() => {
|
|
59
|
+
// Avoid building a model pool (and its deep children traversal) when the
|
|
60
|
+
// node already holds this exact snapshot. Child updates may still await
|
|
61
|
+
// propagation within an action, so flush before trusting the reference.
|
|
62
|
+
flushInternalSnapshot(obj, false)
|
|
63
|
+
if (getInternalSnapshot(obj)?.transformed === sn) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const modelPool = new ModelPool(obj)
|
|
68
|
+
let ret: unknown
|
|
69
|
+
try {
|
|
70
|
+
ret = reconcileSnapshot(obj, sn, modelPool, undefined)
|
|
71
|
+
} finally {
|
|
72
|
+
modelPool.release()
|
|
73
|
+
}
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
if (inDevMode) {
|
|
76
|
+
if (ret !== obj) {
|
|
77
|
+
throw new SnapshotProcessingError({
|
|
78
|
+
message: "assertion failed: reconciled object has to be the same",
|
|
79
|
+
actualSnapshot: sn,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
83
|
+
})
|
|
76
84
|
|
|
77
85
|
if (isArray(sn)) {
|
|
78
86
|
if (!isArray(obj)) {
|
|
@@ -129,7 +137,7 @@ export function internalApplySnapshot<T extends object>(
|
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
132
|
-
if (modelIdPropertyName) {
|
|
140
|
+
if (modelIdPropertyName !== undefined) {
|
|
133
141
|
const id = (sn as any)[modelIdPropertyName]
|
|
134
142
|
if (obj[modelIdKey] !== id) {
|
|
135
143
|
// different id, no reconciliation possible
|
package/src/snapshot/clone.ts
CHANGED
|
@@ -24,8 +24,7 @@ export function clone<T extends object>(node: T, options?: Partial<CloneOptions>
|
|
|
24
24
|
assertTweakedObject(node, "node")
|
|
25
25
|
|
|
26
26
|
const opts = {
|
|
27
|
-
generateNewIds: true,
|
|
28
|
-
...options,
|
|
27
|
+
generateNewIds: options?.generateNewIds ?? true,
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
const sn = getSnapshot(node)
|
|
@@ -10,8 +10,7 @@ import { fromSnapshot } from "./fromSnapshot"
|
|
|
10
10
|
*/
|
|
11
11
|
export function cloneTreeValue<T>(value: T, options?: Partial<CloneOptions>): T {
|
|
12
12
|
const cloneOptions: CloneOptions = {
|
|
13
|
-
generateNewIds: true,
|
|
14
|
-
...options,
|
|
13
|
+
generateNewIds: options?.generateNewIds ?? true,
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
if (isPrimitive(value)) {
|
|
@@ -5,6 +5,7 @@ import { modelTypeKey } from "../model/metadata"
|
|
|
5
5
|
import { getModelOrSnapshotTypeAndId, getSnapshotModelType } from "../model/utils"
|
|
6
6
|
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
7
7
|
import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
|
|
8
|
+
import { hasOwnProp } from "../utils"
|
|
8
9
|
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
9
10
|
import { type FromSnapshotContext, registerSnapshotter } from "./fromSnapshot"
|
|
10
11
|
import type { SnapshotInOfModel } from "./SnapshotOf"
|
|
@@ -14,7 +15,7 @@ import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPrio
|
|
|
14
15
|
function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotContext): AnyModel {
|
|
15
16
|
const type = sn[modelTypeKey]
|
|
16
17
|
|
|
17
|
-
if (
|
|
18
|
+
if (type === undefined) {
|
|
18
19
|
throw new SnapshotProcessingError({
|
|
19
20
|
message: `a model snapshot must contain a type key (${modelTypeKey}), but none was found`,
|
|
20
21
|
actualSnapshot: sn,
|
|
@@ -30,7 +31,10 @@ function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotCon
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
33
|
-
if (
|
|
34
|
+
if (
|
|
35
|
+
modelIdPropertyName !== undefined &&
|
|
36
|
+
(!hasOwnProp(sn, modelIdPropertyName) || sn[modelIdPropertyName] === undefined)
|
|
37
|
+
) {
|
|
34
38
|
throw new SnapshotProcessingError({
|
|
35
39
|
message: `a model snapshot of type '${type}' must contain an id key (${modelIdPropertyName}), but none was found`,
|
|
36
40
|
actualSnapshot: sn,
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { observable } from "mobx"
|
|
2
2
|
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
|
|
3
|
-
import { isPlainObject, setProtoProp } from "../utils"
|
|
3
|
+
import { isPlainObject, nodeObservableOptions, setProtoProp } from "../utils"
|
|
4
4
|
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
5
|
-
import {
|
|
6
|
-
type FromSnapshotContext,
|
|
7
|
-
internalFromSnapshot,
|
|
8
|
-
observableOptions,
|
|
9
|
-
registerSnapshotter,
|
|
10
|
-
} from "./fromSnapshot"
|
|
5
|
+
import { type FromSnapshotContext, internalFromSnapshot, registerSnapshotter } from "./fromSnapshot"
|
|
11
6
|
import type { SnapshotInOfObject } from "./SnapshotOf"
|
|
12
7
|
import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
|
|
13
8
|
|
|
@@ -37,7 +32,7 @@ function fromPlainObjectSnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotC
|
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
return tweakPlainObject(
|
|
40
|
-
observable.object(plainObj, undefined,
|
|
35
|
+
observable.object(plainObj, undefined, nodeObservableOptions),
|
|
41
36
|
undefined,
|
|
42
37
|
undefined,
|
|
43
38
|
true,
|
|
@@ -5,7 +5,7 @@ import { resolveStandardTypeNoThrow, resolveTypeChecker } from "../types/resolve
|
|
|
5
5
|
import type { AnyType, TypeToData, TypeToSnapshotIn } from "../types/schemas"
|
|
6
6
|
import { isLateTypeChecker, TypeChecker } from "../types/TypeChecker"
|
|
7
7
|
import { resolveCodecSupport } from "../types/utility/typesCodec"
|
|
8
|
-
import { isMap, isPrimitive, isSet, setProtoProp } from "../utils"
|
|
8
|
+
import { isMap, isPrimitive, isSet, nodeObservableOptions, setProtoProp } from "../utils"
|
|
9
9
|
import {
|
|
10
10
|
getCurrentErrorDiagnosticsContext,
|
|
11
11
|
runWithErrorDiagnosticsContext,
|
|
@@ -210,13 +210,9 @@ function snapshotToInitialData(
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
-
const observableInitialData = observable.object(initialData, undefined,
|
|
213
|
+
const observableInitialData = observable.object(initialData, undefined, nodeObservableOptions)
|
|
214
214
|
if (allValuesPrimitive) {
|
|
215
215
|
setModelInitialDataSnapshot(observableInitialData, initialData)
|
|
216
216
|
}
|
|
217
217
|
return observableInitialData
|
|
218
218
|
}
|
|
219
|
-
|
|
220
|
-
export const observableOptions = {
|
|
221
|
-
deep: false,
|
|
222
|
-
}
|
package/src/snapshot/internal.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface SnapshotData {
|
|
|
19
19
|
atom: IAtom | undefined
|
|
20
20
|
// Source of truth for copy-on-write of this snapshot's untransformed data.
|
|
21
21
|
untransformedFrozen: boolean
|
|
22
|
+
// When defined while `untransformed` is not frozen, every value in it except
|
|
23
|
+
// (possibly) these is already frozen, so freezing it only needs to visit them.
|
|
24
|
+
// Values listed here may no longer be in `untransformed`.
|
|
25
|
+
unfrozenValues: object[] | undefined
|
|
22
26
|
dirtyChild: object | undefined
|
|
23
27
|
dirtyChildSnapshot: SnapshotData | undefined
|
|
24
28
|
dirtyChildPath: PathElement | undefined
|
|
@@ -31,10 +35,13 @@ interface DirtyChild {
|
|
|
31
35
|
readonly path: PathElement
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
// `false` means the generic freezer must traverse this raw value; `true` means
|
|
35
|
-
//
|
|
36
|
-
// must be reflected back
|
|
37
|
-
//
|
|
38
|
+
// `false` means the generic freezer must traverse this raw value; `true` means it
|
|
39
|
+
// is frozen; a SnapshotData value means it is mutable and owned by that snapshot,
|
|
40
|
+
// which is where its frozen state must be reflected back. Both the untransformed
|
|
41
|
+
// and the transformed value of a snapshot point to their owner, so freezing can
|
|
42
|
+
// reach descendants that are only shared through processor wrappers. `undefined`
|
|
43
|
+
// is either external / untracked data or a fused frozen clone, whose owner tracks
|
|
44
|
+
// its state directly.
|
|
38
45
|
const snapshotStates = new WeakMap<object, boolean | SnapshotData>()
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -91,6 +98,7 @@ export const setNewInternalSnapshot = action(
|
|
|
91
98
|
transformed,
|
|
92
99
|
atom: undefined, // will be created when first observed
|
|
93
100
|
untransformedFrozen: markAsFrozen,
|
|
101
|
+
unfrozenValues: undefined,
|
|
94
102
|
dirtyChild: undefined,
|
|
95
103
|
dirtyChildSnapshot: undefined,
|
|
96
104
|
dirtyChildPath: undefined,
|
|
@@ -101,7 +109,7 @@ export const setNewInternalSnapshot = action(
|
|
|
101
109
|
snapshotStates.set(untransformed, markAsFrozen ? true : sn)
|
|
102
110
|
|
|
103
111
|
if (transformed !== undefined && transformed !== untransformed) {
|
|
104
|
-
snapshotStates.set(transformed, markAsFrozen)
|
|
112
|
+
snapshotStates.set(transformed, markAsFrozen ? true : sn)
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
getOrCreateTreeNodeMetadata(value).snapshot = sn
|
|
@@ -110,7 +118,10 @@ export const setNewInternalSnapshot = action(
|
|
|
110
118
|
}
|
|
111
119
|
)
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Mutates a snapshot container in place and returns the values it wrote.
|
|
123
|
+
*/
|
|
124
|
+
type MutateInternalSnapshotFn<T> = (prevSn: T) => readonly unknown[]
|
|
114
125
|
|
|
115
126
|
function makeSnapshotMutable(sn: SnapshotData): any {
|
|
116
127
|
let untransformed = sn.untransformed
|
|
@@ -124,7 +135,39 @@ function makeSnapshotMutable(sn: SnapshotData): any {
|
|
|
124
135
|
return untransformed
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
function
|
|
138
|
+
function trackUnfrozenValues(
|
|
139
|
+
sn: SnapshotData,
|
|
140
|
+
untransformed: any,
|
|
141
|
+
wasFrozen: boolean,
|
|
142
|
+
writtenValues: readonly unknown[]
|
|
143
|
+
): void {
|
|
144
|
+
// a cloned frozen container starts with only frozen values
|
|
145
|
+
let unfrozenValues = wasFrozen ? [] : sn.unfrozenValues
|
|
146
|
+
if (unfrozenValues === undefined) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < writtenValues.length; i++) {
|
|
150
|
+
const value = writtenValues[i]
|
|
151
|
+
if (!isPrimitive(value)) {
|
|
152
|
+
unfrozenValues.push(value)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const size = Array.isArray(untransformed)
|
|
156
|
+
? untransformed.length
|
|
157
|
+
: Object.keys(untransformed).length
|
|
158
|
+
if (unfrozenValues.length > size) {
|
|
159
|
+
// cheaper to visit the whole container again
|
|
160
|
+
unfrozenValues = undefined
|
|
161
|
+
}
|
|
162
|
+
sn.unfrozenValues = unfrozenValues
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setSnapshotData(
|
|
166
|
+
sn: SnapshotData,
|
|
167
|
+
untransformed: any,
|
|
168
|
+
writtenValues: readonly unknown[],
|
|
169
|
+
freezeIfCloned = false
|
|
170
|
+
): void {
|
|
128
171
|
// `makeSnapshotMutable` returns a new container exactly when this snapshot
|
|
129
172
|
// was frozen. All callers obtain `untransformed` through that helper.
|
|
130
173
|
const wasFrozen = untransformed !== sn.untransformed
|
|
@@ -135,6 +178,11 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
135
178
|
|
|
136
179
|
const keepFrozen = freezeIfCloned && wasFrozen && !sn.transformFn
|
|
137
180
|
sn.untransformedFrozen = keepFrozen
|
|
181
|
+
if (keepFrozen) {
|
|
182
|
+
sn.unfrozenValues = undefined
|
|
183
|
+
} else {
|
|
184
|
+
trackUnfrozenValues(sn, untransformed, wasFrozen, writtenValues)
|
|
185
|
+
}
|
|
138
186
|
if (wasFrozen && !keepFrozen) {
|
|
139
187
|
snapshotStates.set(untransformed, sn)
|
|
140
188
|
}
|
|
@@ -143,14 +191,14 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
143
191
|
transformed !== undefined &&
|
|
144
192
|
(!wasFrozen || transformed !== untransformed)
|
|
145
193
|
) {
|
|
146
|
-
snapshotStates.set(transformed,
|
|
194
|
+
snapshotStates.set(transformed, sn)
|
|
147
195
|
}
|
|
148
196
|
}
|
|
149
197
|
|
|
150
198
|
function updateOwnSnapshot<T>(sn: SnapshotData, mutate: MutateInternalSnapshotFn<T>): void {
|
|
151
199
|
const untransformed = makeSnapshotMutable(sn)
|
|
152
|
-
mutate(untransformed)
|
|
153
|
-
setSnapshotData(sn, untransformed)
|
|
200
|
+
const writtenValues = mutate(untransformed)
|
|
201
|
+
setSnapshotData(sn, untransformed, writtenValues)
|
|
154
202
|
sn.atom?.reportChanged()
|
|
155
203
|
}
|
|
156
204
|
|
|
@@ -161,7 +209,7 @@ function updateParentSlot(parentSn: SnapshotData, path: PathElement, value: any)
|
|
|
161
209
|
} else {
|
|
162
210
|
untransformed[path] = value
|
|
163
211
|
}
|
|
164
|
-
setSnapshotData(parentSn, untransformed)
|
|
212
|
+
setSnapshotData(parentSn, untransformed, [value])
|
|
165
213
|
parentSn.atom?.reportChanged()
|
|
166
214
|
}
|
|
167
215
|
|
|
@@ -172,7 +220,8 @@ function flushDirtyChild(
|
|
|
172
220
|
childSn: SnapshotData,
|
|
173
221
|
childPath: PathElement,
|
|
174
222
|
freezeAfterFlush: boolean,
|
|
175
|
-
freezeClonedContainer: boolean
|
|
223
|
+
freezeClonedContainer: boolean,
|
|
224
|
+
writtenValues: unknown[]
|
|
176
225
|
): void {
|
|
177
226
|
if (childSn.isUnset) {
|
|
178
227
|
return
|
|
@@ -197,6 +246,7 @@ function flushDirtyChild(
|
|
|
197
246
|
} else {
|
|
198
247
|
untransformed[childPath] = childSn.transformed
|
|
199
248
|
}
|
|
249
|
+
writtenValues.push(childSn.transformed)
|
|
200
250
|
}
|
|
201
251
|
|
|
202
252
|
function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: boolean): boolean {
|
|
@@ -215,6 +265,7 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
215
265
|
const untransformed = makeSnapshotMutable(sn)
|
|
216
266
|
const freezeClonedContainer =
|
|
217
267
|
freezeAfterFlush && untransformed !== sn.untransformed && !sn.transformFn
|
|
268
|
+
const writtenValues: unknown[] = []
|
|
218
269
|
|
|
219
270
|
flushDirtyChild(
|
|
220
271
|
node,
|
|
@@ -223,7 +274,8 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
223
274
|
dirtyChildSnapshot,
|
|
224
275
|
dirtyChildPath,
|
|
225
276
|
freezeAfterFlush,
|
|
226
|
-
freezeClonedContainer
|
|
277
|
+
freezeClonedContainer,
|
|
278
|
+
writtenValues
|
|
227
279
|
)
|
|
228
280
|
additionalDirtyChildren?.forEach((dirtyChild, child) => {
|
|
229
281
|
flushDirtyChild(
|
|
@@ -233,11 +285,12 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
233
285
|
dirtyChild.snapshot,
|
|
234
286
|
dirtyChild.path,
|
|
235
287
|
freezeAfterFlush,
|
|
236
|
-
freezeClonedContainer
|
|
288
|
+
freezeClonedContainer,
|
|
289
|
+
writtenValues
|
|
237
290
|
)
|
|
238
291
|
})
|
|
239
292
|
|
|
240
|
-
setSnapshotData(sn, untransformed, freezeClonedContainer)
|
|
293
|
+
setSnapshotData(sn, untransformed, writtenValues, freezeClonedContainer)
|
|
241
294
|
return freezeClonedContainer
|
|
242
295
|
}
|
|
243
296
|
|
|
@@ -335,7 +388,7 @@ export const refreshInternalSnapshot = action("refreshInternalSnapshot", (value:
|
|
|
335
388
|
// transformed snapshots created by internal transforms must be tracked as mutable
|
|
336
389
|
// until they are exposed through getSnapshot / freezeInternalSnapshot.
|
|
337
390
|
if (snapshotStates.get(newTransformed) === undefined) {
|
|
338
|
-
snapshotStates.set(newTransformed,
|
|
391
|
+
snapshotStates.set(newTransformed, sn)
|
|
339
392
|
}
|
|
340
393
|
|
|
341
394
|
sn.atom?.reportChanged()
|
|
@@ -370,7 +423,23 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
370
423
|
return data
|
|
371
424
|
}
|
|
372
425
|
|
|
373
|
-
|
|
426
|
+
const owner = state === false ? undefined : state
|
|
427
|
+
|
|
428
|
+
// Processors can create untracked wrappers around tracked descendants, and the
|
|
429
|
+
// freezer stops at those (their state is `undefined`). Freeze the source of the
|
|
430
|
+
// transformed value as well, so shared descendants that the transformed
|
|
431
|
+
// traversal cannot reach still switch to copy-on-write.
|
|
432
|
+
if (owner !== undefined && owner.untransformed !== data) {
|
|
433
|
+
freezeInternalSnapshot(owner.untransformed)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const unfrozenValues =
|
|
437
|
+
owner !== undefined && owner.untransformed === data ? owner.unfrozenValues : undefined
|
|
438
|
+
if (unfrozenValues !== undefined) {
|
|
439
|
+
for (let i = 0; i < unfrozenValues.length; i++) {
|
|
440
|
+
freezeInternalSnapshot(unfrozenValues[i])
|
|
441
|
+
}
|
|
442
|
+
} else if (Array.isArray(data)) {
|
|
374
443
|
for (let i = 0; i < data.length; i++) {
|
|
375
444
|
freezeInternalSnapshot(data[i])
|
|
376
445
|
}
|
|
@@ -383,8 +452,9 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
383
452
|
|
|
384
453
|
snapshotStates.set(data, true)
|
|
385
454
|
|
|
386
|
-
if (
|
|
387
|
-
|
|
455
|
+
if (owner !== undefined && owner.untransformed === data) {
|
|
456
|
+
owner.untransformedFrozen = true
|
|
457
|
+
owner.unfrozenValues = undefined
|
|
388
458
|
}
|
|
389
459
|
|
|
390
460
|
return data
|
|
@@ -31,9 +31,20 @@ export function onSnapshot<T extends object>(
|
|
|
31
31
|
assertTweakedObject(node, "node")
|
|
32
32
|
|
|
33
33
|
let currentSnapshot = getSnapshot(node)
|
|
34
|
+
let firstTrackingRun = true
|
|
34
35
|
|
|
35
36
|
return reaction(
|
|
36
|
-
() =>
|
|
37
|
+
() => {
|
|
38
|
+
const snapshot = getSnapshot(nodeFn())
|
|
39
|
+
// Registration inside an action defers the first tracking run until the
|
|
40
|
+
// action ends. Its snapshot, rather than the earlier registration state,
|
|
41
|
+
// is the baseline for the first notification.
|
|
42
|
+
if (firstTrackingRun) {
|
|
43
|
+
firstTrackingRun = false
|
|
44
|
+
currentSnapshot = snapshot
|
|
45
|
+
}
|
|
46
|
+
return snapshot
|
|
47
|
+
},
|
|
37
48
|
(newSnapshot) => {
|
|
38
49
|
const prevSn = currentSnapshot
|
|
39
50
|
currentSnapshot = newSnapshot
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runTypeCheckingAfterChange,
|
|
18
18
|
} from "../tweaker/typeChecking"
|
|
19
19
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
20
|
-
import { isArray, isPrimitive } from "../utils"
|
|
20
|
+
import { hasOwnProp, isArray, isPrimitive } from "../utils"
|
|
21
21
|
import { withErrorModelTrailEntry, withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
22
22
|
import type { ModelPool } from "../utils/ModelPool"
|
|
23
23
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
@@ -58,15 +58,15 @@ function reconcileModelSnapshot(
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const modelIdPropertyName = getModelIdPropertyName(modelClass)
|
|
61
|
-
const incomingModelId = modelIdPropertyName ? sn[modelIdPropertyName] : undefined
|
|
61
|
+
const incomingModelId = modelIdPropertyName !== undefined ? sn[modelIdPropertyName] : undefined
|
|
62
62
|
const positionalModelMatches =
|
|
63
63
|
isModel(value) &&
|
|
64
64
|
value[modelTypeKey] === type &&
|
|
65
|
-
(
|
|
65
|
+
(modelIdPropertyName === undefined || value[modelIdKey] === incomingModelId)
|
|
66
66
|
|
|
67
67
|
// The positional model is already the desired instance in the common
|
|
68
68
|
// stable-update case. Only moved identified models need a pool lookup.
|
|
69
|
-
if (!positionalModelMatches && modelIdPropertyName) {
|
|
69
|
+
if (!positionalModelMatches && modelIdPropertyName !== undefined) {
|
|
70
70
|
const modelInPool = modelPool.findModelByTypeAndId(type, incomingModelId)
|
|
71
71
|
if (modelInPool) {
|
|
72
72
|
value = modelInPool
|
|
@@ -92,7 +92,7 @@ function reconcileModelSnapshot(
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const modelProps = getInternalModelClassPropsInfo(modelClass)
|
|
95
|
-
if (modelIdPropertyName) {
|
|
95
|
+
if (modelIdPropertyName !== undefined) {
|
|
96
96
|
if (value[modelIdKey] !== incomingModelId) {
|
|
97
97
|
// different id, no reconciliation possible
|
|
98
98
|
return fromSnapshot<AnyModel>(sn)
|
|
@@ -134,7 +134,7 @@ function reconcileModelSnapshot(
|
|
|
134
134
|
const dataKeysLen = dataKeys.length
|
|
135
135
|
for (let i = 0; i < dataKeysLen; i++) {
|
|
136
136
|
const k = dataKeys[i]
|
|
137
|
-
if (!(k
|
|
137
|
+
if (!hasOwnProp(processedSn, k)) {
|
|
138
138
|
// use default value if applicable
|
|
139
139
|
const modelProp = modelProps[k] as AnyModelProp | undefined
|
|
140
140
|
const defaultValue = modelProp
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { isObservableObject, remove } from "mobx"
|
|
2
|
+
import { isModel } from "../model/utils"
|
|
2
3
|
import {
|
|
3
4
|
isTypeCheckingAfterChangeEnabled,
|
|
4
5
|
runTypeCheckingAfterChange,
|
|
5
6
|
} from "../tweaker/typeChecking"
|
|
6
7
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
7
|
-
import { isPlainObject } from "../utils"
|
|
8
|
+
import { hasOwnProp, isPlainObject } from "../utils"
|
|
8
9
|
import { withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
9
10
|
import type { ModelPool } from "../utils/ModelPool"
|
|
10
11
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
@@ -19,8 +20,8 @@ function reconcilePlainObjectSnapshot(
|
|
|
19
20
|
sn: SnapshotInOfObject<any>,
|
|
20
21
|
modelPool: ModelPool
|
|
21
22
|
): object {
|
|
22
|
-
// plain
|
|
23
|
-
if (!(isPlainObject(value) || isObservableObject(value))) {
|
|
23
|
+
// Observable models carry instance state and must be replaced, not edited as plain objects.
|
|
24
|
+
if (isModel(value) || !(isPlainObject(value) || isObservableObject(value))) {
|
|
24
25
|
// no reconciliation possible
|
|
25
26
|
return fromSnapshot(sn)
|
|
26
27
|
}
|
|
@@ -35,7 +36,7 @@ function reconcilePlainObjectSnapshot(
|
|
|
35
36
|
const plainObjKeysLen = plainObjKeys.length
|
|
36
37
|
for (let i = 0; i < plainObjKeysLen; i++) {
|
|
37
38
|
const k = plainObjKeys[i]
|
|
38
|
-
if (!(k
|
|
39
|
+
if (!hasOwnProp(sn, k)) {
|
|
39
40
|
remove(plainObj, k)
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -26,7 +26,7 @@ export const arrayActions = {
|
|
|
26
26
|
|
|
27
27
|
concat: standaloneAction(
|
|
28
28
|
`${namespace}::concat`,
|
|
29
|
-
<T>(array: T[], ...items: ConcatArray<T>[]): T[] => {
|
|
29
|
+
<T>(array: T[], ...items: (T | ConcatArray<T>)[]): T[] => {
|
|
30
30
|
return array.concat(...items)
|
|
31
31
|
}
|
|
32
32
|
),
|
|
@@ -89,9 +89,19 @@ export const arrayActions = {
|
|
|
89
89
|
swap: standaloneAction(
|
|
90
90
|
`${namespace}::swap`,
|
|
91
91
|
<T>(array: T[], index1: number, index2: number): boolean => {
|
|
92
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
!Number.isInteger(index1) ||
|
|
94
|
+
!Number.isInteger(index2) ||
|
|
95
|
+
index1 < 0 ||
|
|
96
|
+
index2 < 0 ||
|
|
97
|
+
index1 >= array.length ||
|
|
98
|
+
index2 >= array.length
|
|
99
|
+
) {
|
|
93
100
|
return false
|
|
94
101
|
}
|
|
102
|
+
if (index1 === index2) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
95
105
|
if (index2 < index1) {
|
|
96
106
|
;[index1, index2] = [index2, index1]
|
|
97
107
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isObservable, remove } from "mobx"
|
|
2
|
+
import { isModel } from "../model/utils"
|
|
2
3
|
import { toTreeNode } from "../tweaker/tweak"
|
|
3
4
|
import { assertIsObject, namespace as ns } from "../utils"
|
|
4
5
|
import type { AnyFunction } from "../utils/AnyFunction"
|
|
@@ -11,7 +12,7 @@ export const objectActions = {
|
|
|
11
12
|
set: standaloneAction(
|
|
12
13
|
`${namespace}::set`,
|
|
13
14
|
<T extends object, K extends keyof T>(target: T, key: K, value: T[K]): void => {
|
|
14
|
-
if (isObservable(target)) {
|
|
15
|
+
if (isObservable(target) && !isModel(target)) {
|
|
15
16
|
setIfDifferent(target, key, value)
|
|
16
17
|
} else {
|
|
17
18
|
target[key] = value
|
|
@@ -25,7 +26,7 @@ export const objectActions = {
|
|
|
25
26
|
assertIsObject(partialObject, "partialObject")
|
|
26
27
|
const keys = Object.keys(partialObject)
|
|
27
28
|
|
|
28
|
-
if (isObservable(target)) {
|
|
29
|
+
if (isObservable(target) && !isModel(target)) {
|
|
29
30
|
for (const key of keys) {
|
|
30
31
|
const newValue = (partialObject as any)[key]
|
|
31
32
|
setIfDifferent(target, key, newValue)
|
package/src/transforms/asMap.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelPropTransform } from "../modelShared/prop"
|
|
2
|
+
import { setProtoProp } from "../utils"
|
|
2
3
|
import { asMap } from "../wrappers/asMap"
|
|
3
4
|
|
|
4
5
|
const _objectToMapTransform: ModelPropTransform<Record<string, unknown>, Map<string, unknown>> = {
|
|
@@ -11,7 +12,8 @@ const _objectToMapTransform: ModelPropTransform<Record<string, unknown>, Map<str
|
|
|
11
12
|
|
|
12
13
|
const obj: Record<string, unknown> = {}
|
|
13
14
|
map.forEach((v, k) => {
|
|
14
|
-
|
|
15
|
+
if (k === "__proto__") setProtoProp(obj, v)
|
|
16
|
+
else obj[k] = v
|
|
15
17
|
})
|
|
16
18
|
|
|
17
19
|
return obj
|
|
@@ -29,21 +29,23 @@ export function deepEquals(a: any, b: any): boolean {
|
|
|
29
29
|
return true
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
a
|
|
37
|
-
} else if (isObservable(a)) {
|
|
38
|
-
a = toJS(a, toJSOptions)
|
|
39
|
-
}
|
|
40
|
-
if (isTreeNode(b)) {
|
|
41
|
-
b = getSnapshot(b)
|
|
42
|
-
} else if (isObservable(b)) {
|
|
43
|
-
b = toJS(b, toJSOptions)
|
|
32
|
+
const aIsObject = a !== null && typeof a === "object"
|
|
33
|
+
const bIsObject = b !== null && typeof b === "object"
|
|
34
|
+
if (!aIsObject && !bIsObject) {
|
|
35
|
+
// Strict equality already handled all equal primitives except NaN.
|
|
36
|
+
return Number.isNaN(a) && Number.isNaN(b)
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
return fastDeepEqual(a, b)
|
|
39
|
+
return fastDeepEqual(aIsObject ? comparisonValue(a) : a, bIsObject ? comparisonValue(b) : b)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function comparisonValue(value: object): unknown {
|
|
43
|
+
if (isTreeNode(value)) {
|
|
44
|
+
// Snapshots preserve structural sharing. Model $ objects are deliberately
|
|
45
|
+
// handled as observable objects rather than standalone tree nodes.
|
|
46
|
+
return getSnapshot(value)
|
|
47
|
+
}
|
|
48
|
+
return isObservable(value) ? toJS(value, toJSOptions) : value
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const toJSOptions =
|
package/src/treeUtils/draft.ts
CHANGED
|
@@ -38,6 +38,10 @@ export class Draft<T extends object> {
|
|
|
38
38
|
*/
|
|
39
39
|
@mobxAction
|
|
40
40
|
commitByPath(path: Path): void {
|
|
41
|
+
if (path.length === 0) {
|
|
42
|
+
this.commit()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
41
45
|
const draftTarget = resolvePath(this.data, path)
|
|
42
46
|
if (!draftTarget.resolved) {
|
|
43
47
|
throw failure(`path ${JSON.stringify(path)} could not be resolved in draft object`)
|
|
@@ -76,6 +80,10 @@ export class Draft<T extends object> {
|
|
|
76
80
|
*/
|
|
77
81
|
@mobxAction
|
|
78
82
|
resetByPath(path: Path): void {
|
|
83
|
+
if (path.length === 0) {
|
|
84
|
+
this.reset()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
79
87
|
const originalTarget = resolvePath(this.originalData, path)
|
|
80
88
|
if (!originalTarget.resolved) {
|
|
81
89
|
throw failure(`path ${JSON.stringify(path)} could not be resolved in original object`)
|