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
@@ -1,12 +1,15 @@
1
1
  import type { ActionMiddlewareDisposer } from "../action/middleware"
2
2
  import { modelAction } from "../action/modelAction"
3
+ import { type Frozen, FrozenCheckMode, frozen, isFrozenSnapshot, toFrozenSnapshot } from "../frozen"
3
4
  import { Model } from "../model/Model"
4
5
  import { model } from "../modelShared/modelDecorator"
5
6
  import { fastGetRootPath } from "../parent/path"
6
7
  import type { Path } from "../parent/pathTypes"
7
8
  import { applyPatches, type Patch, type PatchRecorder, patchRecorder } from "../patch"
9
+ import type { FrozenData } from "../snapshot"
8
10
  import { assertTweakedObject } from "../tweaker/core"
9
11
  import { typesArray } from "../types/arrayBased/typesArray"
12
+ import { typesFrozen } from "../types/objectBased/typesObject"
10
13
  import { tProp } from "../types/tProp"
11
14
  import { typesUnchecked } from "../types/utility/typesUnchecked"
12
15
  import {
@@ -19,6 +22,16 @@ import {
19
22
  } from "../utils"
20
23
  import { actionTrackingMiddleware, type SimpleActionContext } from "./actionTrackingMiddleware"
21
24
 
25
+ function checkMaxLevels(value: number | undefined, name: string): void {
26
+ if (
27
+ value !== undefined &&
28
+ value !== Number.POSITIVE_INFINITY &&
29
+ (!Number.isInteger(value) || value < 0)
30
+ ) {
31
+ throw failure(`${name} must be a non-negative integer or Infinity`)
32
+ }
33
+ }
34
+
22
35
  /**
23
36
  * An undo/redo event without attached state.
24
37
  */
@@ -111,16 +124,45 @@ function toSingleEvents(
111
124
  return array
112
125
  }
113
126
 
127
+ // snapshots from before events were frozen have them as plain objects
128
+ function toFrozenEventSnapshots(
129
+ events: ReadonlyArray<UndoEvent | FrozenData<UndoEvent>> | undefined
130
+ ): FrozenData<UndoEvent>[] | undefined {
131
+ return events?.map((e) => (isFrozenSnapshot<UndoEvent>(e) ? e : toFrozenSnapshot(e)))
132
+ }
133
+
134
+ function freezeEvent(event: UndoEvent): Frozen<UndoEvent> {
135
+ // events are only read once recorded, so they are not checked or deeply
136
+ // frozen (that would also freeze the attached states given by the user)
137
+ return frozen(event, FrozenCheckMode.Off)
138
+ }
139
+
114
140
  /**
115
141
  * Store model instance for undo/redo actions.
116
- * Do not manipulate directly, other that creating it.
142
+ * Do not manipulate directly, other than creating it.
143
+ *
144
+ * Events are stored as frozen data, since they never change once recorded.
117
145
  */
118
146
  @model(`${namespace}/UndoStore`)
119
- export class UndoStore extends Model({
120
- // TODO: add proper type checking to undo store
121
- undoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
122
- redoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
123
- }) {
147
+ export class UndoStore extends Model(
148
+ {
149
+ // TODO: add proper type checking to undo store
150
+ undoEvents: tProp(typesArray(typesFrozen(typesUnchecked<UndoEvent>())), () => []),
151
+ redoEvents: tProp(typesArray(typesFrozen(typesUnchecked<UndoEvent>())), () => []),
152
+ },
153
+ {
154
+ fromSnapshotProcessor(sn: {
155
+ undoEvents?: ReadonlyArray<UndoEvent | FrozenData<UndoEvent>>
156
+ redoEvents?: ReadonlyArray<UndoEvent | FrozenData<UndoEvent>>
157
+ }) {
158
+ return {
159
+ ...sn,
160
+ undoEvents: toFrozenEventSnapshots(sn.undoEvents),
161
+ redoEvents: toFrozenEventSnapshots(sn.redoEvents),
162
+ }
163
+ },
164
+ }
165
+ ) {
124
166
  /**
125
167
  * @ignore
126
168
  */
@@ -151,15 +193,13 @@ export class UndoStore extends Model({
151
193
  maxUndoLevels?: number
152
194
  maxRedoLevels?: number
153
195
  }) {
154
- if (maxUndoLevels !== undefined) {
155
- while (this.undoEvents.length > maxUndoLevels) {
156
- this.undoEvents.shift()
157
- }
196
+ checkMaxLevels(maxUndoLevels, "maxUndoLevels")
197
+ checkMaxLevels(maxRedoLevels, "maxRedoLevels")
198
+ if (maxUndoLevels !== undefined && this.undoEvents.length > maxUndoLevels) {
199
+ this.undoEvents.splice(0, this.undoEvents.length - maxUndoLevels)
158
200
  }
159
- if (maxRedoLevels !== undefined) {
160
- while (this.redoEvents.length > maxRedoLevels) {
161
- this.redoEvents.shift()
162
- }
201
+ if (maxRedoLevels !== undefined && this.redoEvents.length > maxRedoLevels) {
202
+ this.redoEvents.splice(0, this.redoEvents.length - maxRedoLevels)
163
203
  }
164
204
  }
165
205
 
@@ -193,7 +233,7 @@ export class UndoStore extends Model({
193
233
  @modelAction
194
234
  _addUndo({ event, maxUndoLevels }: { event: UndoEvent; maxUndoLevels: number | undefined }) {
195
235
  withoutUndo(() => {
196
- this.undoEvents.push(event)
236
+ this.undoEvents.push(freezeEvent(event))
197
237
  // once an undo event is added redo queue is no longer valid
198
238
  this.redoEvents.length = 0
199
239
  this.enforceMaxLevels({ maxUndoLevels })
@@ -261,10 +301,16 @@ export class UndoStore extends Model({
261
301
  running = true
262
302
  },
263
303
  end: () => {
304
+ if (ended) {
305
+ throw failure("cannot end a group when it is already ended")
306
+ }
264
307
  if (running) {
265
308
  api.pause()
266
309
  }
267
310
  ended = true
311
+ if (group.events.length === 0) {
312
+ return
313
+ }
268
314
  if (parentGroup) {
269
315
  this._addUndoToParentGroup(parentGroup, group)
270
316
  } else {
@@ -309,7 +355,7 @@ export class UndoManager {
309
355
  */
310
356
  @mobxComputed
311
357
  get undoQueue(): ReadonlyArray<UndoEvent> {
312
- return this.store.undoEvents
358
+ return this.store.undoEvents.map((e) => e.data)
313
359
  }
314
360
 
315
361
  /**
@@ -318,7 +364,7 @@ export class UndoManager {
318
364
  */
319
365
  @mobxComputed
320
366
  get redoQueue(): ReadonlyArray<UndoEvent> {
321
- return this.store.redoEvents
367
+ return this.store.redoEvents.map((e) => e.data)
322
368
  }
323
369
 
324
370
  /**
@@ -326,7 +372,7 @@ export class UndoManager {
326
372
  */
327
373
  @mobxComputed
328
374
  get undoLevels() {
329
- return this.undoQueue.length
375
+ return this.store.undoEvents.length
330
376
  }
331
377
 
332
378
  /**
@@ -350,7 +396,7 @@ export class UndoManager {
350
396
  */
351
397
  @mobxComputed
352
398
  get redoLevels() {
353
- return this.redoQueue.length
399
+ return this.store.redoEvents.length
354
400
  }
355
401
 
356
402
  /**
@@ -378,7 +424,8 @@ export class UndoManager {
378
424
  if (!this.canUndo) {
379
425
  throw failure("nothing to undo")
380
426
  }
381
- const event = this.undoQueue[this.undoQueue.length - 1]
427
+ const events = this.store.undoEvents
428
+ const event = events[events.length - 1].data
382
429
 
383
430
  withoutUndo(() => {
384
431
  toSingleEvents(event)
@@ -388,7 +435,7 @@ export class UndoManager {
388
435
  })
389
436
 
390
437
  // restore the attached state before the operation was made
391
- if (event.attachedState?.beforeEvent) {
438
+ if (event.attachedState) {
392
439
  this.options?.attachedState?.restore(event.attachedState.beforeEvent)
393
440
  }
394
441
  })
@@ -405,7 +452,8 @@ export class UndoManager {
405
452
  if (!this.canRedo) {
406
453
  throw failure("nothing to redo")
407
454
  }
408
- const event = this.redoQueue[this.redoQueue.length - 1]
455
+ const events = this.store.redoEvents
456
+ const event = events[events.length - 1].data
409
457
 
410
458
  withoutUndo(() => {
411
459
  toSingleEvents(event).forEach((e) => {
@@ -413,7 +461,7 @@ export class UndoManager {
413
461
  })
414
462
 
415
463
  // restore the attached state after the operation was made
416
- if (event.attachedState?.afterEvent) {
464
+ if (event.attachedState) {
417
465
  this.options?.attachedState?.restore(event.attachedState.afterEvent)
418
466
  }
419
467
  })
@@ -586,14 +634,14 @@ export class UndoManager {
586
634
  function next(ret: any): void {
587
635
  if (ret && typeof ret.then === "function") {
588
636
  // an async iterator
589
- ret.then(next, reject)
637
+ Promise.resolve(ret).then(next, reject).catch(reject)
590
638
  } else if (ret.done) {
591
639
  // done
592
640
  group.end()
593
641
  resolve(ret.value)
594
642
  } else {
595
643
  // continue
596
- Promise.resolve(ret.value).then(onFulfilled, onRejected)
644
+ Promise.resolve(ret.value).then(onFulfilled, onRejected).catch(reject)
597
645
  }
598
646
  }
599
647
 
@@ -675,6 +723,8 @@ export function undoMiddleware<S>(
675
723
  options?: UndoMiddlewareOptions<S>
676
724
  ): UndoManager {
677
725
  assertTweakedObject(subtreeRoot, "subtreeRoot")
726
+ checkMaxLevels(options?.maxUndoLevels, "maxUndoLevels")
727
+ checkMaxLevels(options?.maxRedoLevels, "maxRedoLevels")
678
728
 
679
729
  let manager: UndoManager
680
730
 
@@ -691,6 +741,7 @@ export function undoMiddleware<S>(
691
741
  function initPatchRecorder(ctx: SimpleActionContext) {
692
742
  const group = manager.store._currentGroup
693
743
 
744
+ const attachedStateBeforeEvent = options?.attachedState?.save()
694
745
  const patchRecorderData: PatchRecorderData = {
695
746
  recorder: patchRecorder(subtreeRoot, {
696
747
  recording: false,
@@ -702,7 +753,7 @@ export function undoMiddleware<S>(
702
753
  undoRootContext: ctx,
703
754
  group,
704
755
 
705
- attachedStateBeforeEvent: options?.attachedState?.save(),
756
+ attachedStateBeforeEvent,
706
757
  }
707
758
 
708
759
  ctx.rootContext.data[patchRecorderSymbol] = patchRecorderData
@@ -733,42 +784,44 @@ export function undoMiddleware<S>(
733
784
  if (patchRecorderData && patchRecorderData.undoRootContext === ctx) {
734
785
  const patchRecorder = patchRecorderData.recorder
735
786
 
736
- if (patchRecorder.events.length > 0) {
737
- const patches: Patch[] = []
738
- const inversePatches: Patch[] = []
739
-
740
- for (const event of patchRecorder.events) {
741
- patches.push(...event.patches)
742
- inversePatches.push(...event.inversePatches)
743
- }
744
-
745
- const event = {
746
- type: UndoEventType.Single,
747
- targetPath: fastGetRootPath(ctx.target, false).path,
748
- actionName: ctx.actionName,
749
- patches,
750
- inversePatches,
751
- } as const
752
-
753
- const parentGroup = patchRecorderData.group
754
-
755
- if (parentGroup) {
756
- manager.store._addUndoToParentGroup(parentGroup, event)
757
- } else {
758
- manager.store._addUndo({
759
- event: {
760
- ...event,
761
- attachedState: {
762
- beforeEvent: patchRecorderData.attachedStateBeforeEvent,
763
- afterEvent: options?.attachedState?.save(),
787
+ try {
788
+ if (patchRecorder.events.length > 0) {
789
+ const patches: Patch[] = []
790
+ const inversePatches: Patch[] = []
791
+
792
+ for (const event of patchRecorder.events) {
793
+ for (const patch of event.patches) patches.push(patch)
794
+ for (const patch of event.inversePatches) inversePatches.push(patch)
795
+ }
796
+
797
+ const event = {
798
+ type: UndoEventType.Single,
799
+ targetPath: fastGetRootPath(ctx.target, false).path,
800
+ actionName: ctx.actionName,
801
+ patches,
802
+ inversePatches,
803
+ } as const
804
+
805
+ const parentGroup = patchRecorderData.group
806
+
807
+ if (parentGroup) {
808
+ manager.store._addUndoToParentGroup(parentGroup, event)
809
+ } else {
810
+ manager.store._addUndo({
811
+ event: {
812
+ ...event,
813
+ attachedState: {
814
+ beforeEvent: patchRecorderData.attachedStateBeforeEvent,
815
+ afterEvent: options?.attachedState?.save(),
816
+ },
764
817
  },
765
- },
766
- maxUndoLevels: options?.maxUndoLevels,
767
- })
818
+ maxUndoLevels: options?.maxUndoLevels,
819
+ })
820
+ }
768
821
  }
822
+ } finally {
823
+ patchRecorder.dispose()
769
824
  }
770
-
771
- patchRecorder.dispose()
772
825
  }
773
826
  },
774
827
  })
@@ -1,5 +1,6 @@
1
1
  import { modelIdKey } from "../model/metadata"
2
2
  import { isModel } from "../model/utils"
3
+ import { modelToDataNode } from "../parent/core"
3
4
  import type { RootPath } from "../parent/path"
4
5
  import type { Path } from "../parent/pathTypes"
5
6
 
@@ -26,7 +27,7 @@ export function pathToTargetPathIds(root: any, path: Path): (string | null)[] {
26
27
  let current = root // we don't care about the root ID
27
28
 
28
29
  for (let i = 0; i < path.length; i++) {
29
- current = current[path[i]]
30
+ current = modelToDataNode(current)[path[i]]
30
31
  const targetObjId = isModel(current) ? (current[modelIdKey] ?? null) : null
31
32
  targetPathIds.push(targetObjId)
32
33
  }
@@ -5,7 +5,12 @@ import { isDataModelClass } from "../dataModel/utils"
5
5
  import { isModelClass } from "../model/utils"
6
6
  import { isTreeNode } from "../tweaker/core"
7
7
  import { tweak } from "../tweaker/tweak"
8
- import { addLateInitializationFunction, failure, runBeforeOnInitSymbol } from "../utils"
8
+ import {
9
+ addLateInitializationFunction,
10
+ failure,
11
+ isEqualOrBothNaN,
12
+ runBeforeOnInitSymbol,
13
+ } from "../utils"
9
14
  import { checkDecoratorContext, copyFunctionMetadata } from "../utils/decorators"
10
15
  import { getOrCreate } from "../utils/mapUtils"
11
16
 
@@ -54,14 +59,20 @@ export function computedTree(...args: any[]): any {
54
59
  const oldValue = entry.value
55
60
  const newValue = entry.computed.get()
56
61
 
57
- if (oldValue === newValue) {
62
+ if (isEqualOrBothNaN(oldValue, newValue)) {
58
63
  return entry.tweakedValue
59
64
  }
60
65
 
61
66
  const oldTweakedValue = entry.tweakedValue
62
67
  tweak(oldTweakedValue, undefined)
63
68
 
64
- const tweakedValue = tweakComputedTreeNode(newValue, this, propertyKey)
69
+ let tweakedValue: unknown
70
+ try {
71
+ tweakedValue = tweakComputedTreeNode(newValue, this, propertyKey)
72
+ } catch (error) {
73
+ entry.tweakedValue = tweakComputedTreeNode(oldTweakedValue, this, propertyKey)
74
+ throw error
75
+ }
65
76
  entry.value = newValue
66
77
  entry.tweakedValue = tweakedValue
67
78
  return tweakedValue
@@ -120,56 +120,28 @@ class ContextClass<T> implements Context<T> {
120
120
  this.nodeAtom.get(node)?.reportChanged()
121
121
  }
122
122
 
123
- private fastGet(node: object, useAtom: boolean): T {
124
- if (useAtom) {
125
- this.reportNodeAtomObserved(node)
123
+ get(node: object): T {
124
+ const provider = this.getProviderNode(node)
125
+ if (provider) {
126
+ return resolveContextValue(this.nodeContextValue.get(provider)!)
126
127
  }
127
128
 
128
- const obsForNode = this.nodeContextValue.get(node)
129
- if (obsForNode) {
130
- return resolveContextValue(obsForNode)
131
- }
132
-
133
- const parent = fastGetParent(node, useAtom)
134
- if (!parent) {
135
- const overrideValue = this.overrideContextValue.get()
136
- if (overrideValue) {
137
- return resolveContextValue(overrideValue)
138
- }
139
- return this.getDefault()
140
- }
141
-
142
- return this.fastGet(parent, useAtom)
143
- }
144
-
145
- get(node: object) {
146
- assertTweakedObject(node, "node")
147
-
148
- return this.fastGet(node, true)
149
- }
150
-
151
- private fastGetProviderNode(node: object, useAtom: boolean): object | undefined {
152
- if (useAtom) {
153
- this.reportNodeAtomObserved(node)
154
- }
155
-
156
- const obsForNode = this.nodeContextValue.get(node)
157
- if (obsForNode) {
158
- return node
159
- }
160
-
161
- const parent = fastGetParent(node, useAtom)
162
- if (!parent) {
163
- return undefined
164
- }
165
-
166
- return this.fastGetProviderNode(parent, useAtom)
129
+ const overrideValue = this.overrideContextValue.get()
130
+ return overrideValue ? resolveContextValue(overrideValue) : this.getDefault()
167
131
  }
168
132
 
169
133
  getProviderNode(node: object): object | undefined {
170
134
  assertTweakedObject(node, "node")
171
135
 
172
- return this.fastGetProviderNode(node, true)
136
+ let current: object | undefined = node
137
+ while (current) {
138
+ this.reportNodeAtomObserved(current)
139
+ if (this.nodeContextValue.has(current)) {
140
+ return current
141
+ }
142
+ current = fastGetParent(current, true)
143
+ }
144
+ return undefined
173
145
  }
174
146
 
175
147
  getDefault(): T {
@@ -227,7 +199,7 @@ class ContextClass<T> implements Context<T> {
227
199
 
228
200
  try {
229
201
  const ret = fn()
230
- if (isTweakedObject(ret, true)) {
202
+ if (isTweakedObject(ret, false)) {
231
203
  this.set(ret, value)
232
204
  }
233
205
  return ret
@@ -247,7 +219,7 @@ class ContextClass<T> implements Context<T> {
247
219
 
248
220
  try {
249
221
  const ret = fn()
250
- if (isTweakedObject(ret, true)) {
222
+ if (isTweakedObject(ret, false)) {
251
223
  this._setComputed(ret, computedValueFn)
252
224
  }
253
225
  return ret
@@ -1,4 +1,4 @@
1
- import { type ModelClass, propsTypeSymbol } from "../modelShared/BaseModelShared"
1
+ import { propsTypeSymbol } from "../modelShared/BaseModelShared"
2
2
  import { modelInfoByClass } from "../modelShared/modelInfo"
3
3
  import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
4
4
  import {
@@ -15,14 +15,13 @@ import { toTreeNode } from "../tweaker/tweak"
15
15
  import { typesDataModelData } from "../types/objectBased/typesDataModelData"
16
16
  import type { TypeCheckError } from "../types/TypeCheckError"
17
17
  import { typeCheck } from "../types/typeCheck"
18
- import { clonePlainObject, failure, isObject } from "../utils"
18
+ import { clonePlainObject, failure, hasOwnProp, isObject, setProtoProp } from "../utils"
19
19
  import { getOrCreate } from "../utils/mapUtils"
20
20
  import type { DataModelConstructorOptions } from "./DataModelConstructorOptions"
21
+ import { dataModelInstanceCache } from "./dataModelInstanceCache"
21
22
  import { getDataModelMetadata } from "./getDataModelMetadata"
22
23
  import { internalNewDataModel } from "./newDataModel"
23
24
 
24
- const dataModelInstanceCache = new WeakMap<ModelClass<AnyDataModel>, WeakMap<any, AnyDataModel>>()
25
-
26
25
  /**
27
26
  * Base abstract class for data models. Use `DataModel` instead when extending.
28
27
  *
@@ -101,7 +100,8 @@ export abstract class BaseDataModel<TProps extends ModelProps> {
101
100
  const k = modelPropsKeys[i]
102
101
  const propData = modelProps[k]
103
102
 
104
- let newValue = initialData[k]
103
+ const hasInitialValue = hasOwnProp(initialData, k)
104
+ let newValue = hasInitialValue ? initialData[k] : undefined
105
105
  let changed = false
106
106
 
107
107
  // apply untransform (if any)
@@ -117,11 +117,15 @@ export abstract class BaseDataModel<TProps extends ModelProps> {
117
117
  if (defaultValue !== noDefaultValue) {
118
118
  changed = true
119
119
  newValue = defaultValue
120
+ } else if (!hasInitialValue) {
121
+ // for mobx4, we need to set up properties even if they are undefined
122
+ changed = true
120
123
  }
121
124
  }
122
125
 
123
126
  if (changed) {
124
- initialData[k] = newValue
127
+ if (k === "__proto__") setProtoProp(initialData, newValue)
128
+ else initialData[k] = newValue
125
129
  }
126
130
  }
127
131
 
@@ -144,24 +148,24 @@ export abstract class BaseDataModel<TProps extends ModelProps> {
144
148
 
145
149
  Object.setPrototypeOf(this, modelClass.prototype)
146
150
 
147
- internalNewDataModel(this, tweakedData as any, {
148
- modelClass,
149
- })
151
+ try {
152
+ internalNewDataModel(this, tweakedData as any, {
153
+ modelClass,
154
+ })
155
+ } catch (error) {
156
+ instancesForModelClass.delete(tweakedData)
157
+ throw error
158
+ }
150
159
  }
151
160
 
152
161
  toString(options?: { withData?: boolean }) {
153
- const finalOptions = {
154
- withData: true,
155
- ...options,
156
- }
162
+ const { withData = true } = options ?? {}
157
163
 
158
164
  const modelInfo = modelInfoByClass.get(this.constructor as any)
159
165
 
160
166
  const firstPart = `${this.constructor.name}#${modelInfo!.name}`
161
167
 
162
- return finalOptions.withData
163
- ? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]`
164
- : `[${firstPart}]`
168
+ return withData ? `[${firstPart} ${JSON.stringify(getSnapshot(this.$))}]` : `[${firstPart}]`
165
169
  }
166
170
  }
167
171
 
@@ -0,0 +1,8 @@
1
+ import type { ModelClass } from "../modelShared/BaseModelShared"
2
+ import type { AnyDataModel } from "./BaseDataModel"
3
+
4
+ /** @internal */
5
+ export const dataModelInstanceCache = new WeakMap<
6
+ ModelClass<AnyDataModel>,
7
+ WeakMap<object, AnyDataModel>
8
+ >()