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
@@ -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
  }
@@ -13,6 +13,7 @@ import {
13
13
  getModelNotRegisteredErrorMessage,
14
14
  modelInfoByClass,
15
15
  } from "../modelShared/modelInfo"
16
+ import { withChangeBatch } from "../tweaker/changeRollback"
16
17
  import { assertTweakedObject } from "../tweaker/core"
17
18
  import { assertIsObject, inDevMode, isArray, isMap, isPlainObject, isSet, lazy } from "../utils"
18
19
  import { runWithErrorDiagnosticsContext } from "../utils/errorDiagnostics"
@@ -24,6 +25,7 @@ import { SnapshotProcessingError } from "./SnapshotProcessingError"
24
25
 
25
26
  /**
26
27
  * Applies a full snapshot over an object, reconciling it with the current contents of the object.
28
+ * If it throws, the changes it made are rolled back (errors thrown by listeners do not roll them back).
27
29
  *
28
30
  * @template T Object type.
29
31
  * @param node Target object (model object, object or array).
@@ -52,27 +54,33 @@ export function internalApplySnapshot<T extends object>(
52
54
  ): void {
53
55
  const obj = this
54
56
 
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
- }
57
+ const reconcile = () =>
58
+ withChangeBatch(() => {
59
+ // Avoid building a model pool (and its deep children traversal) when the
60
+ // node already holds this exact snapshot. Child updates may still await
61
+ // propagation within an action, so flush before trusting the reference.
62
+ flushInternalSnapshot(obj, false)
63
+ if (getInternalSnapshot(obj)?.transformed === sn) {
64
+ return
65
+ }
63
66
 
64
- const modelPool = new ModelPool(obj)
65
- const ret = reconcileSnapshot(obj, sn, modelPool, undefined)
67
+ const modelPool = new ModelPool(obj)
68
+ let ret: unknown
69
+ try {
70
+ ret = reconcileSnapshot(obj, sn, modelPool, undefined)
71
+ } finally {
72
+ modelPool.release()
73
+ }
66
74
 
67
- if (inDevMode) {
68
- if (ret !== obj) {
69
- throw new SnapshotProcessingError({
70
- message: "assertion failed: reconciled object has to be the same",
71
- actualSnapshot: sn,
72
- })
75
+ if (inDevMode) {
76
+ if (ret !== obj) {
77
+ throw new SnapshotProcessingError({
78
+ message: "assertion failed: reconciled object has to be the same",
79
+ actualSnapshot: sn,
80
+ })
81
+ }
73
82
  }
74
- }
75
- }
83
+ })
76
84
 
77
85
  if (isArray(sn)) {
78
86
  if (!isArray(obj)) {
@@ -129,7 +137,7 @@ export function internalApplySnapshot<T extends object>(
129
137
  }
130
138
 
131
139
  const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
132
- if (modelIdPropertyName) {
140
+ if (modelIdPropertyName !== undefined) {
133
141
  const id = (sn as any)[modelIdPropertyName]
134
142
  if (obj[modelIdKey] !== id) {
135
143
  // 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,
@@ -1,13 +1,8 @@
1
1
  import { observable } from "mobx"
2
2
  import { tweakPlainObject } from "../tweaker/tweakPlainObject"
3
- import { isPlainObject, setProtoProp } from "../utils"
3
+ import { isPlainObject, nodeObservableOptions, setProtoProp } from "../utils"
4
4
  import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
5
- import {
6
- type FromSnapshotContext,
7
- internalFromSnapshot,
8
- observableOptions,
9
- registerSnapshotter,
10
- } from "./fromSnapshot"
5
+ import { type FromSnapshotContext, internalFromSnapshot, registerSnapshotter } from "./fromSnapshot"
11
6
  import type { SnapshotInOfObject } from "./SnapshotOf"
12
7
  import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
13
8
 
@@ -37,7 +32,7 @@ function fromPlainObjectSnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotC
37
32
  }
38
33
  }
39
34
  return tweakPlainObject(
40
- observable.object(plainObj, undefined, observableOptions),
35
+ observable.object(plainObj, undefined, nodeObservableOptions),
41
36
  undefined,
42
37
  undefined,
43
38
  true,
@@ -5,7 +5,7 @@ import { resolveStandardTypeNoThrow, resolveTypeChecker } from "../types/resolve
5
5
  import type { AnyType, TypeToData, TypeToSnapshotIn } from "../types/schemas"
6
6
  import { isLateTypeChecker, TypeChecker } from "../types/TypeChecker"
7
7
  import { resolveCodecSupport } from "../types/utility/typesCodec"
8
- import { isMap, isPrimitive, isSet, setProtoProp } from "../utils"
8
+ import { isMap, isPrimitive, isSet, nodeObservableOptions, setProtoProp } from "../utils"
9
9
  import {
10
10
  getCurrentErrorDiagnosticsContext,
11
11
  runWithErrorDiagnosticsContext,
@@ -210,13 +210,9 @@ function snapshotToInitialData(
210
210
  }
211
211
  }
212
212
  }
213
- const observableInitialData = observable.object(initialData, undefined, observableOptions)
213
+ const observableInitialData = observable.object(initialData, undefined, nodeObservableOptions)
214
214
  if (allValuesPrimitive) {
215
215
  setModelInitialDataSnapshot(observableInitialData, initialData)
216
216
  }
217
217
  return observableInitialData
218
218
  }
219
-
220
- export const observableOptions = {
221
- deep: false,
222
- }
@@ -19,6 +19,10 @@ export interface SnapshotData {
19
19
  atom: IAtom | undefined
20
20
  // Source of truth for copy-on-write of this snapshot's untransformed data.
21
21
  untransformedFrozen: boolean
22
+ // When defined while `untransformed` is not frozen, every value in it except
23
+ // (possibly) these is already frozen, so freezing it only needs to visit them.
24
+ // Values listed here may no longer be in `untransformed`.
25
+ unfrozenValues: object[] | undefined
22
26
  dirtyChild: object | undefined
23
27
  dirtyChildSnapshot: SnapshotData | undefined
24
28
  dirtyChildPath: PathElement | undefined
@@ -31,10 +35,13 @@ interface DirtyChild {
31
35
  readonly path: PathElement
32
36
  }
33
37
 
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.
38
+ // `false` means the generic freezer must traverse this raw value; `true` means it
39
+ // is frozen; a SnapshotData value means it is mutable and owned by that snapshot,
40
+ // which is where its frozen state must be reflected back. Both the untransformed
41
+ // and the transformed value of a snapshot point to their owner, so freezing can
42
+ // reach descendants that are only shared through processor wrappers. `undefined`
43
+ // is either external / untracked data or a fused frozen clone, whose owner tracks
44
+ // its state directly.
38
45
  const snapshotStates = new WeakMap<object, boolean | SnapshotData>()
39
46
 
40
47
  /**
@@ -91,6 +98,7 @@ export const setNewInternalSnapshot = action(
91
98
  transformed,
92
99
  atom: undefined, // will be created when first observed
93
100
  untransformedFrozen: markAsFrozen,
101
+ unfrozenValues: undefined,
94
102
  dirtyChild: undefined,
95
103
  dirtyChildSnapshot: undefined,
96
104
  dirtyChildPath: undefined,
@@ -101,7 +109,7 @@ export const setNewInternalSnapshot = action(
101
109
  snapshotStates.set(untransformed, markAsFrozen ? true : sn)
102
110
 
103
111
  if (transformed !== undefined && transformed !== untransformed) {
104
- snapshotStates.set(transformed, markAsFrozen)
112
+ snapshotStates.set(transformed, markAsFrozen ? true : sn)
105
113
  }
106
114
 
107
115
  getOrCreateTreeNodeMetadata(value).snapshot = sn
@@ -110,7 +118,10 @@ export const setNewInternalSnapshot = action(
110
118
  }
111
119
  )
112
120
 
113
- type MutateInternalSnapshotFn<T> = (prevSn: T) => void
121
+ /**
122
+ * Mutates a snapshot container in place and returns the values it wrote.
123
+ */
124
+ type MutateInternalSnapshotFn<T> = (prevSn: T) => readonly unknown[]
114
125
 
115
126
  function makeSnapshotMutable(sn: SnapshotData): any {
116
127
  let untransformed = sn.untransformed
@@ -124,7 +135,39 @@ function makeSnapshotMutable(sn: SnapshotData): any {
124
135
  return untransformed
125
136
  }
126
137
 
127
- function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned = false): void {
138
+ function trackUnfrozenValues(
139
+ sn: SnapshotData,
140
+ untransformed: any,
141
+ wasFrozen: boolean,
142
+ writtenValues: readonly unknown[]
143
+ ): void {
144
+ // a cloned frozen container starts with only frozen values
145
+ let unfrozenValues = wasFrozen ? [] : sn.unfrozenValues
146
+ if (unfrozenValues === undefined) {
147
+ return
148
+ }
149
+ for (let i = 0; i < writtenValues.length; i++) {
150
+ const value = writtenValues[i]
151
+ if (!isPrimitive(value)) {
152
+ unfrozenValues.push(value)
153
+ }
154
+ }
155
+ const size = Array.isArray(untransformed)
156
+ ? untransformed.length
157
+ : Object.keys(untransformed).length
158
+ if (unfrozenValues.length > size) {
159
+ // cheaper to visit the whole container again
160
+ unfrozenValues = undefined
161
+ }
162
+ sn.unfrozenValues = unfrozenValues
163
+ }
164
+
165
+ function setSnapshotData(
166
+ sn: SnapshotData,
167
+ untransformed: any,
168
+ writtenValues: readonly unknown[],
169
+ freezeIfCloned = false
170
+ ): void {
128
171
  // `makeSnapshotMutable` returns a new container exactly when this snapshot
129
172
  // was frozen. All callers obtain `untransformed` through that helper.
130
173
  const wasFrozen = untransformed !== sn.untransformed
@@ -135,6 +178,11 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
135
178
 
136
179
  const keepFrozen = freezeIfCloned && wasFrozen && !sn.transformFn
137
180
  sn.untransformedFrozen = keepFrozen
181
+ if (keepFrozen) {
182
+ sn.unfrozenValues = undefined
183
+ } else {
184
+ trackUnfrozenValues(sn, untransformed, wasFrozen, writtenValues)
185
+ }
138
186
  if (wasFrozen && !keepFrozen) {
139
187
  snapshotStates.set(untransformed, sn)
140
188
  }
@@ -143,14 +191,14 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
143
191
  transformed !== undefined &&
144
192
  (!wasFrozen || transformed !== untransformed)
145
193
  ) {
146
- snapshotStates.set(transformed, false)
194
+ snapshotStates.set(transformed, sn)
147
195
  }
148
196
  }
149
197
 
150
198
  function updateOwnSnapshot<T>(sn: SnapshotData, mutate: MutateInternalSnapshotFn<T>): void {
151
199
  const untransformed = makeSnapshotMutable(sn)
152
- mutate(untransformed)
153
- setSnapshotData(sn, untransformed)
200
+ const writtenValues = mutate(untransformed)
201
+ setSnapshotData(sn, untransformed, writtenValues)
154
202
  sn.atom?.reportChanged()
155
203
  }
156
204
 
@@ -161,7 +209,7 @@ function updateParentSlot(parentSn: SnapshotData, path: PathElement, value: any)
161
209
  } else {
162
210
  untransformed[path] = value
163
211
  }
164
- setSnapshotData(parentSn, untransformed)
212
+ setSnapshotData(parentSn, untransformed, [value])
165
213
  parentSn.atom?.reportChanged()
166
214
  }
167
215
 
@@ -172,7 +220,8 @@ function flushDirtyChild(
172
220
  childSn: SnapshotData,
173
221
  childPath: PathElement,
174
222
  freezeAfterFlush: boolean,
175
- freezeClonedContainer: boolean
223
+ freezeClonedContainer: boolean,
224
+ writtenValues: unknown[]
176
225
  ): void {
177
226
  if (childSn.isUnset) {
178
227
  return
@@ -197,6 +246,7 @@ function flushDirtyChild(
197
246
  } else {
198
247
  untransformed[childPath] = childSn.transformed
199
248
  }
249
+ writtenValues.push(childSn.transformed)
200
250
  }
201
251
 
202
252
  function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: boolean): boolean {
@@ -215,6 +265,7 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
215
265
  const untransformed = makeSnapshotMutable(sn)
216
266
  const freezeClonedContainer =
217
267
  freezeAfterFlush && untransformed !== sn.untransformed && !sn.transformFn
268
+ const writtenValues: unknown[] = []
218
269
 
219
270
  flushDirtyChild(
220
271
  node,
@@ -223,7 +274,8 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
223
274
  dirtyChildSnapshot,
224
275
  dirtyChildPath,
225
276
  freezeAfterFlush,
226
- freezeClonedContainer
277
+ freezeClonedContainer,
278
+ writtenValues
227
279
  )
228
280
  additionalDirtyChildren?.forEach((dirtyChild, child) => {
229
281
  flushDirtyChild(
@@ -233,11 +285,12 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
233
285
  dirtyChild.snapshot,
234
286
  dirtyChild.path,
235
287
  freezeAfterFlush,
236
- freezeClonedContainer
288
+ freezeClonedContainer,
289
+ writtenValues
237
290
  )
238
291
  })
239
292
 
240
- setSnapshotData(sn, untransformed, freezeClonedContainer)
293
+ setSnapshotData(sn, untransformed, writtenValues, freezeClonedContainer)
241
294
  return freezeClonedContainer
242
295
  }
243
296
 
@@ -335,7 +388,7 @@ export const refreshInternalSnapshot = action("refreshInternalSnapshot", (value:
335
388
  // transformed snapshots created by internal transforms must be tracked as mutable
336
389
  // until they are exposed through getSnapshot / freezeInternalSnapshot.
337
390
  if (snapshotStates.get(newTransformed) === undefined) {
338
- snapshotStates.set(newTransformed, false)
391
+ snapshotStates.set(newTransformed, sn)
339
392
  }
340
393
 
341
394
  sn.atom?.reportChanged()
@@ -370,7 +423,23 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
370
423
  return data
371
424
  }
372
425
 
373
- if (Array.isArray(data)) {
426
+ const owner = state === false ? undefined : state
427
+
428
+ // Processors can create untracked wrappers around tracked descendants, and the
429
+ // freezer stops at those (their state is `undefined`). Freeze the source of the
430
+ // transformed value as well, so shared descendants that the transformed
431
+ // traversal cannot reach still switch to copy-on-write.
432
+ if (owner !== undefined && owner.untransformed !== data) {
433
+ freezeInternalSnapshot(owner.untransformed)
434
+ }
435
+
436
+ const unfrozenValues =
437
+ owner !== undefined && owner.untransformed === data ? owner.unfrozenValues : undefined
438
+ if (unfrozenValues !== undefined) {
439
+ for (let i = 0; i < unfrozenValues.length; i++) {
440
+ freezeInternalSnapshot(unfrozenValues[i])
441
+ }
442
+ } else if (Array.isArray(data)) {
374
443
  for (let i = 0; i < data.length; i++) {
375
444
  freezeInternalSnapshot(data[i])
376
445
  }
@@ -383,8 +452,9 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
383
452
 
384
453
  snapshotStates.set(data, true)
385
454
 
386
- if (typeof state === "object" && state.untransformed === data) {
387
- state.untransformedFrozen = true
455
+ if (owner !== undefined && owner.untransformed === data) {
456
+ owner.untransformedFrozen = true
457
+ owner.unfrozenValues = undefined
388
458
  }
389
459
 
390
460
  return data
@@ -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
@@ -17,7 +17,7 @@ import {
17
17
  runTypeCheckingAfterChange,
18
18
  } from "../tweaker/typeChecking"
19
19
  import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
20
- import { isArray, isPrimitive } 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"
@@ -58,15 +58,15 @@ function reconcileModelSnapshot(
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
@@ -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
  }
@@ -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
@@ -29,21 +29,23 @@ export function deepEquals(a: any, b: any): boolean {
29
29
  return true
30
30
  }
31
31
 
32
- // use snapshots to compare if possible
33
- // since snapshots use structural sharing it is more likely
34
- // to speed up comparisons
35
- if (isTreeNode(a)) {
36
- a = getSnapshot(a)
37
- } else if (isObservable(a)) {
38
- a = toJS(a, toJSOptions)
39
- }
40
- if (isTreeNode(b)) {
41
- b = getSnapshot(b)
42
- } else if (isObservable(b)) {
43
- b = toJS(b, toJSOptions)
32
+ const aIsObject = a !== null && typeof a === "object"
33
+ const bIsObject = b !== null && typeof b === "object"
34
+ if (!aIsObject && !bIsObject) {
35
+ // Strict equality already handled all equal primitives except NaN.
36
+ return Number.isNaN(a) && Number.isNaN(b)
44
37
  }
45
38
 
46
- return fastDeepEqual(a, b)
39
+ return fastDeepEqual(aIsObject ? comparisonValue(a) : a, bIsObject ? comparisonValue(b) : b)
40
+ }
41
+
42
+ function comparisonValue(value: object): unknown {
43
+ if (isTreeNode(value)) {
44
+ // Snapshots preserve structural sharing. Model $ objects are deliberately
45
+ // handled as observable objects rather than standalone tree nodes.
46
+ return getSnapshot(value)
47
+ }
48
+ return isObservable(value) ? toJS(value, toJSOptions) : value
47
49
  }
48
50
 
49
51
  const toJSOptions =
@@ -38,6 +38,10 @@ export class Draft<T extends object> {
38
38
  */
39
39
  @mobxAction
40
40
  commitByPath(path: Path): void {
41
+ if (path.length === 0) {
42
+ this.commit()
43
+ return
44
+ }
41
45
  const draftTarget = resolvePath(this.data, path)
42
46
  if (!draftTarget.resolved) {
43
47
  throw failure(`path ${JSON.stringify(path)} could not be resolved in draft object`)
@@ -76,6 +80,10 @@ export class Draft<T extends object> {
76
80
  */
77
81
  @mobxAction
78
82
  resetByPath(path: Path): void {
83
+ if (path.length === 0) {
84
+ this.reset()
85
+ return
86
+ }
79
87
  const originalTarget = resolvePath(this.originalData, path)
80
88
  if (!originalTarget.resolved) {
81
89
  throw failure(`path ${JSON.stringify(path)} could not be resolved in original object`)