mobx-keystone 1.24.1 → 2.0.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 (122) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +5 -1
  3. package/dist/mobx-keystone.esm.js +1608 -1072
  4. package/dist/mobx-keystone.esm.mjs +1608 -1072
  5. package/dist/mobx-keystone.umd.js +1613 -1072
  6. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  7. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  8. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  9. package/dist/types/parent/detach.d.ts +1 -1
  10. package/dist/types/parent/findChildren.d.ts +1 -1
  11. package/dist/types/patch/applyPatches.d.ts +2 -0
  12. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  13. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  14. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  15. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  16. package/dist/types/types/TypeCheckError.d.ts +1 -0
  17. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  18. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  19. package/dist/types/utils/index.d.ts +13 -0
  20. package/dist/types/utils/mapUtils.d.ts +2 -2
  21. package/dist/types/utils/setLike.d.ts +1 -0
  22. package/package.json +7 -7
  23. package/src/action/applyAction.ts +1 -1
  24. package/src/action/middleware.ts +17 -36
  25. package/src/action/modelFlow.ts +4 -5
  26. package/src/action/mutationBatch.ts +10 -10
  27. package/src/action/pendingActions.ts +3 -4
  28. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  29. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  30. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  31. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  32. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  33. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  34. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  35. package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
  36. package/src/actionMiddlewares/undoMiddleware.ts +76 -60
  37. package/src/actionMiddlewares/utils.ts +2 -1
  38. package/src/computedTree/computedTree.ts +14 -3
  39. package/src/context/context.ts +17 -45
  40. package/src/dataModel/BaseDataModel.ts +20 -16
  41. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  42. package/src/deepChange/onDeepChange.ts +179 -24
  43. package/src/frozen/Frozen.ts +50 -33
  44. package/src/model/BaseModel.ts +15 -14
  45. package/src/model/newModel.ts +32 -28
  46. package/src/model/utils.ts +2 -2
  47. package/src/modelShared/modelClassInitializer.ts +3 -2
  48. package/src/modelShared/modelDecorator.ts +34 -14
  49. package/src/modelShared/modelInfo.ts +1 -1
  50. package/src/modelShared/prop.ts +6 -9
  51. package/src/modelShared/sharedInternalModel.ts +16 -3
  52. package/src/parent/detach.ts +1 -1
  53. package/src/parent/findChildren.ts +4 -7
  54. package/src/parent/findParent.ts +14 -9
  55. package/src/parent/onChildAttachedTo.ts +45 -40
  56. package/src/parent/path.ts +11 -7
  57. package/src/parent/setParent.ts +33 -14
  58. package/src/parent/walkTree.ts +21 -37
  59. package/src/patch/applyPatches.ts +75 -60
  60. package/src/patch/emitPatch.ts +142 -26
  61. package/src/patch/jsonPatch.ts +5 -0
  62. package/src/patch/patchRecorder.ts +18 -14
  63. package/src/redux/connectReduxDevTools.ts +24 -18
  64. package/src/redux/redux.ts +5 -21
  65. package/src/ref/core.ts +18 -2
  66. package/src/rootStore/attachDetach.ts +27 -16
  67. package/src/rootStore/rootStore.ts +10 -6
  68. package/src/snapshot/SnapshotOf.ts +2 -2
  69. package/src/snapshot/applySnapshot.ts +10 -1
  70. package/src/snapshot/clone.ts +1 -2
  71. package/src/snapshot/cloneTreeValue.ts +1 -2
  72. package/src/snapshot/fromModelSnapshot.ts +6 -2
  73. package/src/snapshot/fromSnapshot.ts +3 -1
  74. package/src/snapshot/internal.ts +22 -9
  75. package/src/snapshot/modelInitialData.ts +14 -19
  76. package/src/snapshot/onSnapshot.ts +12 -1
  77. package/src/snapshot/reconcileModelSnapshot.ts +16 -8
  78. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  79. package/src/snapshot/reconcileSnapshot.ts +13 -4
  80. package/src/standardActions/arrayActions.ts +12 -2
  81. package/src/standardActions/objectActions.ts +3 -2
  82. package/src/transforms/ImmutableDate.ts +4 -0
  83. package/src/transforms/asMap.ts +3 -1
  84. package/src/treeUtils/deepEquals.ts +15 -13
  85. package/src/treeUtils/draft.ts +8 -0
  86. package/src/treeUtils/sandbox.ts +31 -27
  87. package/src/tweaker/core.ts +2 -4
  88. package/src/tweaker/tweak.ts +8 -8
  89. package/src/tweaker/tweakArray.ts +86 -37
  90. package/src/tweaker/tweakFrozen.ts +0 -1
  91. package/src/tweaker/tweakModel.ts +0 -1
  92. package/src/tweaker/tweakPlainObject.ts +78 -29
  93. package/src/tweaker/typeChecking.ts +132 -33
  94. package/src/types/TypeCheckError.ts +17 -19
  95. package/src/types/createCachedTypeCheck.ts +27 -2
  96. package/src/types/objectBased/typesDataModelData.ts +4 -2
  97. package/src/types/objectBased/typesModel.ts +4 -2
  98. package/src/types/objectBased/typesObject.ts +20 -16
  99. package/src/types/objectBased/typesRecord.ts +6 -13
  100. package/src/types/objectBased/typesRef.ts +2 -2
  101. package/src/types/primitiveBased/typesEnum.ts +4 -2
  102. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  103. package/src/types/typeCheckErrorUtils.ts +1 -0
  104. package/src/types/utility/typesCodecSupport.ts +97 -56
  105. package/src/types/utility/typesMap.ts +24 -41
  106. package/src/types/utility/typesRefinement.ts +3 -2
  107. package/src/types/utility/typesSet.ts +24 -0
  108. package/src/utils/ModelPool.ts +3 -3
  109. package/src/utils/chainFns.ts +4 -0
  110. package/src/utils/decorators.ts +1 -1
  111. package/src/utils/errorDiagnostics.ts +6 -2
  112. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  113. package/src/utils/index.ts +34 -2
  114. package/src/utils/mapUtils.ts +3 -7
  115. package/src/utils/setIfDifferent.ts +3 -2
  116. package/src/utils/setLike.ts +134 -0
  117. package/src/wrappers/ArraySet.ts +5 -9
  118. package/src/wrappers/ObjectMap.ts +15 -11
  119. package/src/wrappers/asMap.ts +30 -18
  120. package/src/wrappers/asSet.ts +36 -12
  121. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  122. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -29,21 +29,23 @@ export function deepEquals(a: any, b: any): boolean {
29
29
  return true
30
30
  }
31
31
 
32
- // use snapshots to compare if possible
33
- // since snapshots use structural sharing it is more likely
34
- // to speed up comparisons
35
- if (isTreeNode(a)) {
36
- a = getSnapshot(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 =
@@ -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`)
@@ -7,7 +7,6 @@ import { createContext } from "../context"
7
7
  import { getParentToChildPath, resolvePath } from "../parent/path"
8
8
  import { applyPatches } from "../patch/applyPatches"
9
9
  import { onPatches } from "../patch/emitPatch"
10
- import type { Patch } from "../patch/Patch"
11
10
  import { type PatchRecorder, patchRecorder } from "../patch/patchRecorder"
12
11
  import {
13
12
  fastIsRootStoreNoAtom,
@@ -78,11 +77,6 @@ export class SandboxManager {
78
77
  */
79
78
  private allowWrite: ReadonlyMiddlewareReturn["allowWrite"]
80
79
 
81
- /**
82
- * Whether changes made in the sandbox are currently being committed to the original subtree.
83
- */
84
- private isCommitting = false
85
-
86
80
  private readonly subtreeRoot: object
87
81
 
88
82
  /**
@@ -127,11 +121,9 @@ export class SandboxManager {
127
121
  if (this.withSandboxPatchRecorder) {
128
122
  throw failure("original subtree must not change while 'withSandbox' executes")
129
123
  }
130
- if (!this.isCommitting) {
131
- this.allowWrite(() => {
132
- applyPatches(this.subtreeRootClone, patches)
133
- })
134
- }
124
+ this.allowWrite(() => {
125
+ applyPatches(this.subtreeRootClone, patches)
126
+ })
135
127
  })
136
128
 
137
129
  const { allowWrite, dispose: disposeReadonlyMW } = readonlyMiddleware(this.subtreeRootClone)
@@ -232,26 +224,38 @@ export class SandboxManager {
232
224
  }
233
225
  if (commit) {
234
226
  if (!isNestedWithSandboxCall) {
235
- const patches: Patch[] = []
236
- const len = recorder.events.length
237
- for (let i = 0; i < len; i++) {
238
- patches.push(...recorder.events[i].patches)
239
- }
240
-
241
- const isCommitting = this.isCommitting
242
- this.isCommitting = true
243
- try {
244
- applyPatches(this.subtreeRoot, patches)
245
- } finally {
246
- this.isCommitting = isCommitting
247
- }
227
+ const events = recorder.events
228
+ // The clone already holds these changes, so revert them and let the regular
229
+ // patch sync mirror the original's final state back in. That way anything a
230
+ // listener changes on the original during the commit reaches the clone too,
231
+ // at a cost proportional to the changes rather than to the tree size.
232
+ // Both halves run in a single action so that observers of the sandbox never
233
+ // see the reverted intermediate state.
234
+ runInAction(() => {
235
+ this.allowWrite(() => {
236
+ for (let i = events.length - 1; i >= 0; i--) {
237
+ applyPatches(this.subtreeRootClone, events[i].inversePatches, true)
238
+ }
239
+ })
240
+ applyPatches(
241
+ this.subtreeRoot,
242
+ events.map((event) => event.patches)
243
+ )
244
+ })
248
245
  }
249
246
  } else {
250
247
  this.allowWrite(() => {
251
248
  runInAction(() => {
252
- let i = recorder.events.length
253
- while (i-- > numRecorderEvents) {
254
- applyPatches(this.subtreeRootClone, recorder.events[i].inversePatches, true)
249
+ const wasRecording = recorder.recording
250
+ recorder.recording = false
251
+ try {
252
+ let i = recorder.events.length
253
+ while (i-- > numRecorderEvents) {
254
+ applyPatches(this.subtreeRootClone, recorder.events[i].inversePatches, true)
255
+ }
256
+ recorder.events.splice(numRecorderEvents)
257
+ } finally {
258
+ recorder.recording = wasRecording
255
259
  }
256
260
  })
257
261
  })
@@ -28,10 +28,8 @@ export function setTweakedObjectUntweakers(
28
28
  * @internal
29
29
  */
30
30
  export function isTweakedObject(value: unknown, canBeDataObject: boolean): value is object {
31
- if (!canBeDataObject && hasDataObjectParent(value as object)) {
32
- return false
33
- }
34
- return treeNodeMetadata.get(value as object)?.tweaked === true
31
+ const metadata = treeNodeMetadata.get(value as object)
32
+ return metadata?.tweaked === true && (canBeDataObject || metadata.dataObjectParent === undefined)
35
33
  }
36
34
 
37
35
  /**
@@ -8,6 +8,7 @@ import { unsetInternalSnapshot } from "../snapshot/internal"
8
8
  import type { AnyStandardType, TypeToData } from "../types/schemas"
9
9
  import { typeCheck } from "../types/typeCheck"
10
10
  import { failure, inDevMode, isMap, isObject, isPrimitive, isSet } from "../utils"
11
+ import { getSafeErrorValuePreview } from "../utils/errorDiagnostics"
11
12
  import { isTweakedObject, runTweakedObjectUntweakers } from "./core"
12
13
  import { registerDefaultTweakers } from "./registerDefaultTweakers"
13
14
  import { treeNodeMetadata } from "./treeNodeMetadata"
@@ -86,16 +87,11 @@ export function registerTweaker<T>(priority: number, tweaker: Tweaker<T>): void
86
87
  }
87
88
 
88
89
  function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T {
89
- if (isPrimitive(value)) {
90
- return value
91
- }
92
-
93
90
  // already tweaked
94
91
  if (isTweakedObject(value, true)) {
95
92
  value = setParent(
96
93
  value,
97
94
  parentPath,
98
- false, // indexChangeAllowed
99
95
  false, // isDataObject
100
96
  true // cloneIfApplicable
101
97
  )
@@ -131,14 +127,19 @@ function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T
131
127
  }
132
128
 
133
129
  throw failure(
134
- `tweak can only work over models, observable objects/arrays, or primitives, but got ${value} instead`
130
+ `tweak can only work over models, observable objects/arrays, or primitives, but got ${getSafeErrorValuePreview(value)} instead`
135
131
  )
136
132
  }
137
133
 
134
+ const tweakNonPrimitive = action("tweak", internalTweak)
135
+
138
136
  /**
139
137
  * @internal
140
138
  */
141
- export const tweak = action("tweak", internalTweak)
139
+ export function tweak<T>(value: T, parentPath: ParentPath<any> | undefined): T {
140
+ // Primitives need no tree bookkeeping or MobX action boundary.
141
+ return isPrimitive(value) ? value : tweakNonPrimitive(value, parentPath)
142
+ }
142
143
 
143
144
  /**
144
145
  * @internal
@@ -169,7 +170,6 @@ export function tryUntweak(value: any): (() => void) | undefined {
169
170
  setParent(
170
171
  children[i], // value
171
172
  undefined, // parentPath
172
- false, // indexChangeAllowed
173
173
  false, // isDataObject
174
174
  // no need to clone if unsetting the parent
175
175
  false // cloneIfApplicable
@@ -12,8 +12,14 @@ import { assertCanWrite } from "../action/protection"
12
12
  import { emitArraySpliceDeepChange, emitArrayUpdateDeepChange } from "../deepChange/onDeepChange"
13
13
  import { getGlobalConfig } from "../globalConfig"
14
14
  import type { ParentPath } from "../parent/path"
15
- import { setParent } from "../parent/setParent"
16
- import { hasPatchListenersFor, InternalPatchRecorder } from "../patch/emitPatch"
15
+ import { reindexArrayChildren, setParent } from "../parent/setParent"
16
+ import {
17
+ beginPatchEmission,
18
+ emitPatches,
19
+ endPatchEmission,
20
+ hasPatchListenersFor,
21
+ InternalPatchRecorder,
22
+ } from "../patch/emitPatch"
17
23
  import type { Patch } from "../patch/Patch"
18
24
  import {
19
25
  flushInternalSnapshot,
@@ -23,11 +29,20 @@ import {
23
29
  updateInternalSnapshot,
24
30
  } from "../snapshot/internal"
25
31
  import { failure, inDevMode, isArray, isPrimitive } from "../utils"
32
+ import {
33
+ addDelayedError,
34
+ type DelayedError,
35
+ throwDelayedError,
36
+ } from "../utils/forEachWithDelayedThrow"
26
37
  import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
27
38
  import { TweakerPriority } from "./TweakerPriority"
28
39
  import { markAsTweakedObject } from "./treeNodeMetadata"
29
40
  import { registerTweaker, tweak } from "./tweak"
30
- import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
41
+ import {
42
+ isTypeCheckingAfterChangeEnabled,
43
+ recordTypeCheckingBatchChange,
44
+ runTypeCheckingAfterChange,
45
+ } from "./typeChecking"
31
46
 
32
47
  /**
33
48
  * @internal
@@ -62,7 +77,6 @@ export function tweakArray<T extends any[]>(
62
77
  setParent(
63
78
  tweakedArr, // value
64
79
  parentPath,
65
- false, // indexChangeAllowed
66
80
  false, // isDataObject
67
81
  // arrays shouldn't be cloned anyway
68
82
  false // cloneIfApplicable
@@ -97,7 +111,6 @@ export function tweakArray<T extends any[]>(
97
111
  setParent(
98
112
  tweakedValue, // value
99
113
  path, // parentPath
100
- false, // indexChangeAllowed
101
114
  false, // isDataObject
102
115
  // the value is already a new value (the result of a fromSnapshot)
103
116
  false // cloneIfApplicable
@@ -140,8 +153,23 @@ function mutateSet(k: number, v: unknown, sn: unknown[]) {
140
153
  sn[k] = v
141
154
  }
142
155
 
156
+ // Conservative bound below every engine's argument-count limit for spread calls.
157
+ const maxSpreadableItems = 8192
158
+
143
159
  function mutateSplice(index: number, removedCount: number, addedItems: any[], sn: any[]) {
144
- sn.splice(index, removedCount, ...addedItems)
160
+ if (addedItems.length < maxSpreadableItems) {
161
+ sn.splice(index, removedCount, ...addedItems)
162
+ return
163
+ }
164
+
165
+ // Splice the tail in a single pass instead of spreading the added items as
166
+ // arguments, which would overflow the engine's limit. copyWithin handles overlap.
167
+ const oldLength = sn.length
168
+ const newLength = oldLength - removedCount + addedItems.length
169
+ if (newLength > oldLength) sn.length = newLength
170
+ sn.copyWithin(index + addedItems.length, index + removedCount, oldLength)
171
+ for (let i = 0; i < addedItems.length; i++) sn[index + i] = addedItems[i]
172
+ sn.length = newLength
145
173
  }
146
174
 
147
175
  const patchRecorder = new InternalPatchRecorder()
@@ -151,24 +179,12 @@ function arrayDidChange(change: IArrayDidChange) {
151
179
 
152
180
  patchRecorder.reset()
153
181
 
154
- switch (change.type) {
155
- case "splice":
156
- emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
157
- break
158
-
159
- case "update":
160
- emitArrayUpdateDeepChange(arr, arr, change.index as number, change.newValue, change.oldValue)
161
-
162
- break
163
-
164
- default:
165
- break
166
- }
167
-
168
182
  if (runningWithoutSnapshotOrPatches) {
169
183
  return
170
184
  }
171
185
 
186
+ recordTypeCheckingBatchChange(change)
187
+
172
188
  const oldSnapshot = getInternalSnapshot(arr as Array<unknown>)!.untransformed
173
189
  const shouldEmitPatches = hasPatchListenersFor(arr)
174
190
  const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
@@ -198,8 +214,53 @@ function arrayDidChange(change: IArrayDidChange) {
198
214
  runTypeCheckingAfterChange(arr, patchRecorder)
199
215
  if (mutate) {
200
216
  updateInternalSnapshot(arr, mutate)
201
- patchRecorder.emit(arr)
202
217
  }
218
+ // Listeners may mutate another node and reuse the shared recorder.
219
+ // Keep this change's patches before publishing the validated deep change.
220
+ const { patches, invPatches } = patchRecorder
221
+ patchRecorder.reset()
222
+
223
+ // Reserve chronological patch notifications, then finish deep-change delivery
224
+ // before publishing them. Reentrant edits append behind this mutation.
225
+ // Every step below runs even when an earlier one throws, so a failing patch recorder
226
+ // cannot keep the mutation from reaching the deep change and public patch listeners,
227
+ // and no failure is lost to a later one.
228
+ beginPatchEmission()
229
+ let delayedError: DelayedError
230
+ try {
231
+ emitPatches(arr, patches, invPatches)
232
+ } catch (error) {
233
+ delayedError = addDelayedError(delayedError, error)
234
+ }
235
+ try {
236
+ switch (change.type) {
237
+ case "splice":
238
+ emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
239
+ break
240
+
241
+ case "update":
242
+ emitArrayUpdateDeepChange(
243
+ arr,
244
+ arr,
245
+ change.index as number,
246
+ change.newValue,
247
+ change.oldValue
248
+ )
249
+
250
+ break
251
+
252
+ default:
253
+ break
254
+ }
255
+ } catch (error) {
256
+ delayedError = addDelayedError(delayedError, error)
257
+ }
258
+ try {
259
+ endPatchEmission()
260
+ } catch (error) {
261
+ delayedError = addDelayedError(delayedError, error)
262
+ }
263
+ throwDelayedError(delayedError)
203
264
  }
204
265
 
205
266
  const undefinedInsideArrayErrorMsg =
@@ -319,10 +380,10 @@ function arrayDidChangeSplice(
319
380
  }
320
381
 
321
382
  if (patches && readdPatches && readdPatches.length > 0) {
322
- patches.push(...readdPatches)
383
+ for (const patch of readdPatches) patches.push(patch)
323
384
  }
324
385
  if (invPatches && readdInvPatches && readdInvPatches.length > 0) {
325
- invPatches.push(...readdInvPatches)
386
+ for (const patch of readdInvPatches) invPatches.push(patch)
326
387
  }
327
388
  // We need to reverse once since inverse patches are applied in reverse.
328
389
  // Repeated unshift calls here would make a large splice quadratic.
@@ -482,20 +543,8 @@ function interceptArrayMutationSplice(change: IArrayWillSplice) {
482
543
  const oldNextIndex = change.index + change.removedCount
483
544
  const newNextIndex = change.index + change.added.length
484
545
 
485
- if (oldNextIndex !== newNextIndex) {
486
- for (let i = oldNextIndex, j = newNextIndex; i < change.object.length; i++, j++) {
487
- setParent(
488
- change.object[i], // value
489
- {
490
- parent: change.object,
491
- path: j,
492
- }, // parentPath
493
- true, // indexChangeAllowed
494
- false, // isDataObject
495
- // just re-indexing
496
- false // cloneIfApplicable
497
- )
498
- }
546
+ if (oldNextIndex !== newNextIndex && oldNextIndex < change.object.length) {
547
+ reindexArrayChildren(change.object, oldNextIndex, newNextIndex)
499
548
  }
500
549
  }
501
550
 
@@ -17,7 +17,6 @@ export function tweakFrozen<T extends Frozen<any>>(
17
17
  setParent(
18
18
  frozenObj, // value
19
19
  parentPath,
20
- false, // indexChangeAllowed
21
20
  false, // isDataObject
22
21
  // a frozen is not a value-type
23
22
  false // cloneIfApplicable
@@ -13,7 +13,6 @@ export function tweakModel<T extends object>(value: T, parentPath: ParentPath<an
13
13
  setParent(
14
14
  value,
15
15
  parentPath,
16
- false, // indexChangeAllowed
17
16
  false, // isDataObject
18
17
  true // cloneIfApplicable
19
18
  )
@@ -19,7 +19,13 @@ import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../model
19
19
  import { dataToModelNode } from "../parent/core"
20
20
  import type { ParentPath } from "../parent/path"
21
21
  import { setParent } from "../parent/setParent"
22
- import { hasPatchListenersFor, InternalPatchRecorder } from "../patch/emitPatch"
22
+ import {
23
+ beginPatchEmission,
24
+ emitPatches,
25
+ endPatchEmission,
26
+ hasPatchListenersFor,
27
+ InternalPatchRecorder,
28
+ } from "../patch/emitPatch"
23
29
  import {
24
30
  flushInternalSnapshot,
25
31
  freezeInternalSnapshot,
@@ -29,13 +35,23 @@ import {
29
35
  updateInternalSnapshot,
30
36
  } from "../snapshot/internal"
31
37
  import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
38
+ import { withoutCachedTypeChecking } from "../types/createCachedTypeCheck"
32
39
  import { failure, isPlainObject, isPrimitive, setProtoProp } from "../utils"
40
+ import {
41
+ addDelayedError,
42
+ type DelayedError,
43
+ throwDelayedError,
44
+ } from "../utils/forEachWithDelayedThrow"
33
45
  import { setIfDifferent } from "../utils/setIfDifferent"
34
46
  import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
35
47
  import { TweakerPriority } from "./TweakerPriority"
36
48
  import { markAsTweakedObject } from "./treeNodeMetadata"
37
49
  import { registerTweaker, tweak } from "./tweak"
38
- import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
50
+ import {
51
+ isTypeCheckingAfterChangeEnabled,
52
+ recordTypeCheckingBatchChange,
53
+ runTypeCheckingAfterChange,
54
+ } from "./typeChecking"
39
55
 
40
56
  /**
41
57
  * @internal
@@ -59,20 +75,19 @@ export function tweakPlainObject<T extends Record<string, any>>(
59
75
  setParent(
60
76
  tweakedObj, // value
61
77
  parentPath,
62
- false, // indexChangeAllowed
63
78
  isDataObject,
64
79
  // an object shouldn't be cloned
65
80
  false // cloneIfApplicable
66
81
  )
67
82
 
68
83
  const initialDataSnapshot =
69
- isDataObject && snapshotModelType ? takeModelInitialDataSnapshot(tweakedObj) : undefined
70
- const reuseInitialDataSnapshot =
71
- !!initialDataSnapshot?.reusable && initialDataSnapshot.allValuesPrimitive
72
- const untransformedSn: any = reuseInitialDataSnapshot ? initialDataSnapshot.snapshot : {}
84
+ isDataObject && snapshotModelType !== undefined
85
+ ? takeModelInitialDataSnapshot(tweakedObj)
86
+ : undefined
87
+ const untransformedSn: any = initialDataSnapshot ?? {}
73
88
 
74
89
  // substitute initial values by tweaked values
75
- const originalObjKeys = reuseInitialDataSnapshot ? [] : Object.keys(originalObj)
90
+ const originalObjKeys = initialDataSnapshot ? [] : Object.keys(originalObj)
76
91
  const originalObjKeysLen = originalObjKeys.length
77
92
  for (let i = 0; i < originalObjKeysLen; i++) {
78
93
  const k = originalObjKeys[i]
@@ -96,7 +111,6 @@ export function tweakPlainObject<T extends Record<string, any>>(
96
111
  setParent(
97
112
  tweakedValue, // value
98
113
  path, // parentPath
99
- false, // indexChangeAllowed
100
114
  false, // isDataObject
101
115
  // the value is already a new value (the result of a fromSnapshot)
102
116
  false // cloneIfApplicable
@@ -116,7 +130,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
116
130
  }
117
131
 
118
132
  let transformFn: SnapshotTransformFn | undefined
119
- if (snapshotModelType) {
133
+ if (snapshotModelType !== undefined) {
120
134
  untransformedSn[modelTypeKey] = snapshotModelType
121
135
 
122
136
  const modelInfo = getModelInfoForName(snapshotModelType)
@@ -171,27 +185,12 @@ function objectDidChange(change: IObjectDidChange): void {
171
185
 
172
186
  patchRecorder.reset()
173
187
 
174
- switch (change.type) {
175
- case "add":
176
- emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
177
- break
178
-
179
- case "update":
180
- emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
181
- break
182
-
183
- case "remove":
184
- emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
185
- break
186
-
187
- default:
188
- throw failure("assertion error: unsupported object change type")
189
- }
190
-
191
188
  if (runningWithoutSnapshotOrPatches) {
192
189
  return
193
190
  }
194
191
 
192
+ recordTypeCheckingBatchChange(change)
193
+
195
194
  const oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed
196
195
  const shouldEmitPatches = hasPatchListenersFor(actualNode)
197
196
  const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
@@ -219,9 +218,59 @@ function objectDidChange(change: IObjectDidChange): void {
219
218
  throw failure("assertion error: unsupported object change type")
220
219
  }
221
220
 
222
- runTypeCheckingAfterChange(obj, patchRecorder)
221
+ if (change.type !== "update" && isTypeCheckingAfterChangeEnabled()) {
222
+ // MobX notifies object observers before invalidating dependencies on missing
223
+ // keys (and removed keys in MobX 4). Cached ancestor checks may still
224
+ // describe the previous shape.
225
+ withoutCachedTypeChecking(obj, () => runTypeCheckingAfterChange(obj, patchRecorder))
226
+ } else {
227
+ runTypeCheckingAfterChange(obj, patchRecorder)
228
+ }
223
229
  updateInternalSnapshot(actualNode, mutate)
224
- patchRecorder.emit(actualNode)
230
+
231
+ // Listeners may mutate another node and reuse the shared recorder.
232
+ // Keep this change's patches before publishing the validated deep change.
233
+ const { patches, invPatches } = patchRecorder
234
+ patchRecorder.reset()
235
+
236
+ // Reserve chronological patch notifications, then finish deep-change delivery
237
+ // before publishing them. Reentrant edits append behind this mutation.
238
+ // Every step below runs even when an earlier one throws, so a failing patch recorder
239
+ // cannot keep the mutation from reaching the deep change and public patch listeners,
240
+ // and no failure is lost to a later one.
241
+ beginPatchEmission()
242
+ let delayedError: DelayedError
243
+ try {
244
+ emitPatches(actualNode, patches, invPatches)
245
+ } catch (error) {
246
+ delayedError = addDelayedError(delayedError, error)
247
+ }
248
+ try {
249
+ switch (change.type) {
250
+ case "add":
251
+ emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
252
+ break
253
+
254
+ case "update":
255
+ emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
256
+ break
257
+
258
+ case "remove":
259
+ emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
260
+ break
261
+
262
+ default:
263
+ throw failure("assertion error: unsupported object change type")
264
+ }
265
+ } catch (error) {
266
+ delayedError = addDelayedError(delayedError, error)
267
+ }
268
+ try {
269
+ endPatchEmission()
270
+ } catch (error) {
271
+ delayedError = addDelayedError(delayedError, error)
272
+ }
273
+ throwDelayedError(delayedError)
225
274
  }
226
275
 
227
276
  function objectDidChangeRemove(