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
@@ -253,15 +253,13 @@ export function actionTrackingMiddleware(
253
253
  state: State.Finished,
254
254
  })
255
255
 
256
- if (hooks.onFinish) {
257
- ret = hooks.onFinish(simpleCtx, ret) || ret
258
- }
259
-
260
- if (parentResumed) {
261
- suspend(parentCtx!)
256
+ try {
257
+ return hooks.onFinish?.(simpleCtx, ret) || ret
258
+ } finally {
259
+ if (parentResumed) {
260
+ suspend(parentCtx!)
261
+ }
262
262
  }
263
-
264
- return ret
265
263
  }
266
264
 
267
265
  const resume = (simpleCtx: SimpleActionContext, real: boolean) => {
@@ -323,10 +321,11 @@ export function actionTrackingMiddleware(
323
321
  retObj = finish(simpleCtx, retObj)
324
322
  } else {
325
323
  try {
326
- retObj = finish(simpleCtx, { result: ActionTrackingResult.Return, value: next() })
324
+ retObj = { result: ActionTrackingResult.Return, value: next() }
327
325
  } catch (err) {
328
- retObj = finish(simpleCtx, { result: ActionTrackingResult.Throw, value: err })
326
+ retObj = { result: ActionTrackingResult.Throw, value: err }
329
327
  }
328
+ retObj = finish(simpleCtx, retObj)
330
329
  }
331
330
 
332
331
  return returnOrThrowActionTrackingReturn(retObj)
@@ -409,9 +408,8 @@ const simpleDataContextSymbol = Symbol("simpleDataContext")
409
408
  * @returns Simplified action context.
410
409
  */
411
410
  export function simplifyActionContext(ctx: ActionContext): SimpleActionContext {
412
- while (ctx.previousAsyncStepContext) {
413
- ctx = ctx.previousAsyncStepContext
414
- }
411
+ // every async step points to the step that spawned its flow (the spawn step points to itself)
412
+ ctx = ctx.spawnAsyncStepContext ?? ctx
415
413
 
416
414
  let simpleCtx = ctx.data[simpleDataContextSymbol]
417
415
  if (!simpleCtx) {
@@ -4,8 +4,10 @@ import { assertIsModel } from "../model/utils"
4
4
  import { addModelClassInitializer } from "../modelShared/modelClassInitializer"
5
5
  import { applyPatches } from "../patch"
6
6
  import { internalPatchRecorder, type PatchRecorder } from "../patch/patchRecorder"
7
- import { assertIsObject, failure } from "../utils"
7
+ import { withTypeCheckingBatch } from "../tweaker/typeChecking"
8
+ import { assertIsObject, failure, MobxKeystoneAggregateError } from "../utils"
8
9
  import { checkDecoratorContext } from "../utils/decorators"
10
+ import { pushDelayedError } from "../utils/forEachWithDelayedThrow"
9
11
  import {
10
12
  ActionTrackingResult,
11
13
  actionTrackingMiddleware,
@@ -36,11 +38,15 @@ export function transactionMiddleware<M extends AnyModel>(target: {
36
38
 
37
39
  const patchRecorderSymbol = Symbol("patchRecorder")
38
40
  function initPatchRecorder(ctx: SimpleActionContext) {
39
- ctx.rootContext.data[patchRecorderSymbol] = internalPatchRecorder(undefined, {
40
- recording: false,
41
- })
41
+ ctx.rootContext.data[patchRecorderSymbol] = {
42
+ recorder: internalPatchRecorder(undefined, { recording: false }),
43
+ activeCount: 0,
44
+ }
42
45
  }
43
- function getPatchRecorder(ctx: SimpleActionContext): PatchRecorder {
46
+ function getPatchRecorderData(ctx: SimpleActionContext): {
47
+ recorder: PatchRecorder
48
+ activeCount: number
49
+ } {
44
50
  return ctx.rootContext.data[patchRecorderSymbol]
45
51
  }
46
52
 
@@ -55,23 +61,48 @@ export function transactionMiddleware<M extends AnyModel>(target: {
55
61
  initPatchRecorder(ctx)
56
62
  }
57
63
  },
64
+ // Actions nest, so recording must only stop once every action of the transaction
65
+ // has been suspended, not as soon as the innermost one returns.
58
66
  onResume(ctx) {
59
- getPatchRecorder(ctx).recording = true
67
+ const data = getPatchRecorderData(ctx)
68
+ data.activeCount++
69
+ data.recorder.recording = true
60
70
  },
61
71
  onSuspend(ctx) {
62
- getPatchRecorder(ctx).recording = false
72
+ const data = getPatchRecorderData(ctx)
73
+ data.activeCount--
74
+ data.recorder.recording = data.activeCount > 0
63
75
  },
64
76
  onFinish(ctx, ret) {
65
77
  if (ctx === ctx.rootContext) {
66
- const patchRecorder = getPatchRecorder(ctx)
78
+ const patchRecorder = getPatchRecorderData(ctx).recorder
67
79
 
68
80
  try {
69
81
  if (ret.result === ActionTrackingResult.Throw) {
70
82
  // undo changes (backwards for inverse patches)
71
83
  const { events } = patchRecorder
72
- for (let i = events.length - 1; i >= 0; i--) {
73
- const event = events[i]
74
- applyPatches(event.target, event.inversePatches, true)
84
+ const rollbackErrors: unknown[] = []
85
+ withTypeCheckingBatch(() => {
86
+ for (let i = events.length - 1; i >= 0; i--) {
87
+ const event = events[i]
88
+ // A listener may throw after a patch has already been applied.
89
+ // Continue at patch granularity so one failure cannot skip the
90
+ // remaining restoration, including patches from the same edit.
91
+ for (let j = event.inversePatches.length - 1; j >= 0; j--) {
92
+ try {
93
+ applyPatches(event.target, [event.inversePatches[j]])
94
+ } catch (error) {
95
+ pushDelayedError(rollbackErrors, error)
96
+ }
97
+ }
98
+ }
99
+ })
100
+ if (rollbackErrors.length > 0) {
101
+ // Keep the action error first so the reason for the rollback is not lost.
102
+ throw new MobxKeystoneAggregateError(
103
+ [ret.value, ...rollbackErrors],
104
+ "transaction rollback callbacks failed"
105
+ )
75
106
  }
76
107
  }
77
108
  } finally {
@@ -19,6 +19,16 @@ import {
19
19
  } from "../utils"
20
20
  import { actionTrackingMiddleware, type SimpleActionContext } from "./actionTrackingMiddleware"
21
21
 
22
+ function checkMaxLevels(value: number | undefined, name: string): void {
23
+ if (
24
+ value !== undefined &&
25
+ value !== Number.POSITIVE_INFINITY &&
26
+ (!Number.isInteger(value) || value < 0)
27
+ ) {
28
+ throw failure(`${name} must be a non-negative integer or Infinity`)
29
+ }
30
+ }
31
+
22
32
  /**
23
33
  * An undo/redo event without attached state.
24
34
  */
@@ -99,21 +109,16 @@ export interface UndoEventGroup {
99
109
 
100
110
  function toSingleEvents(
101
111
  event: UndoEventWithoutAttachedState,
102
- reverse: boolean
103
- ): ReadonlyArray<UndoSingleEvent> {
112
+ array: UndoSingleEvent[] = []
113
+ ): UndoSingleEvent[] {
104
114
  if (event.type === UndoEventType.Single) {
105
- return [event]
115
+ array.push(event)
106
116
  } else {
107
- const array: UndoSingleEvent[] = []
108
117
  for (const e of event.events) {
109
- if (reverse) {
110
- array.unshift(...toSingleEvents(e, true))
111
- } else {
112
- array.push(...toSingleEvents(e, false))
113
- }
118
+ toSingleEvents(e, array)
114
119
  }
115
- return array
116
120
  }
121
+ return array
117
122
  }
118
123
 
119
124
  /**
@@ -156,15 +161,13 @@ export class UndoStore extends Model({
156
161
  maxUndoLevels?: number
157
162
  maxRedoLevels?: number
158
163
  }) {
159
- if (maxUndoLevels !== undefined) {
160
- while (this.undoEvents.length > maxUndoLevels) {
161
- this.undoEvents.shift()
162
- }
164
+ checkMaxLevels(maxUndoLevels, "maxUndoLevels")
165
+ checkMaxLevels(maxRedoLevels, "maxRedoLevels")
166
+ if (maxUndoLevels !== undefined && this.undoEvents.length > maxUndoLevels) {
167
+ this.undoEvents.splice(0, this.undoEvents.length - maxUndoLevels)
163
168
  }
164
- if (maxRedoLevels !== undefined) {
165
- while (this.redoEvents.length > maxRedoLevels) {
166
- this.redoEvents.shift()
167
- }
169
+ if (maxRedoLevels !== undefined && this.redoEvents.length > maxRedoLevels) {
170
+ this.redoEvents.splice(0, this.redoEvents.length - maxRedoLevels)
168
171
  }
169
172
  }
170
173
 
@@ -266,10 +269,16 @@ export class UndoStore extends Model({
266
269
  running = true
267
270
  },
268
271
  end: () => {
272
+ if (ended) {
273
+ throw failure("cannot end a group when it is already ended")
274
+ }
269
275
  if (running) {
270
276
  api.pause()
271
277
  }
272
278
  ended = true
279
+ if (group.events.length === 0) {
280
+ return
281
+ }
273
282
  if (parentGroup) {
274
283
  this._addUndoToParentGroup(parentGroup, group)
275
284
  } else {
@@ -386,12 +395,14 @@ export class UndoManager {
386
395
  const event = this.undoQueue[this.undoQueue.length - 1]
387
396
 
388
397
  withoutUndo(() => {
389
- toSingleEvents(event, true).forEach((e) => {
390
- applyPatches(this.subtreeRoot, e.inversePatches, true)
391
- })
398
+ toSingleEvents(event)
399
+ .reverse()
400
+ .forEach((e) => {
401
+ applyPatches(this.subtreeRoot, e.inversePatches, true)
402
+ })
392
403
 
393
404
  // restore the attached state before the operation was made
394
- if (event.attachedState?.beforeEvent) {
405
+ if (event.attachedState) {
395
406
  this.options?.attachedState?.restore(event.attachedState.beforeEvent)
396
407
  }
397
408
  })
@@ -411,12 +422,12 @@ export class UndoManager {
411
422
  const event = this.redoQueue[this.redoQueue.length - 1]
412
423
 
413
424
  withoutUndo(() => {
414
- toSingleEvents(event, false).forEach((e) => {
425
+ toSingleEvents(event).forEach((e) => {
415
426
  applyPatches(this.subtreeRoot, e.patches)
416
427
  })
417
428
 
418
429
  // restore the attached state after the operation was made
419
- if (event.attachedState?.afterEvent) {
430
+ if (event.attachedState) {
420
431
  this.options?.attachedState?.restore(event.attachedState.afterEvent)
421
432
  }
422
433
  })
@@ -589,14 +600,14 @@ export class UndoManager {
589
600
  function next(ret: any): void {
590
601
  if (ret && typeof ret.then === "function") {
591
602
  // an async iterator
592
- ret.then(next, reject)
603
+ Promise.resolve(ret).then(next, reject).catch(reject)
593
604
  } else if (ret.done) {
594
605
  // done
595
606
  group.end()
596
607
  resolve(ret.value)
597
608
  } else {
598
609
  // continue
599
- Promise.resolve(ret.value).then(onFulfilled, onRejected)
610
+ Promise.resolve(ret.value).then(onFulfilled, onRejected).catch(reject)
600
611
  }
601
612
  }
602
613
 
@@ -678,6 +689,8 @@ export function undoMiddleware<S>(
678
689
  options?: UndoMiddlewareOptions<S>
679
690
  ): UndoManager {
680
691
  assertTweakedObject(subtreeRoot, "subtreeRoot")
692
+ checkMaxLevels(options?.maxUndoLevels, "maxUndoLevels")
693
+ checkMaxLevels(options?.maxRedoLevels, "maxRedoLevels")
681
694
 
682
695
  let manager: UndoManager
683
696
 
@@ -694,6 +707,7 @@ export function undoMiddleware<S>(
694
707
  function initPatchRecorder(ctx: SimpleActionContext) {
695
708
  const group = manager.store._currentGroup
696
709
 
710
+ const attachedStateBeforeEvent = options?.attachedState?.save()
697
711
  const patchRecorderData: PatchRecorderData = {
698
712
  recorder: patchRecorder(subtreeRoot, {
699
713
  recording: false,
@@ -705,7 +719,7 @@ export function undoMiddleware<S>(
705
719
  undoRootContext: ctx,
706
720
  group,
707
721
 
708
- attachedStateBeforeEvent: options?.attachedState?.save(),
722
+ attachedStateBeforeEvent,
709
723
  }
710
724
 
711
725
  ctx.rootContext.data[patchRecorderSymbol] = patchRecorderData
@@ -736,42 +750,44 @@ export function undoMiddleware<S>(
736
750
  if (patchRecorderData && patchRecorderData.undoRootContext === ctx) {
737
751
  const patchRecorder = patchRecorderData.recorder
738
752
 
739
- if (patchRecorder.events.length > 0) {
740
- const patches: Patch[] = []
741
- const inversePatches: Patch[] = []
742
-
743
- for (const event of patchRecorder.events) {
744
- patches.push(...event.patches)
745
- inversePatches.push(...event.inversePatches)
746
- }
747
-
748
- const event = {
749
- type: UndoEventType.Single,
750
- targetPath: fastGetRootPath(ctx.target, false).path,
751
- actionName: ctx.actionName,
752
- patches,
753
- inversePatches,
754
- } as const
755
-
756
- const parentGroup = patchRecorderData.group
757
-
758
- if (parentGroup) {
759
- manager.store._addUndoToParentGroup(parentGroup, event)
760
- } else {
761
- manager.store._addUndo({
762
- event: {
763
- ...event,
764
- attachedState: {
765
- beforeEvent: patchRecorderData.attachedStateBeforeEvent,
766
- afterEvent: options?.attachedState?.save(),
753
+ try {
754
+ if (patchRecorder.events.length > 0) {
755
+ const patches: Patch[] = []
756
+ const inversePatches: Patch[] = []
757
+
758
+ for (const event of patchRecorder.events) {
759
+ for (const patch of event.patches) patches.push(patch)
760
+ for (const patch of event.inversePatches) inversePatches.push(patch)
761
+ }
762
+
763
+ const event = {
764
+ type: UndoEventType.Single,
765
+ targetPath: fastGetRootPath(ctx.target, false).path,
766
+ actionName: ctx.actionName,
767
+ patches,
768
+ inversePatches,
769
+ } as const
770
+
771
+ const parentGroup = patchRecorderData.group
772
+
773
+ if (parentGroup) {
774
+ manager.store._addUndoToParentGroup(parentGroup, event)
775
+ } else {
776
+ manager.store._addUndo({
777
+ event: {
778
+ ...event,
779
+ attachedState: {
780
+ beforeEvent: patchRecorderData.attachedStateBeforeEvent,
781
+ afterEvent: options?.attachedState?.save(),
782
+ },
767
783
  },
768
- },
769
- maxUndoLevels: options?.maxUndoLevels,
770
- })
784
+ maxUndoLevels: options?.maxUndoLevels,
785
+ })
786
+ }
771
787
  }
788
+ } finally {
789
+ patchRecorder.dispose()
772
790
  }
773
-
774
- patchRecorder.dispose()
775
791
  }
776
792
  },
777
793
  })
@@ -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
+ >()