mobx-keystone 1.22.0 → 1.23.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 +7 -0
- package/dist/mobx-keystone.esm.js +6702 -5797
- package/dist/mobx-keystone.esm.mjs +6702 -5797
- package/dist/mobx-keystone.umd.js +6718 -5798
- package/dist/types/action/runUnprotected.d.ts +8 -4
- package/dist/types/model/Model.d.ts +5 -0
- package/dist/types/modelShared/prop.d.ts +8 -4
- package/dist/types/snapshot/modelInitialData.d.ts +1 -0
- package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
- package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
- package/package.json +11 -10
- package/src/action/middleware.ts +5 -18
- package/src/action/mutationBatch.ts +72 -0
- package/src/action/runUnprotected.ts +21 -8
- package/src/action/wrapInAction.ts +45 -15
- package/src/dataModel/BaseDataModel.ts +4 -8
- package/src/deepChange/onDeepChange.ts +246 -16
- package/src/model/BaseModel.ts +4 -12
- package/src/model/Model.ts +5 -0
- package/src/model/newModel.ts +68 -36
- package/src/modelShared/prop.ts +105 -63
- package/src/modelShared/sharedInternalModel.ts +91 -60
- package/src/parent/core.ts +16 -10
- package/src/parent/coreObjectChildren.ts +219 -21
- package/src/parent/path.ts +6 -5
- package/src/parent/setParent.ts +5 -5
- package/src/patch/emitPatch.ts +102 -20
- package/src/snapshot/fromArraySnapshot.ts +11 -2
- package/src/snapshot/fromModelSnapshot.ts +10 -3
- package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
- package/src/snapshot/fromSnapshot.ts +31 -9
- package/src/snapshot/getSnapshot.ts +8 -3
- package/src/snapshot/internal.ts +243 -82
- package/src/snapshot/modelInitialData.ts +41 -0
- package/src/snapshot/reconcileArraySnapshot.ts +9 -3
- package/src/snapshot/reconcileModelSnapshot.ts +97 -17
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
- package/src/tweaker/core.ts +30 -5
- package/src/tweaker/treeNodeMetadata.ts +38 -0
- package/src/tweaker/tweak.ts +15 -4
- package/src/tweaker/tweakArray.ts +159 -121
- package/src/tweaker/tweakFrozen.ts +2 -2
- package/src/tweaker/tweakModel.ts +2 -2
- package/src/tweaker/tweakPlainObject.ts +114 -122
- package/src/tweaker/typeChecking.ts +10 -1
- package/src/types/TypeChecker.ts +261 -15
- package/src/types/arrayBased/typesArray.ts +38 -40
- package/src/types/arrayBased/typesTuple.ts +51 -34
- package/src/types/createCachedTypeCheck.ts +181 -0
- package/src/types/objectBased/typesArraySet.ts +42 -32
- package/src/types/objectBased/typesDataModelData.ts +18 -11
- package/src/types/objectBased/typesModel.ts +1 -1
- package/src/types/objectBased/typesObject.ts +57 -28
- package/src/types/objectBased/typesObjectMap.ts +51 -40
- package/src/types/objectBased/typesRecord.ts +59 -39
- package/src/types/objectBased/typesRef.ts +1 -1
- package/src/types/primitiveBased/typesPrimitive.ts +9 -9
- package/src/types/tProp.ts +80 -44
- package/src/types/utility/typesOr.ts +35 -24
- package/src/types/utility/typesRefinement.ts +21 -8
- package/src/types/utility/typesSkipCheck.ts +21 -8
- package/src/types/utility/typesTag.ts +21 -9
- package/src/types/utility/typesUnchecked.ts +2 -3
- package/src/utils/ModelPool.ts +34 -19
- package/src/utils/errorDiagnostics.ts +60 -18
- package/src/utils/index.ts +9 -7
- package/src/types/createPerEntryCachedCheck.ts +0 -166
- /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { observable } from "mobx"
|
|
2
2
|
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
|
|
3
|
-
import { isPlainObject,
|
|
4
|
-
import {
|
|
3
|
+
import { isPlainObject, setProtoProp } from "../utils"
|
|
4
|
+
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
5
5
|
import {
|
|
6
6
|
type FromSnapshotContext,
|
|
7
7
|
internalFromSnapshot,
|
|
@@ -13,17 +13,28 @@ import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPrio
|
|
|
13
13
|
|
|
14
14
|
function fromPlainObjectSnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotContext): object {
|
|
15
15
|
const plainObj: Record<string, unknown> = {}
|
|
16
|
+
// Hydration visits every property. Keep this direct stack use rather than
|
|
17
|
+
// withErrorPathSegment: it avoids one callback closure per property and has
|
|
18
|
+
// a measured production hydration win.
|
|
19
|
+
const errorDiagnosticsContext = getCurrentErrorDiagnosticsContext()
|
|
16
20
|
|
|
17
21
|
const snKeys = Object.keys(sn)
|
|
18
22
|
const snKeysLen = snKeys.length
|
|
19
23
|
for (let i = 0; i < snKeysLen; i++) {
|
|
20
24
|
const k = snKeys[i]
|
|
21
25
|
const v = sn[k]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
errorDiagnosticsContext?.pushPath(k)
|
|
27
|
+
let snapshotValue: unknown
|
|
28
|
+
try {
|
|
29
|
+
snapshotValue = internalFromSnapshot(v, ctx)
|
|
30
|
+
} finally {
|
|
31
|
+
errorDiagnosticsContext?.popPath()
|
|
32
|
+
}
|
|
33
|
+
if (k === "__proto__") {
|
|
34
|
+
setProtoProp(plainObj, snapshotValue)
|
|
35
|
+
} else {
|
|
36
|
+
plainObj[k] = snapshotValue
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
return tweakPlainObject(
|
|
29
40
|
observable.object(plainObj, undefined, observableOptions),
|
|
@@ -5,8 +5,12 @@ 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,
|
|
9
|
-
import {
|
|
8
|
+
import { isMap, isPrimitive, isSet, setProtoProp } from "../utils"
|
|
9
|
+
import {
|
|
10
|
+
getCurrentErrorDiagnosticsContext,
|
|
11
|
+
runWithErrorDiagnosticsContext,
|
|
12
|
+
} from "../utils/errorDiagnostics"
|
|
13
|
+
import { setModelInitialDataSnapshot } from "./modelInitialData"
|
|
10
14
|
import { registerDefaultSnapshotters } from "./registerDefaultSnapshotters"
|
|
11
15
|
import type { SnapshotInOf, SnapshotInOfModel, SnapshotOutOf } from "./SnapshotOf"
|
|
12
16
|
import { SnapshotProcessingError } from "./SnapshotProcessingError"
|
|
@@ -87,9 +91,8 @@ export function fromSnapshot<T>(arg1: any, arg2: any, arg3?: any): T {
|
|
|
87
91
|
|
|
88
92
|
if (codecSupport.hasCodec) {
|
|
89
93
|
unprocessedSnapshot = arg2
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
94
|
+
const processor = resolveTypeChecker(codecSupport.storedType).getFromSnapshotProcessor()
|
|
95
|
+
snapshot = processor ? processor(unprocessedSnapshot) : unprocessedSnapshot
|
|
93
96
|
options = arg3
|
|
94
97
|
const storedValue = fromSnapshotAction(snapshot, unprocessedSnapshot, options)
|
|
95
98
|
return codecSupport.adapter.toRuntime(storedValue)
|
|
@@ -97,7 +100,8 @@ export function fromSnapshot<T>(arg1: any, arg2: any, arg3?: any): T {
|
|
|
97
100
|
|
|
98
101
|
const typeChecker = resolveTypeChecker(resolvedType)
|
|
99
102
|
unprocessedSnapshot = arg2
|
|
100
|
-
|
|
103
|
+
const processor = typeChecker.getFromSnapshotProcessor()
|
|
104
|
+
snapshot = processor ? processor(unprocessedSnapshot) : unprocessedSnapshot
|
|
101
105
|
options = arg3
|
|
102
106
|
} else {
|
|
103
107
|
snapshot = arg1
|
|
@@ -179,6 +183,11 @@ function snapshotToInitialData(
|
|
|
179
183
|
processedSn: SnapshotInOfModel<AnyModel>
|
|
180
184
|
): any {
|
|
181
185
|
const initialData: Record<string, unknown> = {}
|
|
186
|
+
let allValuesPrimitive = true
|
|
187
|
+
// Model hydration visits every data slot. Keep this direct stack use rather
|
|
188
|
+
// than withErrorPathSegment: it avoids one callback closure per slot and
|
|
189
|
+
// has a measured production hydration win.
|
|
190
|
+
const errorDiagnosticsContext = getCurrentErrorDiagnosticsContext()
|
|
182
191
|
|
|
183
192
|
const processedSnKeys = Object.keys(processedSn)
|
|
184
193
|
const processedSnKeysLen = processedSnKeys.length
|
|
@@ -186,11 +195,24 @@ function snapshotToInitialData(
|
|
|
186
195
|
const k = processedSnKeys[i]
|
|
187
196
|
if (!isReservedModelKey(k)) {
|
|
188
197
|
const v = processedSn[k]
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
errorDiagnosticsContext?.pushPath(k)
|
|
199
|
+
let snapshotValue: unknown
|
|
200
|
+
try {
|
|
201
|
+
snapshotValue = internalFromSnapshot(v, ctx)
|
|
202
|
+
} finally {
|
|
203
|
+
errorDiagnosticsContext?.popPath()
|
|
204
|
+
}
|
|
205
|
+
allValuesPrimitive &&= isPrimitive(snapshotValue)
|
|
206
|
+
if (k === "__proto__") {
|
|
207
|
+
setProtoProp(initialData, snapshotValue)
|
|
208
|
+
} else {
|
|
209
|
+
initialData[k] = snapshotValue
|
|
210
|
+
}
|
|
191
211
|
}
|
|
192
212
|
}
|
|
193
|
-
|
|
213
|
+
const observableInitialData = observable.object(initialData, undefined, observableOptions)
|
|
214
|
+
setModelInitialDataSnapshot(observableInitialData, initialData, allValuesPrimitive)
|
|
215
|
+
return observableInitialData
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
export const observableOptions = {
|
|
@@ -5,6 +5,7 @@ import type { AnyType, TypeToData, TypeToSnapshotOut } from "../types/schemas"
|
|
|
5
5
|
import { resolveCodecSupport } from "../types/utility/typesCodec"
|
|
6
6
|
import { failure, identityFn, isPrimitive } from "../utils"
|
|
7
7
|
import {
|
|
8
|
+
flushInternalSnapshot,
|
|
8
9
|
freezeInternalSnapshot,
|
|
9
10
|
getInternalSnapshot,
|
|
10
11
|
reportInternalSnapshotObserved,
|
|
@@ -48,16 +49,19 @@ export function getSnapshot(...args: [arg1: any] | [arg1: any, arg2: any]): any
|
|
|
48
49
|
const storedValue = codecSupport.adapter.toStored(arg2)
|
|
49
50
|
|
|
50
51
|
if (isPrimitive(storedValue)) {
|
|
51
|
-
|
|
52
|
+
const processor = storedTypeChecker.getToSnapshotProcessor()
|
|
53
|
+
return processor ? processor(storedValue) : storedValue
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
const storedTree = isTweakedObject(storedValue, true)
|
|
55
57
|
? storedValue
|
|
56
58
|
: toTreeNode(codecSupport.storedType, storedValue)
|
|
57
|
-
|
|
59
|
+
const storedSnapshot = getSnapshot(storedTree)
|
|
60
|
+
const processor = storedTypeChecker.getToSnapshotProcessor()
|
|
61
|
+
return processor ? processor(storedSnapshot) : storedSnapshot
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
toSnapshotProcessor = resolveTypeChecker(arg1).
|
|
64
|
+
toSnapshotProcessor = resolveTypeChecker(arg1).getToSnapshotProcessor() ?? identityFn
|
|
61
65
|
nodeOrPrimitive = arg2
|
|
62
66
|
} else {
|
|
63
67
|
nodeOrPrimitive = arg1
|
|
@@ -74,6 +78,7 @@ export function getSnapshot(...args: [arg1: any] | [arg1: any, arg2: any]): any
|
|
|
74
78
|
throw failure("getSnapshot is not supported for this kind of object")
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
flushInternalSnapshot(nodeOrPrimitive, true)
|
|
77
82
|
freezeInternalSnapshot(snapshot.transformed)
|
|
78
83
|
reportInternalSnapshotObserved(snapshot)
|
|
79
84
|
return toSnapshotProcessor(snapshot.transformed)
|
package/src/snapshot/internal.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { action, createAtom, type IAtom } from "mobx"
|
|
2
|
-
import { fastGetParentPath
|
|
2
|
+
import { fastGetParentPath } from "../parent/path"
|
|
3
|
+
import type { PathElement } from "../parent/pathTypes"
|
|
4
|
+
import { getOrCreateTreeNodeMetadata, treeNodeMetadata } from "../tweaker/treeNodeMetadata"
|
|
3
5
|
import { invalidateCachedToSnapshotProcessorResult } from "../types/TypeChecker"
|
|
4
|
-
import { clonePlainObject, isPrimitive,
|
|
6
|
+
import { clonePlainObject, failure, inDevMode, isPrimitive, setProtoProp } from "../utils"
|
|
5
7
|
import type { PrimitiveValue } from "../utils/types"
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -9,18 +11,31 @@ import type { PrimitiveValue } from "../utils/types"
|
|
|
9
11
|
*/
|
|
10
12
|
export type SnapshotTransformFn = (sn: unknown) => unknown
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
/** @internal */
|
|
15
|
+
export interface SnapshotData {
|
|
13
16
|
untransformed: any
|
|
14
17
|
readonly transformFn: SnapshotTransformFn | undefined
|
|
15
18
|
transformed: any
|
|
16
19
|
atom: IAtom | undefined
|
|
20
|
+
// Source of truth for copy-on-write of this snapshot's untransformed data.
|
|
21
|
+
untransformedFrozen: boolean
|
|
22
|
+
dirtyChild: object | undefined
|
|
23
|
+
dirtyChildSnapshot: SnapshotData | undefined
|
|
24
|
+
dirtyChildPath: PathElement | undefined
|
|
25
|
+
additionalDirtyChildren: Map<object, DirtyChild> | undefined
|
|
26
|
+
isUnset: boolean
|
|
17
27
|
}
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
interface DirtyChild {
|
|
30
|
+
readonly snapshot: SnapshotData
|
|
31
|
+
readonly path: PathElement
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
34
|
+
// `false` means the generic freezer must traverse this raw value; `true` means
|
|
35
|
+
// it is frozen; a SnapshotData value means it is mutable and its frozen state
|
|
36
|
+
// must be reflected back into that owner. `undefined` is either external /
|
|
37
|
+
// untracked data or a fused frozen clone, whose owner tracks its state directly.
|
|
38
|
+
const snapshotStates = new WeakMap<object, boolean | SnapshotData>()
|
|
24
39
|
|
|
25
40
|
/**
|
|
26
41
|
* @internal
|
|
@@ -28,43 +43,33 @@ const frozenState = new WeakMap<object, boolean>()
|
|
|
28
43
|
export function getInternalSnapshot<T extends object>(
|
|
29
44
|
value: T
|
|
30
45
|
): Readonly<SnapshotData> | undefined {
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface InternalSnapshotParent {
|
|
35
|
-
parentSnapshot: SnapshotData | undefined
|
|
36
|
-
parentPath: ParentPath<any>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getInternalSnapshotParent(
|
|
40
|
-
sn: Readonly<SnapshotData> | undefined,
|
|
41
|
-
parentPath: ParentPath<any> | undefined
|
|
42
|
-
): InternalSnapshotParent | undefined {
|
|
43
|
-
if (!(parentPath && sn)) {
|
|
44
|
-
return undefined
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const parentSn = getInternalSnapshot(parentPath.parent)
|
|
48
|
-
if (!parentSn) {
|
|
49
|
-
return undefined
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
parentSnapshot: parentSn,
|
|
54
|
-
parentPath: parentPath,
|
|
55
|
-
}
|
|
46
|
+
return treeNodeMetadata.get(value)?.snapshot
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
/**
|
|
59
50
|
* @internal
|
|
60
51
|
*/
|
|
61
52
|
export const unsetInternalSnapshot = action("unsetInternalSnapshot", (value: any) => {
|
|
62
|
-
const
|
|
53
|
+
const metadata = treeNodeMetadata.get(value)
|
|
54
|
+
if (!metadata) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const oldSn = metadata.snapshot
|
|
59
|
+
if (!oldSn) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
63
62
|
|
|
64
|
-
if (oldSn) {
|
|
65
|
-
|
|
66
|
-
oldSn.atom?.reportChanged()
|
|
63
|
+
if (inDevMode && oldSn.dirtyChild !== undefined) {
|
|
64
|
+
throw failure("assertion failed: cannot unset an internal snapshot with dirty children")
|
|
67
65
|
}
|
|
66
|
+
oldSn.isUnset = true
|
|
67
|
+
if (snapshotStates.get(oldSn.untransformed) === oldSn) {
|
|
68
|
+
// Preserve the former frozen-state tracking after dropping the owner.
|
|
69
|
+
snapshotStates.set(oldSn.untransformed, false)
|
|
70
|
+
}
|
|
71
|
+
metadata.snapshot = undefined
|
|
72
|
+
oldSn.atom?.reportChanged()
|
|
68
73
|
})
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -85,15 +90,21 @@ export const setNewInternalSnapshot = action(
|
|
|
85
90
|
transformFn,
|
|
86
91
|
transformed,
|
|
87
92
|
atom: undefined, // will be created when first observed
|
|
93
|
+
untransformedFrozen: markAsFrozen,
|
|
94
|
+
dirtyChild: undefined,
|
|
95
|
+
dirtyChildSnapshot: undefined,
|
|
96
|
+
dirtyChildPath: undefined,
|
|
97
|
+
additionalDirtyChildren: undefined,
|
|
98
|
+
isUnset: false,
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
|
|
101
|
+
snapshotStates.set(untransformed, markAsFrozen ? true : sn)
|
|
91
102
|
|
|
92
103
|
if (transformed !== undefined && transformed !== untransformed) {
|
|
93
|
-
|
|
104
|
+
snapshotStates.set(transformed, markAsFrozen)
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
|
|
107
|
+
getOrCreateTreeNodeMetadata(value).snapshot = sn
|
|
97
108
|
|
|
98
109
|
sn.atom?.reportChanged()
|
|
99
110
|
}
|
|
@@ -101,61 +112,205 @@ export const setNewInternalSnapshot = action(
|
|
|
101
112
|
|
|
102
113
|
type MutateInternalSnapshotFn<T> = (prevSn: T) => void
|
|
103
114
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
function makeSnapshotMutable(sn: SnapshotData): any {
|
|
116
|
+
let untransformed = sn.untransformed
|
|
117
|
+
if (sn.untransformedFrozen) {
|
|
118
|
+
untransformed = Array.isArray(untransformed)
|
|
119
|
+
? untransformed.slice()
|
|
120
|
+
: clonePlainObject(untransformed)
|
|
121
|
+
} else {
|
|
122
|
+
invalidateCachedToSnapshotProcessorResult(untransformed)
|
|
123
|
+
}
|
|
124
|
+
return untransformed
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned = false): void {
|
|
128
|
+
// `makeSnapshotMutable` returns a new container exactly when this snapshot
|
|
129
|
+
// was frozen. All callers obtain `untransformed` through that helper.
|
|
130
|
+
const wasFrozen = untransformed !== sn.untransformed
|
|
131
|
+
const transformed = sn.transformFn ? sn.transformFn(untransformed) : untransformed
|
|
132
|
+
|
|
133
|
+
sn.untransformed = untransformed
|
|
134
|
+
sn.transformed = transformed
|
|
135
|
+
|
|
136
|
+
const keepFrozen = freezeIfCloned && wasFrozen && !sn.transformFn
|
|
137
|
+
sn.untransformedFrozen = keepFrozen
|
|
138
|
+
if (wasFrozen && !keepFrozen) {
|
|
139
|
+
snapshotStates.set(untransformed, sn)
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
sn.transformFn &&
|
|
143
|
+
transformed !== undefined &&
|
|
144
|
+
(!wasFrozen || transformed !== untransformed)
|
|
145
|
+
) {
|
|
146
|
+
snapshotStates.set(transformed, false)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function updateOwnSnapshot<T>(sn: SnapshotData, mutate: MutateInternalSnapshotFn<T>): void {
|
|
151
|
+
const untransformed = makeSnapshotMutable(sn)
|
|
152
|
+
mutate(untransformed)
|
|
153
|
+
setSnapshotData(sn, untransformed)
|
|
154
|
+
sn.atom?.reportChanged()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function updateParentSlot(parentSn: SnapshotData, path: PathElement, value: any): void {
|
|
158
|
+
const untransformed = makeSnapshotMutable(parentSn)
|
|
159
|
+
if (path === "__proto__") {
|
|
160
|
+
setProtoProp(untransformed, value)
|
|
161
|
+
} else {
|
|
162
|
+
untransformed[path] = value
|
|
109
163
|
}
|
|
164
|
+
setSnapshotData(parentSn, untransformed)
|
|
165
|
+
parentSn.atom?.reportChanged()
|
|
166
|
+
}
|
|
110
167
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
168
|
+
function flushDirtyChild(
|
|
169
|
+
node: object,
|
|
170
|
+
untransformed: any,
|
|
171
|
+
child: object,
|
|
172
|
+
childSn: SnapshotData,
|
|
173
|
+
childPath: PathElement,
|
|
174
|
+
freezeAfterFlush: boolean,
|
|
175
|
+
freezeClonedContainer: boolean
|
|
176
|
+
): void {
|
|
177
|
+
if (childSn.isUnset) {
|
|
114
178
|
return
|
|
115
179
|
}
|
|
116
180
|
|
|
117
|
-
|
|
181
|
+
if (inDevMode) {
|
|
182
|
+
const currentParentPath = fastGetParentPath(child, false)
|
|
183
|
+
if (currentParentPath?.parent !== node || currentParentPath.path !== childPath) {
|
|
184
|
+
throw failure("assertion failed: dirty child parent path changed before flush")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const childIsFrozen = flushDirtyChildren(child, childSn, freezeAfterFlush)
|
|
189
|
+
if (freezeClonedContainer && !childIsFrozen) {
|
|
190
|
+
freezeInternalSnapshot(childSn.transformed)
|
|
191
|
+
}
|
|
192
|
+
// The overwhelmingly common snapshot path is a normal property or array
|
|
193
|
+
// index. Keep the `__proto__` safe-definition behavior for the exceptional
|
|
194
|
+
// key without paying its helper call on every observed ancestor flush.
|
|
195
|
+
if (childPath === "__proto__") {
|
|
196
|
+
setProtoProp(untransformed, childSn.transformed)
|
|
197
|
+
} else {
|
|
198
|
+
untransformed[childPath] = childSn.transformed
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: boolean): boolean {
|
|
203
|
+
const dirtyChild = sn.dirtyChild
|
|
204
|
+
const dirtyChildSnapshot = sn.dirtyChildSnapshot
|
|
205
|
+
const dirtyChildPath = sn.dirtyChildPath
|
|
206
|
+
if (!dirtyChild || !dirtyChildSnapshot || dirtyChildPath === undefined) {
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
118
209
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
210
|
+
const additionalDirtyChildren = sn.additionalDirtyChildren
|
|
211
|
+
sn.dirtyChild = undefined
|
|
212
|
+
sn.dirtyChildSnapshot = undefined
|
|
213
|
+
sn.dirtyChildPath = undefined
|
|
214
|
+
sn.additionalDirtyChildren = undefined
|
|
215
|
+
const untransformed = makeSnapshotMutable(sn)
|
|
216
|
+
const freezeClonedContainer =
|
|
217
|
+
freezeAfterFlush && untransformed !== sn.untransformed && !sn.transformFn
|
|
218
|
+
|
|
219
|
+
flushDirtyChild(
|
|
220
|
+
node,
|
|
221
|
+
untransformed,
|
|
222
|
+
dirtyChild,
|
|
223
|
+
dirtyChildSnapshot,
|
|
224
|
+
dirtyChildPath,
|
|
225
|
+
freezeAfterFlush,
|
|
226
|
+
freezeClonedContainer
|
|
227
|
+
)
|
|
228
|
+
additionalDirtyChildren?.forEach((dirtyChild, child) => {
|
|
229
|
+
flushDirtyChild(
|
|
230
|
+
node,
|
|
231
|
+
untransformed,
|
|
232
|
+
child,
|
|
233
|
+
dirtyChild.snapshot,
|
|
234
|
+
dirtyChild.path,
|
|
235
|
+
freezeAfterFlush,
|
|
236
|
+
freezeClonedContainer
|
|
237
|
+
)
|
|
122
238
|
})
|
|
239
|
+
|
|
240
|
+
setSnapshotData(sn, untransformed, freezeClonedContainer)
|
|
241
|
+
return freezeClonedContainer
|
|
123
242
|
}
|
|
124
243
|
|
|
125
244
|
/**
|
|
245
|
+
* Flushes any pending descendant snapshot updates for a node.
|
|
246
|
+
*
|
|
126
247
|
* @internal
|
|
127
248
|
*/
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
249
|
+
export function flushInternalSnapshot(value: object, freezeAfterFlush: boolean): void {
|
|
250
|
+
const sn = treeNodeMetadata.get(value)?.snapshot
|
|
251
|
+
if (sn) {
|
|
252
|
+
flushDirtyChildren(value, sn, freezeAfterFlush)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function updateAncestorSnapshots(value: object, sn: SnapshotData): void {
|
|
257
|
+
let child = value
|
|
258
|
+
let childSn = sn
|
|
132
259
|
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
untransformed = untransformed.slice()
|
|
138
|
-
} else {
|
|
139
|
-
untransformed = clonePlainObject(untransformed)
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
// the processor cached result is no longer valid since we will
|
|
143
|
-
// mutate the object
|
|
144
|
-
invalidateCachedToSnapshotProcessorResult(untransformed)
|
|
260
|
+
while (true) {
|
|
261
|
+
const parentPath = fastGetParentPath(child, false)
|
|
262
|
+
if (!parentPath) {
|
|
263
|
+
return
|
|
145
264
|
}
|
|
146
265
|
|
|
147
|
-
|
|
266
|
+
const parentSn = treeNodeMetadata.get(parentPath.parent)?.snapshot
|
|
267
|
+
if (!parentSn) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
148
270
|
|
|
149
|
-
|
|
150
|
-
|
|
271
|
+
if (parentSn.transformFn) {
|
|
272
|
+
// Model output snapshot processors are part of the public mutation
|
|
273
|
+
// contract. Keep their invocation eager, flushing any plain segment below
|
|
274
|
+
// this model before recomputing it.
|
|
275
|
+
flushDirtyChildren(child, childSn, false)
|
|
276
|
+
flushDirtyChildren(parentPath.parent, parentSn, false)
|
|
277
|
+
updateParentSlot(parentSn, parentPath.path, childSn.transformed)
|
|
278
|
+
child = parentPath.parent
|
|
279
|
+
childSn = parentSn
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
151
282
|
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
283
|
+
const wasDirty = parentSn.dirtyChild !== undefined
|
|
284
|
+
if (!wasDirty) {
|
|
285
|
+
parentSn.dirtyChild = child
|
|
286
|
+
parentSn.dirtyChildSnapshot = childSn
|
|
287
|
+
parentSn.dirtyChildPath = parentPath.path
|
|
288
|
+
} else if (parentSn.dirtyChild !== child) {
|
|
289
|
+
;(parentSn.additionalDirtyChildren ??= new Map()).set(child, {
|
|
290
|
+
snapshot: childSn,
|
|
291
|
+
path: parentPath.path,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
parentSn.atom?.reportChanged()
|
|
295
|
+
if (wasDirty) {
|
|
296
|
+
return
|
|
155
297
|
}
|
|
156
298
|
|
|
157
|
-
|
|
158
|
-
|
|
299
|
+
child = parentPath.parent
|
|
300
|
+
childSn = parentSn
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @internal
|
|
306
|
+
*/
|
|
307
|
+
export const updateInternalSnapshot = action(
|
|
308
|
+
"updateInternalSnapshot",
|
|
309
|
+
<T extends object>(value: any, mutate: MutateInternalSnapshotFn<T>): void => {
|
|
310
|
+
const sn = getInternalSnapshot(value)! as SnapshotData
|
|
311
|
+
flushDirtyChildren(value, sn, false)
|
|
312
|
+
updateOwnSnapshot(sn, mutate)
|
|
313
|
+
updateAncestorSnapshots(value, sn)
|
|
159
314
|
}
|
|
160
315
|
)
|
|
161
316
|
|
|
@@ -179,12 +334,12 @@ export const refreshInternalSnapshot = action("refreshInternalSnapshot", (value:
|
|
|
179
334
|
|
|
180
335
|
// transformed snapshots created by internal transforms must be tracked as mutable
|
|
181
336
|
// until they are exposed through getSnapshot / freezeInternalSnapshot.
|
|
182
|
-
if (
|
|
183
|
-
|
|
337
|
+
if (snapshotStates.get(newTransformed) === undefined) {
|
|
338
|
+
snapshotStates.set(newTransformed, false)
|
|
184
339
|
}
|
|
185
340
|
|
|
186
341
|
sn.atom?.reportChanged()
|
|
187
|
-
|
|
342
|
+
updateAncestorSnapshots(value, sn)
|
|
188
343
|
})
|
|
189
344
|
|
|
190
345
|
/**
|
|
@@ -205,10 +360,12 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
205
360
|
return data
|
|
206
361
|
}
|
|
207
362
|
|
|
208
|
-
//
|
|
209
|
-
|
|
363
|
+
// `undefined` means external/untracked data (for example, from a transform).
|
|
364
|
+
// For a tracked frozen value, all reachable tracked descendants were frozen
|
|
365
|
+
// first, so returning early here preserves snapshot immutability.
|
|
366
|
+
const state = snapshotStates.get(data)
|
|
210
367
|
|
|
211
|
-
if (
|
|
368
|
+
if (state === undefined || state === true) {
|
|
212
369
|
// already frozen or an external data (e.g. from a transform)
|
|
213
370
|
return data
|
|
214
371
|
}
|
|
@@ -224,7 +381,11 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
224
381
|
}
|
|
225
382
|
}
|
|
226
383
|
|
|
227
|
-
|
|
384
|
+
snapshotStates.set(data, true)
|
|
385
|
+
|
|
386
|
+
if (typeof state === "object" && state.untransformed === data) {
|
|
387
|
+
state.untransformedFrozen = true
|
|
388
|
+
}
|
|
228
389
|
|
|
229
390
|
return data
|
|
230
391
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
interface ModelInitialDataSnapshot {
|
|
2
|
+
readonly snapshot: Record<string, unknown>
|
|
3
|
+
readonly allValuesPrimitive: boolean
|
|
4
|
+
reusable: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const modelInitialDataSnapshots = new WeakMap<object, ModelInitialDataSnapshot>()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export function setModelInitialDataSnapshot(
|
|
13
|
+
data: object,
|
|
14
|
+
snapshot: Record<string, unknown>,
|
|
15
|
+
allValuesPrimitive: boolean
|
|
16
|
+
): void {
|
|
17
|
+
modelInitialDataSnapshots.set(data, {
|
|
18
|
+
snapshot,
|
|
19
|
+
allValuesPrimitive,
|
|
20
|
+
reusable: true,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function invalidateModelInitialDataSnapshot(data: object): void {
|
|
28
|
+
const initialDataSnapshot = modelInitialDataSnapshots.get(data)
|
|
29
|
+
if (initialDataSnapshot) {
|
|
30
|
+
initialDataSnapshot.reusable = false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export function takeModelInitialDataSnapshot(data: object): ModelInitialDataSnapshot | undefined {
|
|
38
|
+
const initialDataSnapshot = modelInitialDataSnapshots.get(data)
|
|
39
|
+
modelInitialDataSnapshots.delete(data)
|
|
40
|
+
return initialDataSnapshot
|
|
41
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isTypeCheckingAfterChangeEnabled,
|
|
3
|
+
runTypeCheckingAfterChange,
|
|
4
|
+
} from "../tweaker/typeChecking"
|
|
2
5
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
3
6
|
import { isArray } from "../utils"
|
|
4
7
|
import { withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
@@ -20,7 +23,8 @@ function reconcileArraySnapshot(
|
|
|
20
23
|
return fromSnapshot(sn)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
const
|
|
26
|
+
const typeCheckingAfterChangeEnabled = isTypeCheckingAfterChangeEnabled()
|
|
27
|
+
const snapshotBeforeChanges = typeCheckingAfterChangeEnabled ? getSnapshot(value) : undefined
|
|
24
28
|
|
|
25
29
|
withoutTypeChecking(() => {
|
|
26
30
|
// remove excess items
|
|
@@ -52,7 +56,9 @@ function reconcileArraySnapshot(
|
|
|
52
56
|
}
|
|
53
57
|
})
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
if (typeCheckingAfterChangeEnabled) {
|
|
60
|
+
runTypeCheckingAfterChange(value, undefined, snapshotBeforeChanges)
|
|
61
|
+
}
|
|
56
62
|
|
|
57
63
|
return value
|
|
58
64
|
}
|