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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/mobx-keystone.esm.js +6702 -5797
  3. package/dist/mobx-keystone.esm.mjs +6702 -5797
  4. package/dist/mobx-keystone.umd.js +6718 -5798
  5. package/dist/types/action/runUnprotected.d.ts +8 -4
  6. package/dist/types/model/Model.d.ts +5 -0
  7. package/dist/types/modelShared/prop.d.ts +8 -4
  8. package/dist/types/snapshot/modelInitialData.d.ts +1 -0
  9. package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
  10. package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
  11. package/package.json +11 -10
  12. package/src/action/middleware.ts +5 -18
  13. package/src/action/mutationBatch.ts +72 -0
  14. package/src/action/runUnprotected.ts +21 -8
  15. package/src/action/wrapInAction.ts +45 -15
  16. package/src/dataModel/BaseDataModel.ts +4 -8
  17. package/src/deepChange/onDeepChange.ts +246 -16
  18. package/src/model/BaseModel.ts +4 -12
  19. package/src/model/Model.ts +5 -0
  20. package/src/model/newModel.ts +68 -36
  21. package/src/modelShared/prop.ts +105 -63
  22. package/src/modelShared/sharedInternalModel.ts +91 -60
  23. package/src/parent/core.ts +16 -10
  24. package/src/parent/coreObjectChildren.ts +219 -21
  25. package/src/parent/path.ts +6 -5
  26. package/src/parent/setParent.ts +5 -5
  27. package/src/patch/emitPatch.ts +102 -20
  28. package/src/snapshot/fromArraySnapshot.ts +11 -2
  29. package/src/snapshot/fromModelSnapshot.ts +10 -3
  30. package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
  31. package/src/snapshot/fromSnapshot.ts +31 -9
  32. package/src/snapshot/getSnapshot.ts +8 -3
  33. package/src/snapshot/internal.ts +243 -82
  34. package/src/snapshot/modelInitialData.ts +41 -0
  35. package/src/snapshot/reconcileArraySnapshot.ts +9 -3
  36. package/src/snapshot/reconcileModelSnapshot.ts +97 -17
  37. package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
  38. package/src/tweaker/core.ts +30 -5
  39. package/src/tweaker/treeNodeMetadata.ts +38 -0
  40. package/src/tweaker/tweak.ts +15 -4
  41. package/src/tweaker/tweakArray.ts +159 -121
  42. package/src/tweaker/tweakFrozen.ts +2 -2
  43. package/src/tweaker/tweakModel.ts +2 -2
  44. package/src/tweaker/tweakPlainObject.ts +114 -122
  45. package/src/tweaker/typeChecking.ts +10 -1
  46. package/src/types/TypeChecker.ts +261 -15
  47. package/src/types/arrayBased/typesArray.ts +38 -40
  48. package/src/types/arrayBased/typesTuple.ts +51 -34
  49. package/src/types/createCachedTypeCheck.ts +181 -0
  50. package/src/types/objectBased/typesArraySet.ts +42 -32
  51. package/src/types/objectBased/typesDataModelData.ts +18 -11
  52. package/src/types/objectBased/typesModel.ts +1 -1
  53. package/src/types/objectBased/typesObject.ts +57 -28
  54. package/src/types/objectBased/typesObjectMap.ts +51 -40
  55. package/src/types/objectBased/typesRecord.ts +59 -39
  56. package/src/types/objectBased/typesRef.ts +1 -1
  57. package/src/types/primitiveBased/typesPrimitive.ts +9 -9
  58. package/src/types/tProp.ts +80 -44
  59. package/src/types/utility/typesOr.ts +35 -24
  60. package/src/types/utility/typesRefinement.ts +21 -8
  61. package/src/types/utility/typesSkipCheck.ts +21 -8
  62. package/src/types/utility/typesTag.ts +21 -9
  63. package/src/types/utility/typesUnchecked.ts +2 -3
  64. package/src/utils/ModelPool.ts +34 -19
  65. package/src/utils/errorDiagnostics.ts +60 -18
  66. package/src/utils/index.ts +9 -7
  67. package/src/types/createPerEntryCachedCheck.ts +0 -166
  68. /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, setOwnProp } from "../utils"
4
- import { withErrorPathSegment } from "../utils/errorDiagnostics"
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
- setOwnProp(
23
- plainObj,
24
- k,
25
- withErrorPathSegment(k, () => internalFromSnapshot(v, ctx))
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, setOwnProp } from "../utils"
9
- import { runWithErrorDiagnosticsContext, withErrorPathSegment } from "../utils/errorDiagnostics"
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
- snapshot = resolveTypeChecker(codecSupport.storedType).fromSnapshotProcessor(
91
- unprocessedSnapshot
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
- snapshot = typeChecker.fromSnapshotProcessor(unprocessedSnapshot)
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
- const snapshotValue = withErrorPathSegment(k, () => internalFromSnapshot(v, ctx))
190
- setOwnProp(initialData, k, snapshotValue)
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
- return observable.object(initialData, undefined, observableOptions)
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
- return storedTypeChecker.toSnapshotProcessor(storedValue)
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
- return storedTypeChecker.toSnapshotProcessor(getSnapshot(storedTree))
59
+ const storedSnapshot = getSnapshot(storedTree)
60
+ const processor = storedTypeChecker.getToSnapshotProcessor()
61
+ return processor ? processor(storedSnapshot) : storedSnapshot
58
62
  }
59
63
 
60
- toSnapshotProcessor = resolveTypeChecker(arg1).toSnapshotProcessor
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)
@@ -1,7 +1,9 @@
1
1
  import { action, createAtom, type IAtom } from "mobx"
2
- import { fastGetParentPath, type ParentPath } from "../parent/path"
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, setOwnProp } from "../utils"
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
- interface SnapshotData {
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
- const snapshots = new WeakMap<object, SnapshotData>()
29
+ interface DirtyChild {
30
+ readonly snapshot: SnapshotData
31
+ readonly path: PathElement
32
+ }
20
33
 
21
- // true if it has been accessed publicly and therefore should be cloned
22
- // rather than modified in place
23
- const frozenState = new WeakMap<object, boolean>()
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 snapshots.get(value)
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 oldSn = getInternalSnapshot(value)
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
- snapshots.delete(value)
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
- frozenState.set(untransformed, markAsFrozen)
101
+ snapshotStates.set(untransformed, markAsFrozen ? true : sn)
91
102
 
92
103
  if (transformed !== undefined && transformed !== untransformed) {
93
- frozenState.set(transformed, markAsFrozen)
104
+ snapshotStates.set(transformed, markAsFrozen)
94
105
  }
95
106
 
96
- snapshots.set(value, sn)
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 updateParentSnapshots(value: any, sn: SnapshotData): void {
105
- // also update parent(s) snapshot(s) if needed
106
- const parent = getInternalSnapshotParent(sn, fastGetParentPath(value, false))
107
- if (!parent) {
108
- return
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
- const { parentSnapshot, parentPath } = parent
112
- // might be false in the cases where the parent has not yet been created
113
- if (!parentSnapshot) {
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
- const path = parentPath.path
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
- // patches for parent changes should not be emitted
120
- updateInternalSnapshot(parentPath.parent, (objOrArray: any) => {
121
- setOwnProp(objOrArray, path, sn.transformed)
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 const updateInternalSnapshot = action(
129
- "updateInternalSnapshot",
130
- <T extends object>(value: any, mutate: MutateInternalSnapshotFn<T>): void => {
131
- const sn = getInternalSnapshot(value)! as SnapshotData
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
- let untransformed = sn.untransformed
134
- const snFrozen = frozenState.get(untransformed)!
135
- if (snFrozen) {
136
- if (Array.isArray(untransformed)) {
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
- mutate(untransformed)
266
+ const parentSn = treeNodeMetadata.get(parentPath.parent)?.snapshot
267
+ if (!parentSn) {
268
+ return
269
+ }
148
270
 
149
- sn.untransformed = untransformed
150
- sn.transformed = sn.transformFn ? sn.transformFn(untransformed) : untransformed
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
- frozenState.set(sn.untransformed, false)
153
- if (sn.transformed !== undefined) {
154
- frozenState.set(sn.transformed, false)
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
- sn.atom?.reportChanged()
158
- updateParentSnapshots(value, sn)
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 (frozenState.get(newTransformed) === undefined) {
183
- frozenState.set(newTransformed, false)
337
+ if (snapshotStates.get(newTransformed) === undefined) {
338
+ snapshotStates.set(newTransformed, false)
184
339
  }
185
340
 
186
341
  sn.atom?.reportChanged()
187
- updateParentSnapshots(value, sn)
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
- // this might be undefined if the data comes from example from transforms
209
- const isFrozen = frozenState.get(data)
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 (isFrozen === undefined || isFrozen) {
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
- frozenState.set(data, true)
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 { runTypeCheckingAfterChange } from "../tweaker/typeChecking"
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 snapshotBeforeChanges = getSnapshot(value)
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
- runTypeCheckingAfterChange(value, undefined, snapshotBeforeChanges)
59
+ if (typeCheckingAfterChangeEnabled) {
60
+ runTypeCheckingAfterChange(value, undefined, snapshotBeforeChanges)
61
+ }
56
62
 
57
63
  return value
58
64
  }