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.
Files changed (141) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/dist/mobx-keystone.esm.js +3627 -1540
  3. package/dist/mobx-keystone.esm.mjs +3627 -1540
  4. package/dist/mobx-keystone.umd.js +3638 -1543
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
  8. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  9. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/detach.d.ts +1 -1
  12. package/dist/types/parent/findChildren.d.ts +1 -1
  13. package/dist/types/parent/treeOrder.d.ts +1 -0
  14. package/dist/types/patch/applyPatches.d.ts +3 -0
  15. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  16. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  17. package/dist/types/ref/nodesById.d.ts +1 -0
  18. package/dist/types/ref/refIdIndex.d.ts +1 -0
  19. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  20. package/dist/types/snapshot/applySnapshot.d.ts +1 -0
  21. package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
  22. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  23. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  24. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  25. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  26. package/dist/types/types/TypeCheckError.d.ts +1 -0
  27. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  28. package/dist/types/utils/ModelPool.d.ts +7 -1
  29. package/dist/types/utils/chunks.d.ts +1 -0
  30. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  31. package/dist/types/utils/index.d.ts +13 -0
  32. package/dist/types/utils/mapUtils.d.ts +2 -2
  33. package/dist/types/utils/setLike.d.ts +1 -0
  34. package/package.json +2 -2
  35. package/src/action/applyAction.ts +1 -1
  36. package/src/action/middleware.ts +17 -36
  37. package/src/action/modelFlow.ts +4 -5
  38. package/src/action/mutationBatch.ts +10 -10
  39. package/src/action/pendingActions.ts +3 -4
  40. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  41. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  42. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  43. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  44. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  45. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  46. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  47. package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
  48. package/src/actionMiddlewares/undoMiddleware.ts +112 -59
  49. package/src/actionMiddlewares/utils.ts +2 -1
  50. package/src/computedTree/computedTree.ts +14 -3
  51. package/src/context/context.ts +17 -45
  52. package/src/dataModel/BaseDataModel.ts +20 -16
  53. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  54. package/src/deepChange/onDeepChange.ts +179 -24
  55. package/src/frozen/Frozen.ts +50 -33
  56. package/src/model/BaseModel.ts +10 -13
  57. package/src/model/newModel.ts +21 -24
  58. package/src/model/utils.ts +2 -2
  59. package/src/modelShared/modelClassInitializer.ts +3 -2
  60. package/src/modelShared/modelDecorator.ts +39 -39
  61. package/src/modelShared/modelInfo.ts +1 -1
  62. package/src/modelShared/prop.ts +6 -9
  63. package/src/modelShared/sharedInternalModel.ts +16 -3
  64. package/src/parent/arrayPaths.ts +242 -0
  65. package/src/parent/coreObjectChildren.ts +300 -48
  66. package/src/parent/detach.ts +1 -1
  67. package/src/parent/findChildren.ts +4 -7
  68. package/src/parent/onChildAttachedTo.ts +157 -55
  69. package/src/parent/path.ts +15 -5
  70. package/src/parent/setParent.ts +34 -15
  71. package/src/parent/treeOrder.ts +53 -0
  72. package/src/parent/walkTree.ts +12 -101
  73. package/src/patch/applyPatches.ts +74 -34
  74. package/src/patch/emitPatch.ts +142 -26
  75. package/src/patch/jsonPatch.ts +5 -0
  76. package/src/patch/patchRecorder.ts +18 -14
  77. package/src/redux/connectReduxDevTools.ts +24 -18
  78. package/src/redux/redux.ts +5 -21
  79. package/src/ref/core.ts +81 -29
  80. package/src/ref/modelRefIdIndex.ts +131 -0
  81. package/src/ref/nodesById.ts +60 -0
  82. package/src/ref/refIdIndex.ts +209 -0
  83. package/src/rootStore/attachDetach.ts +27 -16
  84. package/src/rootStore/rootStore.ts +10 -6
  85. package/src/snapshot/SnapshotOf.ts +2 -2
  86. package/src/snapshot/applySnapshot.ts +27 -19
  87. package/src/snapshot/clone.ts +1 -2
  88. package/src/snapshot/cloneTreeValue.ts +1 -2
  89. package/src/snapshot/fromModelSnapshot.ts +6 -2
  90. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  91. package/src/snapshot/fromSnapshot.ts +2 -6
  92. package/src/snapshot/internal.ts +89 -19
  93. package/src/snapshot/onSnapshot.ts +12 -1
  94. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  95. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  96. package/src/standardActions/arrayActions.ts +12 -2
  97. package/src/standardActions/objectActions.ts +3 -2
  98. package/src/transforms/ImmutableDate.ts +4 -0
  99. package/src/transforms/asMap.ts +3 -1
  100. package/src/treeUtils/deepEquals.ts +15 -13
  101. package/src/treeUtils/draft.ts +8 -0
  102. package/src/treeUtils/sandbox.ts +31 -27
  103. package/src/tweaker/changeRollback.ts +218 -0
  104. package/src/tweaker/core.ts +33 -22
  105. package/src/tweaker/treeNodeMetadata.ts +12 -2
  106. package/src/tweaker/tweak.ts +183 -19
  107. package/src/tweaker/tweakArray.ts +176 -33
  108. package/src/tweaker/tweakPlainObject.ts +117 -39
  109. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  110. package/src/tweaker/typeChecking.ts +25 -15
  111. package/src/types/TypeCheckError.ts +17 -19
  112. package/src/types/arrayBased/typesArray.ts +10 -11
  113. package/src/types/createCachedTypeCheck.ts +541 -88
  114. package/src/types/objectBased/typesDataModelData.ts +4 -2
  115. package/src/types/objectBased/typesModel.ts +4 -2
  116. package/src/types/objectBased/typesObject.ts +20 -16
  117. package/src/types/objectBased/typesRecord.ts +8 -18
  118. package/src/types/objectBased/typesRef.ts +2 -2
  119. package/src/types/primitiveBased/typesEnum.ts +4 -2
  120. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  121. package/src/types/typeCheckErrorUtils.ts +1 -0
  122. package/src/types/utility/typesCodecSupport.ts +97 -56
  123. package/src/types/utility/typesMap.ts +24 -41
  124. package/src/types/utility/typesRefinement.ts +3 -2
  125. package/src/types/utility/typesSet.ts +24 -0
  126. package/src/utils/ModelPool.ts +78 -22
  127. package/src/utils/chainFns.ts +4 -0
  128. package/src/utils/chunks.ts +271 -0
  129. package/src/utils/decorators.ts +1 -1
  130. package/src/utils/errorDiagnostics.ts +6 -2
  131. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  132. package/src/utils/index.ts +71 -2
  133. package/src/utils/mapUtils.ts +3 -7
  134. package/src/utils/setIfDifferent.ts +3 -2
  135. package/src/utils/setLike.ts +134 -0
  136. package/src/wrappers/ArraySet.ts +5 -9
  137. package/src/wrappers/ObjectMap.ts +15 -11
  138. package/src/wrappers/asMap.ts +30 -18
  139. package/src/wrappers/asSet.ts +36 -12
  140. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  141. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -11,10 +11,20 @@ import {
11
11
  import { assertCanWrite } from "../action/protection"
12
12
  import { emitArraySpliceDeepChange, emitArrayUpdateDeepChange } from "../deepChange/onDeepChange"
13
13
  import { getGlobalConfig } from "../globalConfig"
14
- import type { ParentPath } from "../parent/path"
14
+ import { getModelMetadata } from "../model/getModelMetadata"
15
+ import { isModel } from "../model/utils"
16
+ import { getArrayPaths, setLazyArrayParentPath } from "../parent/arrayPaths"
17
+ import { fastGetParentPath, type ParentPath } from "../parent/path"
15
18
  import { reindexArrayChildren, setParent } from "../parent/setParent"
16
- import { hasPatchListenersFor, InternalPatchRecorder } from "../patch/emitPatch"
19
+ import {
20
+ beginPatchEmission,
21
+ emitPatches,
22
+ endPatchEmission,
23
+ hasPatchListenersFor,
24
+ InternalPatchRecorder,
25
+ } from "../patch/emitPatch"
17
26
  import type { Patch } from "../patch/Patch"
27
+ import { clone } from "../snapshot/clone"
18
28
  import {
19
29
  flushInternalSnapshot,
20
30
  freezeInternalSnapshot,
@@ -23,10 +33,19 @@ import {
23
33
  updateInternalSnapshot,
24
34
  } from "../snapshot/internal"
25
35
  import { failure, inDevMode, isArray, isPrimitive } from "../utils"
26
- import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
36
+ import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
37
+ import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
38
+ import { activateHandlers, areHandlersActive, runningWithoutSnapshotOrPatches } from "./core"
27
39
  import { TweakerPriority } from "./TweakerPriority"
28
40
  import { markAsTweakedObject } from "./treeNodeMetadata"
29
- import { registerTweaker, tweak } from "./tweak"
41
+ import {
42
+ assertCanAttach,
43
+ assertCanAttachValue,
44
+ type IsDetachedByChange,
45
+ registerTweaker,
46
+ tweak,
47
+ } from "./tweak"
48
+ import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
30
49
  import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
31
50
 
32
51
  /**
@@ -124,45 +143,57 @@ export function tweakArray<T extends any[]>(
124
143
  }
125
144
  setNewInternalSnapshot(tweakedArr, untransformedSn, undefined)
126
145
 
127
- const interceptDisposer = intercept(
128
- tweakedArr,
129
- interceptArrayMutation.bind(undefined, tweakedArr)
130
- )
131
- const observeDisposer = observe(tweakedArr, arrayDidChange)
132
- setTweakedObjectUntweakers(tweakedArr, interceptDisposer, observeDisposer)
146
+ if (activateHandlers(tweakedArr)) {
147
+ intercept(tweakedArr, interceptArrayMutation.bind(undefined, tweakedArr))
148
+ observe(tweakedArr, arrayDidChange)
149
+ }
133
150
 
134
151
  return tweakedArr as any
135
152
  }
136
153
 
137
- function mutateSet(k: number, v: unknown, sn: unknown[]) {
154
+ function mutateSet(k: number, v: unknown, sn: unknown[]): readonly unknown[] {
138
155
  sn[k] = v
156
+ return [v]
139
157
  }
140
158
 
141
- function mutateSplice(index: number, removedCount: number, addedItems: any[], sn: any[]) {
142
- sn.splice(index, removedCount, ...addedItems)
159
+ // Conservative bound below every engine's argument-count limit for spread calls.
160
+ const maxSpreadableItems = 8192
161
+
162
+ function mutateSplice(
163
+ index: number,
164
+ removedCount: number,
165
+ addedItems: any[],
166
+ sn: any[]
167
+ ): readonly unknown[] {
168
+ if (addedItems.length < maxSpreadableItems) {
169
+ sn.splice(index, removedCount, ...addedItems)
170
+ return addedItems
171
+ }
172
+
173
+ // Splice the tail in a single pass instead of spreading the added items as
174
+ // arguments, which would overflow the engine's limit. copyWithin handles overlap.
175
+ const oldLength = sn.length
176
+ const newLength = oldLength - removedCount + addedItems.length
177
+ if (newLength > oldLength) sn.length = newLength
178
+ sn.copyWithin(index + addedItems.length, index + removedCount, oldLength)
179
+ for (let i = 0; i < addedItems.length; i++) sn[index + i] = addedItems[i]
180
+ sn.length = newLength
181
+ return addedItems
143
182
  }
144
183
 
145
184
  const patchRecorder = new InternalPatchRecorder()
146
185
 
147
186
  function arrayDidChange(change: IArrayDidChange) {
187
+ if (!areHandlersActive(change.object)) {
188
+ // idle handler of an untweaked array
189
+ return
190
+ }
191
+ notifyTweakedChangeListeners(change)
192
+
148
193
  const arr = change.object
149
194
 
150
195
  patchRecorder.reset()
151
196
 
152
- switch (change.type) {
153
- case "splice":
154
- emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
155
- break
156
-
157
- case "update":
158
- emitArrayUpdateDeepChange(arr, arr, change.index as number, change.newValue, change.oldValue)
159
-
160
- break
161
-
162
- default:
163
- break
164
- }
165
-
166
197
  if (runningWithoutSnapshotOrPatches) {
167
198
  return
168
199
  }
@@ -171,7 +202,7 @@ function arrayDidChange(change: IArrayDidChange) {
171
202
  const shouldEmitPatches = hasPatchListenersFor(arr)
172
203
  const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
173
204
 
174
- let mutate: ((sn: any[]) => void) | undefined
205
+ let mutate: ((sn: any[]) => readonly unknown[]) | undefined
175
206
  switch (change.type) {
176
207
  case "splice":
177
208
  mutate = arrayDidChangeSplice(
@@ -194,10 +225,57 @@ function arrayDidChange(change: IArrayDidChange) {
194
225
  }
195
226
 
196
227
  runTypeCheckingAfterChange(arr, patchRecorder)
228
+ // (after type checking, which undoes a rejected change, but before listeners)
229
+ recordChangeForRollback(change)
197
230
  if (mutate) {
198
231
  updateInternalSnapshot(arr, mutate)
199
- patchRecorder.emit(arr)
200
232
  }
233
+ // Listeners may mutate another node and reuse the shared recorder.
234
+ // Keep this change's patches before publishing the validated deep change.
235
+ const { patches, invPatches } = patchRecorder
236
+ patchRecorder.reset()
237
+
238
+ // Reserve chronological patch notifications, then finish deep-change delivery
239
+ // before publishing them. Reentrant edits append behind this mutation.
240
+ // Every step below runs even when an earlier one throws, so a failing patch recorder
241
+ // cannot keep the mutation from reaching the deep change and public patch listeners,
242
+ // and no failure is lost to a later one.
243
+ beginPatchEmission()
244
+ let delayedError: DelayedError
245
+ try {
246
+ emitPatches(arr, patches, invPatches)
247
+ } catch (error) {
248
+ delayedError = addDelayedError(delayedError, error)
249
+ }
250
+ try {
251
+ switch (change.type) {
252
+ case "splice":
253
+ emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
254
+ break
255
+
256
+ case "update":
257
+ emitArrayUpdateDeepChange(
258
+ arr,
259
+ arr,
260
+ change.index as number,
261
+ change.newValue,
262
+ change.oldValue
263
+ )
264
+
265
+ break
266
+
267
+ default:
268
+ break
269
+ }
270
+ } catch (error) {
271
+ delayedError = addDelayedError(delayedError, error)
272
+ }
273
+ try {
274
+ endPatchEmission()
275
+ } catch (error) {
276
+ delayedError = addDelayedError(delayedError, error)
277
+ }
278
+ throwDelayedListenerError(delayedError)
201
279
  }
202
280
 
203
281
  const undefinedInsideArrayErrorMsg =
@@ -317,10 +395,10 @@ function arrayDidChangeSplice(
317
395
  }
318
396
 
319
397
  if (patches && readdPatches && readdPatches.length > 0) {
320
- patches.push(...readdPatches)
398
+ for (const patch of readdPatches) patches.push(patch)
321
399
  }
322
400
  if (invPatches && readdInvPatches && readdInvPatches.length > 0) {
323
- invPatches.push(...readdInvPatches)
401
+ for (const patch of readdInvPatches) invPatches.push(patch)
324
402
  }
325
403
  // We need to reverse once since inverse patches are applied in reverse.
326
404
  // Repeated unshift calls here would make a large splice quadratic.
@@ -410,6 +488,10 @@ function interceptArrayMutation(
410
488
  array: IObservableArray,
411
489
  change: IArrayWillChange | IArrayWillSplice
412
490
  ) {
491
+ if (!areHandlersActive(array)) {
492
+ // idle handler of an untweaked array
493
+ return change
494
+ }
413
495
  assertCanWrite()
414
496
 
415
497
  switch (change.type) {
@@ -446,9 +528,18 @@ function validateArrayMutationUpdate(change: IArrayWillChange) {
446
528
  function interceptArrayMutationUpdate(change: IArrayWillChange, array: IObservableArray) {
447
529
  // TODO: should be change.object, but mobx is bugged and doesn't send the proxy
448
530
  const oldVal = array[change.index]
531
+ if (change.newValue === oldVal) {
532
+ return
533
+ }
534
+ assertCanAttachValue(change.newValue, (node) => node === oldVal)
449
535
  tweak(oldVal, undefined) // set old prop obj parent to undefined
450
536
 
451
- change.newValue = tweak(change.newValue, { parent: array, path: change.index })
537
+ const paths = getArrayPaths(array, 0)
538
+ const entry = paths?.set(change.index, change.newValue)
539
+ change.newValue = tweak(change.newValue, entry ?? { parent: array, path: change.index })
540
+ if (entry) {
541
+ setLazyArrayParentPath(entry, change.newValue)
542
+ }
452
543
  }
453
544
 
454
545
  function validateArrayMutationSplice(change: IArrayWillSplice) {
@@ -463,12 +554,65 @@ function validateArrayMutationSplice(change: IArrayWillSplice) {
463
554
  }
464
555
  }
465
556
 
557
+ /**
558
+ * Checks the added items can be attached before the splice detaches anything,
559
+ * so a rejected splice leaves every item as it was. Value types that are added
560
+ * to the array at the index they already have get cloned here, since
561
+ * `setParent` cannot tell they are being added again (it clones them when they
562
+ * are added anywhere else).
563
+ */
564
+ function checkAddedItems(change: IArrayWillSplice) {
565
+ const array = change.object
566
+ const added = change.added
567
+ const removedEnd = change.index + change.removedCount
568
+ const isDetachedByChange: IsDetachedByChange = (_node, parentPath) =>
569
+ parentPath.parent === array &&
570
+ (parentPath.path as number) >= change.index &&
571
+ (parentPath.path as number) < removedEnd
572
+
573
+ assertCanAttach(added, isDetachedByChange, (value) => {
574
+ if (!isPrimitive(value) && isModel(value) && getModelMetadata(value).valueType) {
575
+ const parentPath = fastGetParentPath(value, false)
576
+ if (parentPath?.parent === array && !isDetachedByChange(value, parentPath)) {
577
+ return clone(value, { generateNewIds: true })
578
+ }
579
+ }
580
+ return value
581
+ })
582
+ }
583
+
466
584
  function interceptArrayMutationSplice(change: IArrayWillSplice) {
585
+ checkAddedItems(change)
586
+
587
+ const array = change.object
588
+ const oldNextIndex = change.index + change.removedCount
589
+ const paths =
590
+ change.removedCount !== change.added.length
591
+ ? getArrayPaths(array, array.length - oldNextIndex)
592
+ : getArrayPaths(array, 0)
593
+
467
594
  for (let i = 0; i < change.removedCount; i++) {
468
595
  const removedValue = change.object[change.index + i]
469
596
  tweak(removedValue, undefined)
470
597
  }
471
598
 
599
+ if (paths) {
600
+ const addedPaths = paths.createPaths(change.index, change.added)
601
+ for (let i = 0; i < change.added.length; i++) {
602
+ const addedPath = addedPaths[i]
603
+ change.added[i] = tweak(
604
+ change.added[i],
605
+ addedPath ?? { parent: array, path: change.index + i }
606
+ )
607
+ if (addedPath) {
608
+ setLazyArrayParentPath(addedPath, change.added[i])
609
+ }
610
+ }
611
+ // no reindexing: the items after the splice follow their chunks
612
+ paths.applySplice(change.index, change.removedCount, addedPaths)
613
+ return
614
+ }
615
+
472
616
  for (let i = 0; i < change.added.length; i++) {
473
617
  change.added[i] = tweak(change.added[i], {
474
618
  parent: change.object,
@@ -477,7 +621,6 @@ function interceptArrayMutationSplice(change: IArrayWillSplice) {
477
621
  }
478
622
 
479
623
  // we might also need to update the parent of the next indexes
480
- const oldNextIndex = change.index + change.removedCount
481
624
  const newNextIndex = change.index + change.added.length
482
625
 
483
626
  if (oldNextIndex !== newNextIndex && oldNextIndex < change.object.length) {
@@ -13,13 +13,21 @@ import {
13
13
  emitObjectUpdateDeepChange,
14
14
  } from "../deepChange/onDeepChange"
15
15
  import type { AnyModel } from "../model/BaseModel"
16
+ import { getModelIdPropertyName } from "../model/getModelMetadata"
16
17
  import { modelTypeKey } from "../model/metadata"
17
18
  import type { ModelClass } from "../modelShared/BaseModelShared"
18
19
  import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
19
20
  import { dataToModelNode } from "../parent/core"
21
+ import { onModelIdChanged } from "../parent/coreObjectChildren"
20
22
  import type { ParentPath } from "../parent/path"
21
23
  import { setParent } from "../parent/setParent"
22
- import { hasPatchListenersFor, InternalPatchRecorder } from "../patch/emitPatch"
24
+ import {
25
+ beginPatchEmission,
26
+ emitPatches,
27
+ endPatchEmission,
28
+ hasPatchListenersFor,
29
+ InternalPatchRecorder,
30
+ } from "../patch/emitPatch"
23
31
  import {
24
32
  flushInternalSnapshot,
25
33
  freezeInternalSnapshot,
@@ -29,12 +37,16 @@ import {
29
37
  updateInternalSnapshot,
30
38
  } from "../snapshot/internal"
31
39
  import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
32
- import { failure, isPlainObject, isPrimitive, setProtoProp } from "../utils"
40
+ import { withoutCachedTypeChecking } from "../types/createCachedTypeCheck"
41
+ import { failure, isPlainObject, isPrimitive, nodeObservableOptions, setProtoProp } from "../utils"
42
+ import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
33
43
  import { setIfDifferent } from "../utils/setIfDifferent"
34
- import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
44
+ import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
45
+ import { activateHandlers, runningWithoutSnapshotOrPatches } from "./core"
35
46
  import { TweakerPriority } from "./TweakerPriority"
36
- import { markAsTweakedObject } from "./treeNodeMetadata"
37
- import { registerTweaker, tweak } from "./tweak"
47
+ import { markAsTweakedObject, treeNodeMetadata } from "./treeNodeMetadata"
48
+ import { assertCanAttachValue, registerTweaker, tweak } from "./tweak"
49
+ import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
38
50
  import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
39
51
 
40
52
  /**
@@ -51,7 +63,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
51
63
  const originalIsObservableObject = isObservableObject(originalObj)
52
64
  const tweakedObj = originalIsObservableObject
53
65
  ? originalObj
54
- : observable.object({}, undefined, observableOptions)
66
+ : observable.object({}, undefined, nodeObservableOptions)
55
67
 
56
68
  // Mark it as tweaked before processing children. The cleanup functions are
57
69
  // installed after the MobX handlers have been registered.
@@ -65,7 +77,9 @@ export function tweakPlainObject<T extends Record<string, any>>(
65
77
  )
66
78
 
67
79
  const initialDataSnapshot =
68
- isDataObject && snapshotModelType ? takeModelInitialDataSnapshot(tweakedObj) : undefined
80
+ isDataObject && snapshotModelType !== undefined
81
+ ? takeModelInitialDataSnapshot(tweakedObj)
82
+ : undefined
69
83
  const untransformedSn: any = initialDataSnapshot ?? {}
70
84
 
71
85
  // substitute initial values by tweaked values
@@ -112,7 +126,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
112
126
  }
113
127
 
114
128
  let transformFn: SnapshotTransformFn | undefined
115
- if (snapshotModelType) {
129
+ if (snapshotModelType !== undefined) {
116
130
  untransformedSn[modelTypeKey] = snapshotModelType
117
131
 
118
132
  const modelInfo = getModelInfoForName(snapshotModelType)
@@ -132,58 +146,60 @@ export function tweakPlainObject<T extends Record<string, any>>(
132
146
  transformFn
133
147
  )
134
148
 
135
- const interceptDisposer = intercept(tweakedObj, interceptObjectMutation)
136
- const observeDisposer = observe(tweakedObj, objectDidChange)
137
- setTweakedObjectUntweakers(tweakedObj, interceptDisposer, observeDisposer)
149
+ if (activateHandlers(tweakedObj)) {
150
+ intercept(tweakedObj, interceptObjectMutation)
151
+ observe(tweakedObj, objectDidChange)
152
+ }
138
153
 
139
154
  return tweakedObj as any
140
155
  }
141
156
 
142
- const observableOptions = {
143
- deep: false,
144
- }
145
-
146
- function mutateSet(k: PropertyKey, v: unknown, sn: Record<PropertyKey, unknown>) {
157
+ function mutateSet(
158
+ k: PropertyKey,
159
+ v: unknown,
160
+ sn: Record<PropertyKey, unknown>
161
+ ): readonly unknown[] {
147
162
  if (k === "__proto__") {
148
163
  setProtoProp(sn, v)
149
164
  } else {
150
165
  sn[k] = v
151
166
  }
167
+ return [v]
152
168
  }
153
169
 
154
- function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>) {
170
+ const noValues: readonly unknown[] = []
171
+
172
+ function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>): readonly unknown[] {
155
173
  delete sn[k]
174
+ return noValues
156
175
  }
157
176
 
158
177
  const patchRecorder = new InternalPatchRecorder()
159
178
 
160
179
  function objectDidChange(change: IObjectDidChange): void {
180
+ const obj = change.object
181
+ const metadata = treeNodeMetadata.get(obj)
182
+ if (!metadata?.handlersActive) {
183
+ // idle handler of an untweaked object
184
+ return
185
+ }
161
186
  if (typeof change.name !== "string") {
162
187
  throw failure("change.name is not a string")
163
188
  }
164
189
 
165
- const obj = change.object
166
- const actualNode = dataToModelNode(obj)
167
-
168
- patchRecorder.reset()
169
-
170
- switch (change.type) {
171
- case "add":
172
- emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
173
- break
174
-
175
- case "update":
176
- emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
177
- break
190
+ notifyTweakedChangeListeners(change)
178
191
 
179
- case "remove":
180
- emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
181
- break
192
+ const actualNode = metadata.dataObjectParent ?? obj
182
193
 
183
- default:
184
- throw failure("assertion error: unsupported object change type")
194
+ if (
195
+ actualNode !== obj &&
196
+ change.name === getModelIdPropertyName(actualNode.constructor as ModelClass<AnyModel>)
197
+ ) {
198
+ onModelIdChanged(actualNode)
185
199
  }
186
200
 
201
+ patchRecorder.reset()
202
+
187
203
  if (runningWithoutSnapshotOrPatches) {
188
204
  return
189
205
  }
@@ -192,7 +208,7 @@ function objectDidChange(change: IObjectDidChange): void {
192
208
  const shouldEmitPatches = hasPatchListenersFor(actualNode)
193
209
  const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
194
210
 
195
- let mutate: ((sn: any) => void) | undefined
211
+ let mutate: ((sn: any) => readonly unknown[]) | undefined
196
212
  switch (change.type) {
197
213
  case "add":
198
214
  case "update":
@@ -215,9 +231,61 @@ function objectDidChange(change: IObjectDidChange): void {
215
231
  throw failure("assertion error: unsupported object change type")
216
232
  }
217
233
 
218
- runTypeCheckingAfterChange(obj, patchRecorder)
234
+ if (change.type !== "update" && isTypeCheckingAfterChangeEnabled()) {
235
+ // MobX notifies object observers before invalidating dependencies on missing
236
+ // keys (and removed keys in MobX 4). Cached ancestor checks may still
237
+ // describe the previous shape.
238
+ withoutCachedTypeChecking(obj, () => runTypeCheckingAfterChange(obj, patchRecorder))
239
+ } else {
240
+ runTypeCheckingAfterChange(obj, patchRecorder)
241
+ }
242
+ // (after type checking, which undoes a rejected change, but before listeners)
243
+ recordChangeForRollback(change)
219
244
  updateInternalSnapshot(actualNode, mutate)
220
- patchRecorder.emit(actualNode)
245
+
246
+ // Listeners may mutate another node and reuse the shared recorder.
247
+ // Keep this change's patches before publishing the validated deep change.
248
+ const { patches, invPatches } = patchRecorder
249
+ patchRecorder.reset()
250
+
251
+ // Reserve chronological patch notifications, then finish deep-change delivery
252
+ // before publishing them. Reentrant edits append behind this mutation.
253
+ // Every step below runs even when an earlier one throws, so a failing patch recorder
254
+ // cannot keep the mutation from reaching the deep change and public patch listeners,
255
+ // and no failure is lost to a later one.
256
+ beginPatchEmission()
257
+ let delayedError: DelayedError
258
+ try {
259
+ emitPatches(actualNode, patches, invPatches)
260
+ } catch (error) {
261
+ delayedError = addDelayedError(delayedError, error)
262
+ }
263
+ try {
264
+ switch (change.type) {
265
+ case "add":
266
+ emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
267
+ break
268
+
269
+ case "update":
270
+ emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
271
+ break
272
+
273
+ case "remove":
274
+ emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
275
+ break
276
+
277
+ default:
278
+ throw failure("assertion error: unsupported object change type")
279
+ }
280
+ } catch (error) {
281
+ delayedError = addDelayedError(delayedError, error)
282
+ }
283
+ try {
284
+ endPatchEmission()
285
+ } catch (error) {
286
+ delayedError = addDelayedError(delayedError, error)
287
+ }
288
+ throwDelayedListenerError(delayedError)
221
289
  }
222
290
 
223
291
  function objectDidChangeRemove(
@@ -284,7 +352,14 @@ function objectDidChangeAddOrUpdate(
284
352
  return mutate
285
353
  }
286
354
 
355
+ const noneDetached = () => false
356
+
287
357
  function interceptObjectMutation(change: IObjectWillChange) {
358
+ const metadata = treeNodeMetadata.get(change.object)
359
+ if (!metadata?.handlersActive) {
360
+ // idle handler of an untweaked object
361
+ return change
362
+ }
288
363
  assertCanWrite()
289
364
 
290
365
  if (typeof change.name === "symbol") {
@@ -294,10 +369,11 @@ function interceptObjectMutation(change: IObjectWillChange) {
294
369
  // This must happen before detach/reparent work below. It keeps the old value
295
370
  // available for inverse patches and folds a departing dirty child into its
296
371
  // parent snapshot while its parent path still exists.
297
- flushInternalSnapshot(dataToModelNode(change.object), false)
372
+ flushInternalSnapshot(metadata.dataObjectParent ?? change.object, false)
298
373
 
299
374
  switch (change.type) {
300
375
  case "add":
376
+ assertCanAttachValue(change.newValue, noneDetached)
301
377
  change.newValue = tweak(change.newValue, {
302
378
  parent: change.object,
303
379
  path: String(change.name),
@@ -313,6 +389,8 @@ function interceptObjectMutation(change: IObjectWillChange) {
313
389
  const oldVal = change.object[change.name]
314
390
  const newVal = change.newValue
315
391
  if (newVal !== oldVal) {
392
+ // (first, so a value that cannot be attached leaves the old one in place)
393
+ assertCanAttachValue(newVal, (node) => node === oldVal)
316
394
  tweak(oldVal, undefined)
317
395
 
318
396
  change.newValue = tweak(newVal, {
@@ -0,0 +1,61 @@
1
+ import type { IArrayDidChange, IObjectDidChange } from "mobx"
2
+
3
+ type TweakedChange = IArrayDidChange | IObjectDidChange
4
+
5
+ /**
6
+ * @internal
7
+ */
8
+ export interface TweakedChangeListener<C extends TweakedChange> {
9
+ applyChange(change: C): void
10
+ /**
11
+ * Called when the target is tweaked again after being untweaked, since the
12
+ * changes made in between were not received.
13
+ */
14
+ resync(): void
15
+ }
16
+
17
+ const changeListeners = new WeakMap<object, TweakedChangeListener<any>[]>()
18
+
19
+ /**
20
+ * Listens to the changes of a tweaked array or plain object, in order, before
21
+ * anything else reacts to them (including the type checks after the change).
22
+ * Unlike `observe`, changes made while reacting to a change (such as a
23
+ * type-check rollback) are received after it.
24
+ *
25
+ * @internal
26
+ */
27
+ export function addTweakedChangeListener<T extends object>(
28
+ target: T,
29
+ listener: TweakedChangeListener<T extends readonly unknown[] ? IArrayDidChange : IObjectDidChange>
30
+ ): void {
31
+ let listeners = changeListeners.get(target)
32
+ if (!listeners) {
33
+ listeners = []
34
+ changeListeners.set(target, listeners)
35
+ }
36
+ listeners.push(listener)
37
+ }
38
+
39
+ /**
40
+ * @internal
41
+ */
42
+ export function notifyTweakedChangeListeners(change: TweakedChange): void {
43
+ const listeners = changeListeners.get(change.object)
44
+ if (listeners) {
45
+ for (const listener of listeners) {
46
+ listener.applyChange(change)
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @internal
53
+ */
54
+ export function resyncTweakedChangeListeners(target: object): void {
55
+ const listeners = changeListeners.get(target)
56
+ if (listeners) {
57
+ for (const listener of listeners) {
58
+ listener.resync()
59
+ }
60
+ }
61
+ }