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
package/src/ref/core.ts CHANGED
@@ -88,10 +88,26 @@ export function internalCustomRef<T extends object>(
88
88
  trackedRefs.add(this as Ref<object>)
89
89
  this.internalForceUpdateBackRefs(initialTarget)
90
90
 
91
+ // MobX 4/5 do not pass the previous value to reaction effects.
92
+ // The reaction is usually created inside an action, so its first tracking run is deferred
93
+ // to the end of the batch and `initialTarget` may be stale by then; capture the value of
94
+ // that first run instead, since that is the one the effect will be compared against.
95
+ let previousTarget = initialTarget
96
+ let firstTrackingRun = true
91
97
  // according to mwestrate this won't leak as long as we don't keep the disposer around
92
98
  reaction(
93
- () => this.maybeCurrent,
94
- (newTarget, oldTarget) => {
99
+ () => {
100
+ const target = this.maybeCurrent
101
+ if (firstTrackingRun) {
102
+ firstTrackingRun = false
103
+ previousTarget = target
104
+ this.internalForceUpdateBackRefs(target)
105
+ }
106
+ return target
107
+ },
108
+ (newTarget) => {
109
+ const oldTarget = previousTarget
110
+ previousTarget = newTarget
95
111
  this.internalForceUpdateBackRefs(newTarget)
96
112
 
97
113
  if (onResolvedValueChange && newTarget !== oldTarget) {
@@ -4,9 +4,10 @@ import { HookAction } from "../action/hookActions"
4
4
  import { wrapInAction, wrapModelMethodInActionIfNeeded } from "../action/wrapInAction"
5
5
  import { type AnyModel, BaseModel } from "../model/BaseModel"
6
6
  import { WalkTreeMode, walkTree } from "../parent/walkTree"
7
+ import { forEachWithDelayedThrow } from "../utils/forEachWithDelayedThrow"
7
8
 
8
9
  const onAttachedDisposers = new WeakMap<object, () => void>()
9
- const attachedToRootStore = new WeakSet<object>()
10
+ const attachedToRootStore = new WeakMap<object, object>()
10
11
 
11
12
  /**
12
13
  * @internal
@@ -16,6 +17,7 @@ export const attachToRootStore = action(
16
17
  (rootStore: object, child: object): void => {
17
18
  // we use an array to ensure they will get called even if the actual hook modifies the tree
18
19
  const childrenToCall: AnyModel[] = []
20
+ const attachment = {}
19
21
 
20
22
  walkTree(
21
23
  child,
@@ -25,7 +27,7 @@ export const attachToRootStore = action(
25
27
  if (attachedToRootStore.has(ch)) {
26
28
  return
27
29
  }
28
- attachedToRootStore.add(ch)
30
+ attachedToRootStore.set(ch, attachment)
29
31
 
30
32
  if (ch instanceof BaseModel && (ch as any).onAttachedToRootStore) {
31
33
  wrapModelMethodInActionIfNeeded(
@@ -40,15 +42,23 @@ export const attachToRootStore = action(
40
42
  WalkTreeMode.ParentFirst
41
43
  )
42
44
 
43
- const childrenToCallLen = childrenToCall.length
44
- for (let i = 0; i < childrenToCallLen; i++) {
45
- const ch = childrenToCall[i]
45
+ forEachWithDelayedThrow(childrenToCall, (ch) => {
46
+ // An earlier hook may have detached or reattached this node already.
47
+ if (attachedToRootStore.get(ch) !== attachment) {
48
+ return
49
+ }
46
50
 
47
51
  const disposer = (ch as any).onAttachedToRootStore(rootStore)
48
52
  if (disposer) {
49
- onAttachedDisposers.set(ch, disposer)
53
+ if (attachedToRootStore.get(ch) === attachment) {
54
+ onAttachedDisposers.set(ch, disposer)
55
+ } else {
56
+ // The hook detached or reattached this node before returning its cleanup.
57
+ // Dispose this attachment without overwriting a newer attachment's cleanup.
58
+ wrapDisposer(ch, disposer)()
59
+ }
50
60
  }
51
- }
61
+ })
52
62
  }
53
63
  )
54
64
 
@@ -69,11 +79,7 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
69
79
  const disposer = onAttachedDisposers.get(ch)
70
80
  if (disposer) {
71
81
  // wrap disposer in action
72
- const disposerAction = wrapInAction({
73
- nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
74
- fn: disposer,
75
- actionType: ActionContextActionType.Sync,
76
- }).bind(ch)
82
+ const disposerAction = wrapDisposer(ch, disposer)
77
83
  onAttachedDisposers.delete(ch)
78
84
 
79
85
  disposersToCall.push(disposerAction)
@@ -82,8 +88,13 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
82
88
  WalkTreeMode.ChildrenFirst
83
89
  )
84
90
 
85
- const disposersToCallLen = disposersToCall.length
86
- for (let i = 0; i < disposersToCallLen; i++) {
87
- disposersToCall[i]()
88
- }
91
+ forEachWithDelayedThrow(disposersToCall, (dispose) => dispose())
89
92
  })
93
+
94
+ function wrapDisposer(node: object, disposer: () => void): () => void {
95
+ return wrapInAction({
96
+ nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
97
+ fn: disposer,
98
+ actionType: ActionContextActionType.Sync,
99
+ }).bind(node)
100
+ }
@@ -45,9 +45,11 @@ export const registerRootStore: <T extends object>(node: T) => T = action(
45
45
 
46
46
  entry.is = true
47
47
 
48
- attachToRootStore(node, node)
49
-
50
- entry.atom?.reportChanged()
48
+ try {
49
+ attachToRootStore(node, node)
50
+ } finally {
51
+ entry.atom?.reportChanged()
52
+ }
51
53
  return node
52
54
  }
53
55
  )
@@ -65,9 +67,11 @@ export const unregisterRootStore: (node: object) => void = action("unregisterRoo
65
67
  const entry = getOrCreateRootStoreEntry(node)
66
68
  entry.is = false
67
69
 
68
- detachFromRootStore(node)
69
-
70
- entry.atom?.reportChanged()
70
+ try {
71
+ detachFromRootStore(node)
72
+ } finally {
73
+ entry.atom?.reportChanged()
74
+ }
71
75
  })
72
76
 
73
77
  /**
@@ -69,13 +69,13 @@ export type SnapshotInOfModel<M extends AnyModel> = ModelFromSnapshot<M>
69
69
  export interface SnapshotInOfFrozen<F extends Frozen<any>> extends FrozenData<F["data"]> {}
70
70
 
71
71
  export interface SnapshotInOfObjectMap<V> {
72
- items?: { [k: string]: SnapshotOutOf<V> }
72
+ items?: { [k: string]: SnapshotInOf<V> }
73
73
  [modelTypeKey]?: string
74
74
  [modelIdKey]: string
75
75
  }
76
76
 
77
77
  export interface SnapshotInOfArraySet<V> {
78
- items?: SnapshotOutOf<V>[]
78
+ items?: SnapshotInOf<V>[]
79
79
  [modelTypeKey]?: string
80
80
  [modelIdKey]: string
81
81
  }
@@ -17,6 +17,7 @@ import { assertTweakedObject } from "../tweaker/core"
17
17
  import { assertIsObject, inDevMode, isArray, isMap, isPlainObject, isSet, lazy } from "../utils"
18
18
  import { runWithErrorDiagnosticsContext } from "../utils/errorDiagnostics"
19
19
  import { ModelPool } from "../utils/ModelPool"
20
+ import { flushInternalSnapshot, getInternalSnapshot } from "./internal"
20
21
  import { reconcileSnapshot } from "./reconcileSnapshot"
21
22
  import type { SnapshotInOf, SnapshotOutOf } from "./SnapshotOf"
22
23
  import { SnapshotProcessingError } from "./SnapshotProcessingError"
@@ -52,6 +53,14 @@ export function internalApplySnapshot<T extends object>(
52
53
  const obj = this
53
54
 
54
55
  const reconcile = () => {
56
+ // Avoid building a model pool (and its deep children traversal) when the
57
+ // node already holds this exact snapshot. Child updates may still await
58
+ // propagation within an action, so flush before trusting the reference.
59
+ flushInternalSnapshot(obj, false)
60
+ if (getInternalSnapshot(obj)?.transformed === sn) {
61
+ return
62
+ }
63
+
55
64
  const modelPool = new ModelPool(obj)
56
65
  const ret = reconcileSnapshot(obj, sn, modelPool, undefined)
57
66
 
@@ -120,7 +129,7 @@ export function internalApplySnapshot<T extends object>(
120
129
  }
121
130
 
122
131
  const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
123
- if (modelIdPropertyName) {
132
+ if (modelIdPropertyName !== undefined) {
124
133
  const id = (sn as any)[modelIdPropertyName]
125
134
  if (obj[modelIdKey] !== id) {
126
135
  // different id, no reconciliation possible
@@ -24,8 +24,7 @@ export function clone<T extends object>(node: T, options?: Partial<CloneOptions>
24
24
  assertTweakedObject(node, "node")
25
25
 
26
26
  const opts = {
27
- generateNewIds: true,
28
- ...options,
27
+ generateNewIds: options?.generateNewIds ?? true,
29
28
  }
30
29
 
31
30
  const sn = getSnapshot(node)
@@ -10,8 +10,7 @@ import { fromSnapshot } from "./fromSnapshot"
10
10
  */
11
11
  export function cloneTreeValue<T>(value: T, options?: Partial<CloneOptions>): T {
12
12
  const cloneOptions: CloneOptions = {
13
- generateNewIds: true,
14
- ...options,
13
+ generateNewIds: options?.generateNewIds ?? true,
15
14
  }
16
15
 
17
16
  if (isPrimitive(value)) {
@@ -5,6 +5,7 @@ import { modelTypeKey } from "../model/metadata"
5
5
  import { getModelOrSnapshotTypeAndId, getSnapshotModelType } from "../model/utils"
6
6
  import type { ModelClass } from "../modelShared/BaseModelShared"
7
7
  import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
8
+ import { hasOwnProp } from "../utils"
8
9
  import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
9
10
  import { type FromSnapshotContext, registerSnapshotter } from "./fromSnapshot"
10
11
  import type { SnapshotInOfModel } from "./SnapshotOf"
@@ -14,7 +15,7 @@ import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPrio
14
15
  function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotContext): AnyModel {
15
16
  const type = sn[modelTypeKey]
16
17
 
17
- if (!type) {
18
+ if (type === undefined) {
18
19
  throw new SnapshotProcessingError({
19
20
  message: `a model snapshot must contain a type key (${modelTypeKey}), but none was found`,
20
21
  actualSnapshot: sn,
@@ -30,7 +31,10 @@ function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotCon
30
31
  }
31
32
 
32
33
  const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
33
- if (modelIdPropertyName && sn[modelIdPropertyName] === undefined) {
34
+ if (
35
+ modelIdPropertyName !== undefined &&
36
+ (!hasOwnProp(sn, modelIdPropertyName) || sn[modelIdPropertyName] === undefined)
37
+ ) {
34
38
  throw new SnapshotProcessingError({
35
39
  message: `a model snapshot of type '${type}' must contain an id key (${modelIdPropertyName}), but none was found`,
36
40
  actualSnapshot: sn,
@@ -211,7 +211,9 @@ function snapshotToInitialData(
211
211
  }
212
212
  }
213
213
  const observableInitialData = observable.object(initialData, undefined, observableOptions)
214
- setModelInitialDataSnapshot(observableInitialData, initialData, allValuesPrimitive)
214
+ if (allValuesPrimitive) {
215
+ setModelInitialDataSnapshot(observableInitialData, initialData)
216
+ }
215
217
  return observableInitialData
216
218
  }
217
219
 
@@ -31,10 +31,13 @@ interface DirtyChild {
31
31
  readonly path: PathElement
32
32
  }
33
33
 
34
- // `false` means the generic freezer must traverse this raw value; `true` means
35
- // it is frozen; a SnapshotData value means it is mutable and its frozen state
36
- // must be reflected back into that owner. `undefined` is either external /
37
- // untracked data or a fused frozen clone, whose owner tracks its state directly.
34
+ // `false` means the generic freezer must traverse this raw value; `true` means it
35
+ // is frozen; a SnapshotData value means it is mutable and owned by that snapshot,
36
+ // which is where its frozen state must be reflected back. Both the untransformed
37
+ // and the transformed value of a snapshot point to their owner, so freezing can
38
+ // reach descendants that are only shared through processor wrappers. `undefined`
39
+ // is either external / untracked data or a fused frozen clone, whose owner tracks
40
+ // its state directly.
38
41
  const snapshotStates = new WeakMap<object, boolean | SnapshotData>()
39
42
 
40
43
  /**
@@ -101,7 +104,7 @@ export const setNewInternalSnapshot = action(
101
104
  snapshotStates.set(untransformed, markAsFrozen ? true : sn)
102
105
 
103
106
  if (transformed !== undefined && transformed !== untransformed) {
104
- snapshotStates.set(transformed, markAsFrozen)
107
+ snapshotStates.set(transformed, markAsFrozen ? true : sn)
105
108
  }
106
109
 
107
110
  getOrCreateTreeNodeMetadata(value).snapshot = sn
@@ -143,7 +146,7 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
143
146
  transformed !== undefined &&
144
147
  (!wasFrozen || transformed !== untransformed)
145
148
  ) {
146
- snapshotStates.set(transformed, false)
149
+ snapshotStates.set(transformed, sn)
147
150
  }
148
151
  }
149
152
 
@@ -335,7 +338,7 @@ export const refreshInternalSnapshot = action("refreshInternalSnapshot", (value:
335
338
  // transformed snapshots created by internal transforms must be tracked as mutable
336
339
  // until they are exposed through getSnapshot / freezeInternalSnapshot.
337
340
  if (snapshotStates.get(newTransformed) === undefined) {
338
- snapshotStates.set(newTransformed, false)
341
+ snapshotStates.set(newTransformed, sn)
339
342
  }
340
343
 
341
344
  sn.atom?.reportChanged()
@@ -370,6 +373,16 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
370
373
  return data
371
374
  }
372
375
 
376
+ const owner = state === false ? undefined : state
377
+
378
+ // Processors can create untracked wrappers around tracked descendants, and the
379
+ // freezer stops at those (their state is `undefined`). Freeze the source of the
380
+ // transformed value as well, so shared descendants that the transformed
381
+ // traversal cannot reach still switch to copy-on-write.
382
+ if (owner !== undefined && owner.untransformed !== data) {
383
+ freezeInternalSnapshot(owner.untransformed)
384
+ }
385
+
373
386
  if (Array.isArray(data)) {
374
387
  for (let i = 0; i < data.length; i++) {
375
388
  freezeInternalSnapshot(data[i])
@@ -383,8 +396,8 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
383
396
 
384
397
  snapshotStates.set(data, true)
385
398
 
386
- if (typeof state === "object" && state.untransformed === data) {
387
- state.untransformedFrozen = true
399
+ if (owner !== undefined && owner.untransformed === data) {
400
+ owner.untransformedFrozen = true
388
401
  }
389
402
 
390
403
  return data
@@ -1,40 +1,35 @@
1
- interface ModelInitialDataSnapshot {
2
- readonly snapshot: Record<string, unknown>
3
- readonly allValuesPrimitive: boolean
4
- reusable: boolean
5
- }
1
+ import { isPrimitive, setProtoProp } from "../utils"
6
2
 
7
- const modelInitialDataSnapshots = new WeakMap<object, ModelInitialDataSnapshot>()
3
+ // Only primitive-only initial data can double as a snapshot without tweaking.
4
+ const modelInitialDataSnapshots = new WeakMap<object, Record<string, unknown>>()
8
5
 
9
6
  /**
10
7
  * @internal
11
8
  */
12
- export function setModelInitialDataSnapshot(
13
- data: object,
14
- snapshot: Record<string, unknown>,
15
- allValuesPrimitive: boolean
16
- ): void {
17
- modelInitialDataSnapshots.set(data, {
18
- snapshot,
19
- allValuesPrimitive,
20
- reusable: true,
21
- })
9
+ export function setModelInitialDataSnapshot(data: object, snapshot: Record<string, unknown>): void {
10
+ modelInitialDataSnapshots.set(data, snapshot)
22
11
  }
23
12
 
24
13
  /**
25
14
  * @internal
26
15
  */
27
- export function invalidateModelInitialDataSnapshot(data: object): void {
16
+ export function updateModelInitialDataSnapshot(data: object, key: string, value: unknown): void {
28
17
  const initialDataSnapshot = modelInitialDataSnapshots.get(data)
29
18
  if (initialDataSnapshot) {
30
- initialDataSnapshot.reusable = false
19
+ if (!isPrimitive(value)) {
20
+ modelInitialDataSnapshots.delete(data)
21
+ } else if (key === "__proto__") {
22
+ setProtoProp(initialDataSnapshot, value)
23
+ } else {
24
+ initialDataSnapshot[key] = value
25
+ }
31
26
  }
32
27
  }
33
28
 
34
29
  /**
35
30
  * @internal
36
31
  */
37
- export function takeModelInitialDataSnapshot(data: object): ModelInitialDataSnapshot | undefined {
32
+ export function takeModelInitialDataSnapshot(data: object): Record<string, unknown> | undefined {
38
33
  const initialDataSnapshot = modelInitialDataSnapshots.get(data)
39
34
  modelInitialDataSnapshots.delete(data)
40
35
  return initialDataSnapshot
@@ -31,9 +31,20 @@ export function onSnapshot<T extends object>(
31
31
  assertTweakedObject(node, "node")
32
32
 
33
33
  let currentSnapshot = getSnapshot(node)
34
+ let firstTrackingRun = true
34
35
 
35
36
  return reaction(
36
- () => getSnapshot(nodeFn()),
37
+ () => {
38
+ const snapshot = getSnapshot(nodeFn())
39
+ // Registration inside an action defers the first tracking run until the
40
+ // action ends. Its snapshot, rather than the earlier registration state,
41
+ // is the baseline for the first notification.
42
+ if (firstTrackingRun) {
43
+ firstTrackingRun = false
44
+ currentSnapshot = snapshot
45
+ }
46
+ return snapshot
47
+ },
37
48
  (newSnapshot) => {
38
49
  const prevSn = currentSnapshot
39
50
  currentSnapshot = newSnapshot
@@ -1,4 +1,4 @@
1
- import { remove } from "mobx"
1
+ import { remove, set } from "mobx"
2
2
  import type { AnyModel } from "../model/BaseModel"
3
3
  import { getModelIdPropertyName } from "../model/getModelMetadata"
4
4
  import { isReservedModelKey, modelIdKey, modelTypeKey } from "../model/metadata"
@@ -17,7 +17,7 @@ import {
17
17
  runTypeCheckingAfterChange,
18
18
  } from "../tweaker/typeChecking"
19
19
  import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
20
- import { isArray } from "../utils"
20
+ import { hasOwnProp, isArray, isPrimitive } from "../utils"
21
21
  import { withErrorModelTrailEntry, withErrorPathSegment } from "../utils/errorDiagnostics"
22
22
  import type { ModelPool } from "../utils/ModelPool"
23
23
  import { setIfDifferent } from "../utils/setIfDifferent"
@@ -51,22 +51,22 @@ function reconcileModelSnapshot(
51
51
  // the canonical output snapshot. A successful canonical no-op therefore
52
52
  // cannot produce a diagnostic, so keep this hottest path outside the
53
53
  // model-trail bookkeeping and defer id metadata until needed. The
54
- // positional value was already flushed by the getSnapshot call in
54
+ // positional value was already flushed by the internal snapshot read in
55
55
  // reconcileSnapshot, so its transformed snapshot is current.
56
56
  if (isCanonicalSnapshotNoOp(value, type, sn)) {
57
57
  return value
58
58
  }
59
59
 
60
60
  const modelIdPropertyName = getModelIdPropertyName(modelClass)
61
- const incomingModelId = modelIdPropertyName ? sn[modelIdPropertyName] : undefined
61
+ const incomingModelId = modelIdPropertyName !== undefined ? sn[modelIdPropertyName] : undefined
62
62
  const positionalModelMatches =
63
63
  isModel(value) &&
64
64
  value[modelTypeKey] === type &&
65
- (!modelIdPropertyName || value[modelIdKey] === incomingModelId)
65
+ (modelIdPropertyName === undefined || value[modelIdKey] === incomingModelId)
66
66
 
67
67
  // The positional model is already the desired instance in the common
68
68
  // stable-update case. Only moved identified models need a pool lookup.
69
- if (!positionalModelMatches && modelIdPropertyName) {
69
+ if (!positionalModelMatches && modelIdPropertyName !== undefined) {
70
70
  const modelInPool = modelPool.findModelByTypeAndId(type, incomingModelId)
71
71
  if (modelInPool) {
72
72
  value = modelInPool
@@ -92,7 +92,7 @@ function reconcileModelSnapshot(
92
92
  }
93
93
 
94
94
  const modelProps = getInternalModelClassPropsInfo(modelClass)
95
- if (modelIdPropertyName) {
95
+ if (modelIdPropertyName !== undefined) {
96
96
  if (value[modelIdKey] !== incomingModelId) {
97
97
  // different id, no reconciliation possible
98
98
  return fromSnapshot<AnyModel>(sn)
@@ -134,7 +134,7 @@ function reconcileModelSnapshot(
134
134
  const dataKeysLen = dataKeys.length
135
135
  for (let i = 0; i < dataKeysLen; i++) {
136
136
  const k = dataKeys[i]
137
- if (!(k in processedSn)) {
137
+ if (!hasOwnProp(processedSn, k)) {
138
138
  // use default value if applicable
139
139
  const modelProp = modelProps[k] as AnyModelProp | undefined
140
140
  const defaultValue = modelProp
@@ -157,6 +157,14 @@ function reconcileModelSnapshot(
157
157
  const v = processedSn[k]
158
158
 
159
159
  const oldValue = data[k]
160
+ // Non-null primitives need neither reconciliation nor detachment.
161
+ // Nullish values must still pass through default handling below.
162
+ if (v != null && isPrimitive(v)) {
163
+ if (v !== oldValue) {
164
+ set(data, k, v)
165
+ }
166
+ continue
167
+ }
160
168
  let newValue = withErrorPathSegment(k, () =>
161
169
  reconcileSnapshot(oldValue, v, modelPool, modelObj)
162
170
  )
@@ -1,10 +1,11 @@
1
1
  import { isObservableObject, remove } from "mobx"
2
+ import { isModel } from "../model/utils"
2
3
  import {
3
4
  isTypeCheckingAfterChangeEnabled,
4
5
  runTypeCheckingAfterChange,
5
6
  } from "../tweaker/typeChecking"
6
7
  import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
7
- import { isPlainObject } from "../utils"
8
+ import { hasOwnProp, isPlainObject } from "../utils"
8
9
  import { withErrorPathSegment } from "../utils/errorDiagnostics"
9
10
  import type { ModelPool } from "../utils/ModelPool"
10
11
  import { setIfDifferent } from "../utils/setIfDifferent"
@@ -19,8 +20,8 @@ function reconcilePlainObjectSnapshot(
19
20
  sn: SnapshotInOfObject<any>,
20
21
  modelPool: ModelPool
21
22
  ): object {
22
- // plain obj
23
- if (!(isPlainObject(value) || isObservableObject(value))) {
23
+ // Observable models carry instance state and must be replaced, not edited as plain objects.
24
+ if (isModel(value) || !(isPlainObject(value) || isObservableObject(value))) {
24
25
  // no reconciliation possible
25
26
  return fromSnapshot(sn)
26
27
  }
@@ -35,7 +36,7 @@ function reconcilePlainObjectSnapshot(
35
36
  const plainObjKeysLen = plainObjKeys.length
36
37
  for (let i = 0; i < plainObjKeysLen; i++) {
37
38
  const k = plainObjKeys[i]
38
- if (!(k in sn)) {
39
+ if (!hasOwnProp(sn, k)) {
39
40
  remove(plainObj, k)
40
41
  }
41
42
  }
@@ -5,6 +5,7 @@ import { fastGetParentPathIncludingDataObjects } from "../parent"
5
5
  import { isMap, isPrimitive, isSet } from "../utils"
6
6
  import type { ModelPool } from "../utils/ModelPool"
7
7
  import { getSnapshot } from "./getSnapshot"
8
+ import { flushInternalSnapshot, getInternalSnapshot } from "./internal"
8
9
  import { registerDefaultReconcilers } from "./registerDefaultReconcilers"
9
10
  import { SnapshotProcessingError } from "./SnapshotProcessingError"
10
11
 
@@ -28,10 +29,18 @@ export function reconcileSnapshot(value: any, sn: any, modelPool: ModelPool, par
28
29
  return sn
29
30
  }
30
31
 
31
- // if the snapshot passed is exactly the same as the current one
32
- // then it is already reconciled
33
- if (getSnapshot(value) === sn) {
34
- return value
32
+ // Unlike getSnapshot, this comparison never exposes the snapshot, so it needs
33
+ // neither freezing nor observation registration.
34
+ const currentSnapshot = getInternalSnapshot(value)
35
+ if (currentSnapshot) {
36
+ flushInternalSnapshot(value, false)
37
+ if (currentSnapshot.transformed === sn) {
38
+ return value
39
+ }
40
+ } else if (!isPrimitive(value)) {
41
+ // A non-primitive without an internal snapshot is not a valid reconciliation
42
+ // target (e.g. a non-node object); let getSnapshot raise the proper error.
43
+ getSnapshot(value)
35
44
  }
36
45
 
37
46
  registerDefaultReconcilers()
@@ -26,7 +26,7 @@ export const arrayActions = {
26
26
 
27
27
  concat: standaloneAction(
28
28
  `${namespace}::concat`,
29
- <T>(array: T[], ...items: ConcatArray<T>[]): T[] => {
29
+ <T>(array: T[], ...items: (T | ConcatArray<T>)[]): T[] => {
30
30
  return array.concat(...items)
31
31
  }
32
32
  ),
@@ -89,9 +89,19 @@ export const arrayActions = {
89
89
  swap: standaloneAction(
90
90
  `${namespace}::swap`,
91
91
  <T>(array: T[], index1: number, index2: number): boolean => {
92
- if (index1 < 0 || index2 < 0 || index1 >= array.length || index2 >= array.length) {
92
+ if (
93
+ !Number.isInteger(index1) ||
94
+ !Number.isInteger(index2) ||
95
+ index1 < 0 ||
96
+ index2 < 0 ||
97
+ index1 >= array.length ||
98
+ index2 >= array.length
99
+ ) {
93
100
  return false
94
101
  }
102
+ if (index1 === index2) {
103
+ return true
104
+ }
95
105
  if (index2 < index1) {
96
106
  ;[index1, index2] = [index2, index1]
97
107
  }
@@ -1,4 +1,5 @@
1
1
  import { isObservable, remove } from "mobx"
2
+ import { isModel } from "../model/utils"
2
3
  import { toTreeNode } from "../tweaker/tweak"
3
4
  import { assertIsObject, namespace as ns } from "../utils"
4
5
  import type { AnyFunction } from "../utils/AnyFunction"
@@ -11,7 +12,7 @@ export const objectActions = {
11
12
  set: standaloneAction(
12
13
  `${namespace}::set`,
13
14
  <T extends object, K extends keyof T>(target: T, key: K, value: T[K]): void => {
14
- if (isObservable(target)) {
15
+ if (isObservable(target) && !isModel(target)) {
15
16
  setIfDifferent(target, key, value)
16
17
  } else {
17
18
  target[key] = value
@@ -25,7 +26,7 @@ export const objectActions = {
25
26
  assertIsObject(partialObject, "partialObject")
26
27
  const keys = Object.keys(partialObject)
27
28
 
28
- if (isObservable(target)) {
29
+ if (isObservable(target) && !isModel(target)) {
29
30
  for (const key of keys) {
30
31
  const newValue = (partialObject as any)[key]
31
32
  setIfDifferent(target, key, newValue)
@@ -31,6 +31,10 @@ const errMessage = "this Date object is immutable"
31
31
  export class ImmutableDate extends Date {
32
32
  // disable mutable methods
33
33
 
34
+ setYear(): never {
35
+ throw failure(errMessage)
36
+ }
37
+
34
38
  setTime(): any {
35
39
  throw failure(errMessage)
36
40
  }
@@ -1,4 +1,5 @@
1
1
  import type { ModelPropTransform } from "../modelShared/prop"
2
+ import { setProtoProp } from "../utils"
2
3
  import { asMap } from "../wrappers/asMap"
3
4
 
4
5
  const _objectToMapTransform: ModelPropTransform<Record<string, unknown>, Map<string, unknown>> = {
@@ -11,7 +12,8 @@ const _objectToMapTransform: ModelPropTransform<Record<string, unknown>, Map<str
11
12
 
12
13
  const obj: Record<string, unknown> = {}
13
14
  map.forEach((v, k) => {
14
- obj[k] = v
15
+ if (k === "__proto__") setProtoProp(obj, v)
16
+ else obj[k] = v
15
17
  })
16
18
 
17
19
  return obj