mobx-keystone 1.25.0 → 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 (115) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/mobx-keystone.esm.js +1478 -952
  3. package/dist/mobx-keystone.esm.mjs +1478 -952
  4. package/dist/mobx-keystone.umd.js +1483 -952
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  7. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  8. package/dist/types/parent/detach.d.ts +1 -1
  9. package/dist/types/parent/findChildren.d.ts +1 -1
  10. package/dist/types/patch/applyPatches.d.ts +2 -0
  11. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  12. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  13. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  14. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  15. package/dist/types/types/TypeCheckError.d.ts +1 -0
  16. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  17. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  18. package/dist/types/utils/index.d.ts +13 -0
  19. package/dist/types/utils/mapUtils.d.ts +2 -2
  20. package/dist/types/utils/setLike.d.ts +1 -0
  21. package/package.json +1 -1
  22. package/src/action/applyAction.ts +1 -1
  23. package/src/action/middleware.ts +17 -36
  24. package/src/action/modelFlow.ts +4 -5
  25. package/src/action/mutationBatch.ts +10 -10
  26. package/src/action/pendingActions.ts +3 -4
  27. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  28. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  29. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  30. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  31. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  32. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  33. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  34. package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
  35. package/src/actionMiddlewares/undoMiddleware.ts +65 -46
  36. package/src/actionMiddlewares/utils.ts +2 -1
  37. package/src/computedTree/computedTree.ts +14 -3
  38. package/src/context/context.ts +17 -45
  39. package/src/dataModel/BaseDataModel.ts +20 -16
  40. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  41. package/src/deepChange/onDeepChange.ts +179 -24
  42. package/src/frozen/Frozen.ts +50 -33
  43. package/src/model/BaseModel.ts +4 -9
  44. package/src/model/newModel.ts +21 -24
  45. package/src/model/utils.ts +2 -2
  46. package/src/modelShared/modelClassInitializer.ts +3 -2
  47. package/src/modelShared/modelDecorator.ts +34 -14
  48. package/src/modelShared/modelInfo.ts +1 -1
  49. package/src/modelShared/prop.ts +6 -9
  50. package/src/modelShared/sharedInternalModel.ts +16 -3
  51. package/src/parent/detach.ts +1 -1
  52. package/src/parent/findChildren.ts +4 -7
  53. package/src/parent/onChildAttachedTo.ts +44 -28
  54. package/src/parent/path.ts +5 -3
  55. package/src/parent/setParent.ts +7 -0
  56. package/src/parent/walkTree.ts +21 -37
  57. package/src/patch/applyPatches.ts +69 -34
  58. package/src/patch/emitPatch.ts +142 -26
  59. package/src/patch/jsonPatch.ts +5 -0
  60. package/src/patch/patchRecorder.ts +18 -14
  61. package/src/redux/connectReduxDevTools.ts +24 -18
  62. package/src/redux/redux.ts +5 -21
  63. package/src/ref/core.ts +18 -2
  64. package/src/rootStore/attachDetach.ts +27 -16
  65. package/src/rootStore/rootStore.ts +10 -6
  66. package/src/snapshot/SnapshotOf.ts +2 -2
  67. package/src/snapshot/applySnapshot.ts +1 -1
  68. package/src/snapshot/clone.ts +1 -2
  69. package/src/snapshot/cloneTreeValue.ts +1 -2
  70. package/src/snapshot/fromModelSnapshot.ts +6 -2
  71. package/src/snapshot/internal.ts +22 -9
  72. package/src/snapshot/onSnapshot.ts +12 -1
  73. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  74. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  75. package/src/standardActions/arrayActions.ts +12 -2
  76. package/src/standardActions/objectActions.ts +3 -2
  77. package/src/transforms/ImmutableDate.ts +4 -0
  78. package/src/transforms/asMap.ts +3 -1
  79. package/src/treeUtils/deepEquals.ts +15 -13
  80. package/src/treeUtils/draft.ts +8 -0
  81. package/src/treeUtils/sandbox.ts +31 -27
  82. package/src/tweaker/core.ts +2 -4
  83. package/src/tweaker/tweak.ts +2 -1
  84. package/src/tweaker/tweakArray.ts +83 -20
  85. package/src/tweaker/tweakPlainObject.ts +76 -23
  86. package/src/tweaker/typeChecking.ts +124 -15
  87. package/src/types/TypeCheckError.ts +17 -19
  88. package/src/types/createCachedTypeCheck.ts +27 -2
  89. package/src/types/objectBased/typesDataModelData.ts +4 -2
  90. package/src/types/objectBased/typesModel.ts +4 -2
  91. package/src/types/objectBased/typesObject.ts +20 -16
  92. package/src/types/objectBased/typesRecord.ts +6 -13
  93. package/src/types/objectBased/typesRef.ts +2 -2
  94. package/src/types/primitiveBased/typesEnum.ts +4 -2
  95. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  96. package/src/types/typeCheckErrorUtils.ts +1 -0
  97. package/src/types/utility/typesCodecSupport.ts +97 -56
  98. package/src/types/utility/typesMap.ts +24 -41
  99. package/src/types/utility/typesRefinement.ts +3 -2
  100. package/src/types/utility/typesSet.ts +24 -0
  101. package/src/utils/ModelPool.ts +3 -3
  102. package/src/utils/chainFns.ts +4 -0
  103. package/src/utils/decorators.ts +1 -1
  104. package/src/utils/errorDiagnostics.ts +6 -2
  105. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  106. package/src/utils/index.ts +34 -2
  107. package/src/utils/mapUtils.ts +3 -7
  108. package/src/utils/setIfDifferent.ts +3 -2
  109. package/src/utils/setLike.ts +134 -0
  110. package/src/wrappers/ArraySet.ts +5 -9
  111. package/src/wrappers/ObjectMap.ts +15 -11
  112. package/src/wrappers/asMap.ts +30 -18
  113. package/src/wrappers/asSet.ts +36 -12
  114. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  115. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -19,7 +19,13 @@ import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../model
19
19
  import { dataToModelNode } from "../parent/core"
20
20
  import type { ParentPath } from "../parent/path"
21
21
  import { setParent } from "../parent/setParent"
22
- import { hasPatchListenersFor, InternalPatchRecorder } from "../patch/emitPatch"
22
+ import {
23
+ beginPatchEmission,
24
+ emitPatches,
25
+ endPatchEmission,
26
+ hasPatchListenersFor,
27
+ InternalPatchRecorder,
28
+ } from "../patch/emitPatch"
23
29
  import {
24
30
  flushInternalSnapshot,
25
31
  freezeInternalSnapshot,
@@ -29,13 +35,23 @@ import {
29
35
  updateInternalSnapshot,
30
36
  } from "../snapshot/internal"
31
37
  import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
38
+ import { withoutCachedTypeChecking } from "../types/createCachedTypeCheck"
32
39
  import { failure, isPlainObject, isPrimitive, setProtoProp } from "../utils"
40
+ import {
41
+ addDelayedError,
42
+ type DelayedError,
43
+ throwDelayedError,
44
+ } from "../utils/forEachWithDelayedThrow"
33
45
  import { setIfDifferent } from "../utils/setIfDifferent"
34
46
  import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
35
47
  import { TweakerPriority } from "./TweakerPriority"
36
48
  import { markAsTweakedObject } from "./treeNodeMetadata"
37
49
  import { registerTweaker, tweak } from "./tweak"
38
- import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
50
+ import {
51
+ isTypeCheckingAfterChangeEnabled,
52
+ recordTypeCheckingBatchChange,
53
+ runTypeCheckingAfterChange,
54
+ } from "./typeChecking"
39
55
 
40
56
  /**
41
57
  * @internal
@@ -65,7 +81,9 @@ export function tweakPlainObject<T extends Record<string, any>>(
65
81
  )
66
82
 
67
83
  const initialDataSnapshot =
68
- isDataObject && snapshotModelType ? takeModelInitialDataSnapshot(tweakedObj) : undefined
84
+ isDataObject && snapshotModelType !== undefined
85
+ ? takeModelInitialDataSnapshot(tweakedObj)
86
+ : undefined
69
87
  const untransformedSn: any = initialDataSnapshot ?? {}
70
88
 
71
89
  // substitute initial values by tweaked values
@@ -112,7 +130,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
112
130
  }
113
131
 
114
132
  let transformFn: SnapshotTransformFn | undefined
115
- if (snapshotModelType) {
133
+ if (snapshotModelType !== undefined) {
116
134
  untransformedSn[modelTypeKey] = snapshotModelType
117
135
 
118
136
  const modelInfo = getModelInfoForName(snapshotModelType)
@@ -167,27 +185,12 @@ function objectDidChange(change: IObjectDidChange): void {
167
185
 
168
186
  patchRecorder.reset()
169
187
 
170
- switch (change.type) {
171
- case "add":
172
- emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
173
- break
174
-
175
- case "update":
176
- emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
177
- break
178
-
179
- case "remove":
180
- emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
181
- break
182
-
183
- default:
184
- throw failure("assertion error: unsupported object change type")
185
- }
186
-
187
188
  if (runningWithoutSnapshotOrPatches) {
188
189
  return
189
190
  }
190
191
 
192
+ recordTypeCheckingBatchChange(change)
193
+
191
194
  const oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed
192
195
  const shouldEmitPatches = hasPatchListenersFor(actualNode)
193
196
  const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
@@ -215,9 +218,59 @@ function objectDidChange(change: IObjectDidChange): void {
215
218
  throw failure("assertion error: unsupported object change type")
216
219
  }
217
220
 
218
- runTypeCheckingAfterChange(obj, patchRecorder)
221
+ if (change.type !== "update" && isTypeCheckingAfterChangeEnabled()) {
222
+ // MobX notifies object observers before invalidating dependencies on missing
223
+ // keys (and removed keys in MobX 4). Cached ancestor checks may still
224
+ // describe the previous shape.
225
+ withoutCachedTypeChecking(obj, () => runTypeCheckingAfterChange(obj, patchRecorder))
226
+ } else {
227
+ runTypeCheckingAfterChange(obj, patchRecorder)
228
+ }
219
229
  updateInternalSnapshot(actualNode, mutate)
220
- patchRecorder.emit(actualNode)
230
+
231
+ // Listeners may mutate another node and reuse the shared recorder.
232
+ // Keep this change's patches before publishing the validated deep change.
233
+ const { patches, invPatches } = patchRecorder
234
+ patchRecorder.reset()
235
+
236
+ // Reserve chronological patch notifications, then finish deep-change delivery
237
+ // before publishing them. Reentrant edits append behind this mutation.
238
+ // Every step below runs even when an earlier one throws, so a failing patch recorder
239
+ // cannot keep the mutation from reaching the deep change and public patch listeners,
240
+ // and no failure is lost to a later one.
241
+ beginPatchEmission()
242
+ let delayedError: DelayedError
243
+ try {
244
+ emitPatches(actualNode, patches, invPatches)
245
+ } catch (error) {
246
+ delayedError = addDelayedError(delayedError, error)
247
+ }
248
+ try {
249
+ switch (change.type) {
250
+ case "add":
251
+ emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
252
+ break
253
+
254
+ case "update":
255
+ emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
256
+ break
257
+
258
+ case "remove":
259
+ emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
260
+ break
261
+
262
+ default:
263
+ throw failure("assertion error: unsupported object change type")
264
+ }
265
+ } catch (error) {
266
+ delayedError = addDelayedError(delayedError, error)
267
+ }
268
+ try {
269
+ endPatchEmission()
270
+ } catch (error) {
271
+ delayedError = addDelayedError(delayedError, error)
272
+ }
273
+ throwDelayedError(delayedError)
221
274
  }
222
275
 
223
276
  function objectDidChangeRemove(
@@ -1,3 +1,4 @@
1
+ import { type IArrayDidChange, type IObjectDidChange, remove, set } from "mobx"
1
2
  import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
2
3
  import type { AnyModel } from "../model/BaseModel"
3
4
  import { getModelMetadata } from "../model/getModelMetadata"
@@ -8,7 +9,7 @@ import { internalApplyPatches } from "../patch/applyPatches"
8
9
  import type { InternalPatchRecorder } from "../patch/emitPatch"
9
10
  import { internalApplySnapshot } from "../snapshot/applySnapshot"
10
11
  import { runWithoutSnapshotOrPatches } from "./core"
11
- import { isTypeCheckingAllowed } from "./withoutTypeChecking"
12
+ import { isTypeCheckingAllowed, withoutTypeChecking } from "./withoutTypeChecking"
12
13
 
13
14
  // Re-entrancy guard: when true, we are inside a type-check rollback.
14
15
  // During rollback, observe callbacks still fire, but we skip the auto
@@ -34,14 +35,15 @@ function isModelWithTypeChecker(obj: object): obj is AnyModel {
34
35
  * @internal
35
36
  *
36
37
  * Walks up parent chain from `obj`, calling `callback` for each typed model
37
- * ancestor (nearest-first / bottom-up). Stops early if the callback throws.
38
+ * including the target itself (nearest-first / bottom-up). Stops if the callback throws.
38
39
  */
39
40
  function forEachTypedModelAncestor(obj: object, callback: (model: AnyModel) => void): void {
40
41
  // obj might be a $ data object, so resolve to the model if applicable
41
42
  const start = dataToModelNode(obj)
42
43
 
43
- // If we started from a $ data object, check the model itself
44
- if (start !== obj && isModelWithTypeChecker(start)) {
44
+ // Both model-data mutations and whole-model reconciliation must validate
45
+ // the model itself before checking its ancestors.
46
+ if (isModelWithTypeChecker(start)) {
45
47
  callback(start)
46
48
  }
47
49
 
@@ -76,20 +78,19 @@ export function runTypeCheckingAfterChange(
76
78
  forEachTypedModelAncestor(obj, (model) => {
77
79
  const err = model.typeCheck()
78
80
  if (err) {
79
- // quietly apply inverse patches (do not generate patches, snapshots, actions, etc)
80
- // The re-entrancy guard prevents auto type checking during rollback:
81
- // rollback mutations trigger observe callbacks which call this function,
82
- // and with "all" scope those re-entrant checks could see partially-restored
83
- // intermediate state and attempt another rollback, causing infinite recursion.
81
+ // A rejected individual mutation has not updated its snapshot yet,
82
+ // so inverse patches must stay quiet. Snapshot reconciliation has
83
+ // already published changes: its rollback must update snapshots and
84
+ // emit compensating patches as well.
84
85
  isRollingBackTypeCheckFailure = true
85
86
  try {
86
- runWithoutSnapshotOrPatches(() => {
87
- if (patchRecorder) {
87
+ if (patchRecorder) {
88
+ runWithoutSnapshotOrPatches(() => {
88
89
  internalApplyPatches.call(obj, patchRecorder.invPatches, true)
89
- } else if (snapshotBeforeChanges) {
90
- internalApplySnapshot.call(obj, snapshotBeforeChanges)
91
- }
92
- })
90
+ })
91
+ } else if (snapshotBeforeChanges) {
92
+ internalApplySnapshot.call(obj, snapshotBeforeChanges)
93
+ }
93
94
  } finally {
94
95
  isRollingBackTypeCheckFailure = false
95
96
  }
@@ -98,3 +99,111 @@ export function runTypeCheckingAfterChange(
98
99
  })
99
100
  }
100
101
  }
102
+
103
+ interface TypeCheckingBatch {
104
+ models: Set<AnyModel>
105
+ undo: (() => void)[]
106
+ recording: boolean
107
+ /** Last recorded change target, so a run of changes to it walks ancestors once. */
108
+ previousTarget?: object
109
+ }
110
+
111
+ let typeCheckingBatch: TypeCheckingBatch | undefined
112
+
113
+ /** @internal Called only when automatic type checking is enabled. */
114
+ export function typeCheckAfterCreation(model: AnyModel): void {
115
+ if (typeCheckingBatch?.recording) {
116
+ typeCheckingBatch.models.add(model)
117
+ } else {
118
+ const error = model.typeCheck()
119
+ if (error) error.throw()
120
+ }
121
+ }
122
+
123
+ /**
124
+ * @internal
125
+ * Capture stored values before any listener can mutate them again. Restoring
126
+ * live values preserves identity and literal data, without snapshot processors
127
+ * or model defaults changing the state being restored.
128
+ */
129
+ export function recordTypeCheckingBatchChange(change: IObjectDidChange | IArrayDidChange): void {
130
+ const batch = typeCheckingBatch
131
+ if (!batch?.recording) return
132
+ if (
133
+ change.type === "splice" &&
134
+ change.addedCount === change.removedCount &&
135
+ change.added.every((value, index) => value === change.removed[index])
136
+ )
137
+ return
138
+
139
+ const target = change.object
140
+ if (target !== batch.previousTarget) {
141
+ forEachTypedModelAncestor(target, (model) => batch.models.add(model))
142
+ batch.previousTarget = target
143
+ }
144
+
145
+ if (change.type === "splice") {
146
+ const { object, index, addedCount } = change
147
+ const removed = change.removed.slice()
148
+ batch.undo.push(() => object.spliceWithArray(index, addedCount, removed))
149
+ } else if ("index" in change) {
150
+ const { object, index, oldValue } = change
151
+ batch.undo.push(() => {
152
+ object[index] = oldValue
153
+ })
154
+ } else {
155
+ const { object } = change
156
+ const key = change.name as string
157
+ if (change.type === "add") {
158
+ batch.undo.push(() => remove(object, key))
159
+ } else {
160
+ const { oldValue } = change
161
+ batch.undo.push(() => set(object, key, oldValue))
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * @internal
168
+ * Nested calls and synchronous listener edits join the same validation batch.
169
+ * Rollback publishes compensating changes, just like snapshot reconciliation.
170
+ */
171
+ export function withTypeCheckingBatch(fn: () => void): void {
172
+ if (typeCheckingBatch || !isTypeCheckingAfterChangeEnabled()) {
173
+ fn()
174
+ return
175
+ }
176
+
177
+ const batch: TypeCheckingBatch = { models: new Set(), undo: [], recording: true }
178
+ typeCheckingBatch = batch
179
+ let validating = false
180
+ try {
181
+ withoutTypeChecking(fn)
182
+ // MobX has now invalidated dependencies, including added/removed keys.
183
+ validating = true
184
+ for (const model of batch.models) {
185
+ const error = model.typeCheck()
186
+ if (error) error.throw()
187
+ }
188
+ } catch (error) {
189
+ // Only a rejected validation rolls back. Errors thrown by the batch body
190
+ // itself (an invalid patch, a throwing listener) keep the same partial
191
+ // application they produce while automatic type checking is disabled.
192
+ if (validating) {
193
+ batch.recording = false
194
+ withoutTypeChecking(() => {
195
+ for (let i = batch.undo.length - 1; i >= 0; i--) {
196
+ try {
197
+ batch.undo[i]()
198
+ } catch {
199
+ // Keep restoring after a rollback listener throws. The original
200
+ // failure takes precedence over errors from compensating changes.
201
+ }
202
+ }
203
+ })
204
+ }
205
+ throw error
206
+ } finally {
207
+ typeCheckingBatch = undefined
208
+ }
209
+ }
@@ -1,4 +1,5 @@
1
- import { fastGetRootPath } from "../parent/path"
1
+ import { dataToModelNode } from "../parent/core"
2
+ import { fastGetParent, fastGetRootPath } from "../parent/path"
2
3
  import type { Path } from "../parent/pathTypes"
3
4
  import { getSnapshot } from "../snapshot/getSnapshot"
4
5
  import { isTweakedObject } from "../tweaker/core"
@@ -10,6 +11,7 @@ import {
10
11
  } from "../utils/errorDiagnostics"
11
12
 
12
13
  export interface TypeCheckErrorData {
14
+ /** Path relative to the value being type checked. */
13
15
  path: Path
14
16
  expectedTypeName: string
15
17
  actualValue: any
@@ -71,8 +73,15 @@ export class TypeCheckError {
71
73
  */
72
74
  throw(): never {
73
75
  const diagnosticsPath = getErrorPathSnapshot()
76
+ const hasTreeParent =
77
+ isTweakedObject(this.typeCheckedValue, true) &&
78
+ fastGetParent(dataToModelNode(this.typeCheckedValue), false) !== undefined
79
+ // Attached values supply their location through the tree. Context supplies
80
+ // the location for detached values, such as models being hydrated.
74
81
  const path =
75
- diagnosticsPath && diagnosticsPath.length > 0 ? [...diagnosticsPath, ...this.path] : this.path
82
+ diagnosticsPath && diagnosticsPath.length > 0 && !hasTreeParent
83
+ ? [...diagnosticsPath, ...this.path]
84
+ : this.path
76
85
 
77
86
  throw new TypeCheckErrorFailure({
78
87
  path,
@@ -146,7 +155,7 @@ function resolveTypeCheckErrorData(data: TypeCheckErrorData): {
146
155
  const modelTrail = data.modelTrail ?? getErrorModelTrailSnapshot()
147
156
  const fullPath = resolveFullPath(data.path, data.typeCheckedValue)
148
157
  const actualValueSnapshot = isTweakedObject(data.actualValue, true)
149
- ? getSnapshot(data.actualValue)
158
+ ? getSnapshot(dataToModelNode(data.actualValue))
150
159
  : data.actualValue
151
160
 
152
161
  return {
@@ -162,22 +171,11 @@ function resolveTypeCheckErrorData(data: TypeCheckErrorData): {
162
171
  }
163
172
 
164
173
  function resolveFullPath(path: Path, typeCheckedValue: any): Path {
165
- let rootPath: Path = []
166
- if (typeCheckedValue && isTweakedObject(typeCheckedValue, true)) {
167
- rootPath = fastGetRootPath(typeCheckedValue, false).path
168
- }
169
- return rootPath.length > 0 && !pathStartsWith(path, rootPath) ? [...rootPath, ...path] : path
170
- }
171
-
172
- function pathStartsWith(path: Path, prefix: Path): boolean {
173
- if (prefix.length > path.length) {
174
- return false
175
- }
176
-
177
- for (let i = 0; i < prefix.length; i++) {
178
- if (path[i] !== prefix[i]) {
179
- return false
174
+ if (isTweakedObject(typeCheckedValue, true)) {
175
+ const rootPath = fastGetRootPath(dataToModelNode(typeCheckedValue), false).path
176
+ if (rootPath.length > 0) {
177
+ return [...rootPath, ...path]
180
178
  }
181
179
  }
182
- return true
180
+ return path
183
181
  }
@@ -1,8 +1,32 @@
1
1
  import { computed, type IComputedValue, type IObjectDidChange, observe } from "mobx"
2
+ import { dataToModelNode, modelToDataNode } from "../parent/core"
3
+ import { fastGetParent } from "../parent/path"
2
4
  import type { Path } from "../parent/pathTypes"
3
5
  import { isTweakedObject } from "../tweaker/core"
4
6
  import { TypeCheckError } from "./TypeCheckError"
5
7
 
8
+ let uncachedTypeCheckObjects: Set<object> | undefined
9
+
10
+ /** @internal */
11
+ export function withoutCachedTypeChecking(obj: object, fn: () => void): void {
12
+ const previous = uncachedTypeCheckObjects
13
+ const affected = new Set(previous)
14
+ // Only these containers can retain a cached result for the old shape.
15
+ // Model schema checks run on $ data, while refinements can check the model.
16
+ let current: object | undefined = dataToModelNode(obj)
17
+ while (current) {
18
+ affected.add(current)
19
+ affected.add(modelToDataNode(current))
20
+ current = fastGetParent(current, false)
21
+ }
22
+ uncachedTypeCheckObjects = affected
23
+ try {
24
+ fn()
25
+ } finally {
26
+ uncachedTypeCheckObjects = previous
27
+ }
28
+ }
29
+
6
30
  const emptyPath: Path = []
7
31
 
8
32
  function contextualizeCachedError(
@@ -16,6 +40,7 @@ function contextualizeCachedError(
16
40
 
17
41
  return new TypeCheckError({
18
42
  path: [...path, ...error.path],
43
+ modelTrail: error.modelTrail,
19
44
  expectedTypeName: error.expectedTypeName,
20
45
  actualValue: error.actualValue,
21
46
  typeCheckedValue,
@@ -52,7 +77,7 @@ function createAdaptivePerEntryCachedCheck<K>(
52
77
  iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
53
78
 
54
79
  return (value, path, typeCheckedValue) => {
55
- if (!isTweakedObject(value, true)) {
80
+ if (uncachedTypeCheckObjects?.has(value) || !isTweakedObject(value, true)) {
56
81
  return checkAllEntries(value, path, typeCheckedValue)
57
82
  }
58
83
 
@@ -142,7 +167,7 @@ export function createWholeContainerCachedCheck(
142
167
  const cache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
143
168
 
144
169
  return (value, path, typeCheckedValue) => {
145
- if (!isTweakedObject(value, true)) {
170
+ if (uncachedTypeCheckObjects?.has(value) || !isTweakedObject(value, true)) {
146
171
  return check(value, path, typeCheckedValue)
147
172
  }
148
173
 
@@ -10,7 +10,7 @@ import type {
10
10
  import { modelInfoByClass } from "../../modelShared/modelInfo"
11
11
  import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo"
12
12
  import { noDefaultValue } from "../../modelShared/prop"
13
- import { failure, lazy } from "../../utils"
13
+ import { failure, lazy, setProtoProp } from "../../utils"
14
14
  import { getTypeInfo } from "../getTypeInfo"
15
15
  import {
16
16
  registerStandardTypeResolver,
@@ -168,12 +168,14 @@ export class DataModelDataTypeInfo extends TypeInfo {
168
168
  hasDefault = true
169
169
  }
170
170
 
171
- propTypes[propName] = {
171
+ const propInfo = {
172
172
  type,
173
173
  typeInfo,
174
174
  hasDefault,
175
175
  default: defaultValue,
176
176
  }
177
+ if (propName === "__proto__") setProtoProp(propTypes, propInfo)
178
+ else propTypes[propName] = propInfo
177
179
  })
178
180
  return propTypes
179
181
  })
@@ -7,7 +7,7 @@ import type { ModelClass } from "../../modelShared/BaseModelShared"
7
7
  import { modelInfoByClass } from "../../modelShared/modelInfo"
8
8
  import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo"
9
9
  import { noDefaultValue } from "../../modelShared/prop"
10
- import { isObject, lazy } from "../../utils"
10
+ import { isObject, lazy, setProtoProp } from "../../utils"
11
11
  import { getTypeInfo } from "../getTypeInfo"
12
12
  import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker"
13
13
  import type { AnyStandardType, ModelType } from "../schemas"
@@ -178,12 +178,14 @@ export class ModelTypeInfo extends TypeInfo {
178
178
  hasDefault = true
179
179
  }
180
180
 
181
- propTypes[propName] = {
181
+ const propInfo = {
182
182
  type,
183
183
  typeInfo,
184
184
  hasDefault,
185
185
  default: defaultValue,
186
186
  }
187
+ if (propName === "__proto__") setProtoProp(propTypes, propInfo)
188
+ else propTypes[propName] = propInfo
187
189
  })
188
190
  return propTypes
189
191
  })
@@ -2,7 +2,16 @@ import { isObservableObject, keys } from "mobx"
2
2
  import type { O } from "ts-toolbelt"
3
3
  import { Frozen } from "../../frozen/Frozen"
4
4
  import type { Path } from "../../parent/pathTypes"
5
- import { assertIsFunction, assertIsObject, getMobxVersion, isObject, lazy } from "../../utils"
5
+ import {
6
+ assertIsFunction,
7
+ assertIsObject,
8
+ copyOwnEnumerableProps,
9
+ getMobxVersion,
10
+ isArray,
11
+ isObject,
12
+ lazy,
13
+ setProtoProp,
14
+ } from "../../utils"
6
15
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
7
16
  import { createIndexedPerEntryCachedCheck } from "../createCachedTypeCheck"
8
17
  import { getTypeInfo } from "../getTypeInfo"
@@ -106,27 +115,20 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
106
115
  obj: Record<string, unknown>,
107
116
  processors: ReadonlyArray<SnapshotProcessor | undefined>
108
117
  ) => {
109
- const newObj: typeof obj = {}
110
-
111
118
  // note: we allow excess properties when checking objects
112
- const keys = Object.keys(obj)
113
- for (let i = 0; i < keys.length; i++) {
114
- const k = keys[i]
119
+ return copyOwnEnumerableProps({}, obj, (value, k) => {
115
120
  const schemaEntry = schemaEntryByPropName[k]
116
121
  if (schemaEntry) {
117
122
  const processor = processors[schemaEntry.processorIndex]
118
- newObj[k] = processor ? withErrorPathSegment(k, () => processor(obj[k])) : obj[k]
119
- } else {
120
- // unknown prop, copy as is
121
- newObj[k] = obj[k]
123
+ return processor ? withErrorPathSegment(k, () => processor(value)) : value
122
124
  }
123
- }
124
-
125
- return newObj
125
+ // unknown prop, copy as is
126
+ return value
127
+ })
126
128
  }
127
129
 
128
130
  const checkObjectType = (obj: any, path: Path, typeCheckedValue: any) => {
129
- if (!isObject(obj) || (frozen && !(obj instanceof Frozen))) {
131
+ if (!isObject(obj) || isArray(obj) || (frozen && !(obj instanceof Frozen))) {
130
132
  return new TypeCheckError({
131
133
  path,
132
134
  expectedTypeName: getTypeName(thisTc),
@@ -187,7 +189,7 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
187
189
  typeInfoGen,
188
190
 
189
191
  (obj) => {
190
- if (!isObject(obj)) {
192
+ if (!isObject(obj) || isArray(obj)) {
191
193
  return null
192
194
  }
193
195
 
@@ -278,7 +280,9 @@ export class ObjectTypeInfo extends TypeInfo {
278
280
  const propTypes: O.Writable<ObjectTypeInfoProps> = {}
279
281
  Object.keys(objSchema).forEach((propName) => {
280
282
  const type = resolveStandardType(objSchema[propName])
281
- propTypes[propName] = { type, typeInfo: getTypeInfo(type) }
283
+ const propInfo = { type, typeInfo: getTypeInfo(type) }
284
+ if (propName === "__proto__") setProtoProp(propTypes, propInfo)
285
+ else propTypes[propName] = propInfo
282
286
  })
283
287
  return propTypes
284
288
  })
@@ -1,6 +1,6 @@
1
1
  import { isObservableObject, keys } from "mobx"
2
2
  import type { Path } from "../../parent/pathTypes"
3
- import { failure, isObject } from "../../utils"
3
+ import { copyOwnEnumerableProps, failure, isArray, isObject } from "../../utils"
4
4
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
5
5
  import { createAdaptiveRecordCachedCheck } from "../createCachedTypeCheck"
6
6
  import { getTypeInfo } from "../getTypeInfo"
@@ -48,16 +48,9 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
48
48
  return obj
49
49
  }
50
50
 
51
- const newObj: typeof obj = {}
52
-
53
- const keys = Object.keys(obj)
54
- for (let i = 0; i < keys.length; i++) {
55
- const k = keys[i]
56
- const v = withErrorPathSegment(k, () => processor(obj[k]))
57
- newObj[k] = v
58
- }
59
-
60
- return newObj
51
+ return copyOwnEnumerableProps({}, obj, (value, key) =>
52
+ withErrorPathSegment(key, () => processor(value))
53
+ )
61
54
  }
62
55
 
63
56
  const iterateRecordEntries = (
@@ -97,7 +90,7 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
97
90
  TypeCheckerBaseType.Object,
98
91
 
99
92
  (obj, path, typeCheckedValue) => {
100
- if (!isObject(obj)) {
93
+ if (!isObject(obj) || isArray(obj)) {
101
94
  return new TypeCheckError({
102
95
  path,
103
96
  expectedTypeName: getTypeName(thisTc),
@@ -117,7 +110,7 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
117
110
  typeInfoGen,
118
111
 
119
112
  (obj) => {
120
- if (!isObject(obj)) {
113
+ if (!isObject(obj) || isArray(obj)) {
121
114
  return null
122
115
  }
123
116
 
@@ -1,6 +1,6 @@
1
1
  import { modelTypeKey } from "../../model/metadata"
2
2
  import { modelInfoByClass } from "../../modelShared/modelInfo"
3
- import { Ref, type RefConstructor } from "../../ref/Ref"
3
+ import type { Ref, RefConstructor } from "../../ref/Ref"
4
4
  import { isObject } from "../../utils"
5
5
  import { typesString } from "../primitiveBased/typesPrimitive"
6
6
  import { resolveTypeChecker } from "../resolveTypeChecker"
@@ -36,7 +36,7 @@ export function typesRef<O extends object>(refConstructor: RefConstructor<O>): M
36
36
  TypeCheckerBaseType.Object,
37
37
 
38
38
  (value, path, typeCheckedValue) => {
39
- if (!(value instanceof Ref)) {
39
+ if (!(value instanceof refConstructor.refClass)) {
40
40
  return new TypeCheckError({
41
41
  path,
42
42
  expectedTypeName: typeName,
@@ -1,4 +1,4 @@
1
- import { assertIsObject } from "../../utils"
1
+ import { assertIsObject, isEqualOrBothNaN } from "../../utils"
2
2
  import type { IdentityType } from "../schemas"
3
3
  import { typesOr } from "../utility/typesOr"
4
4
  import { typesLiteral } from "./typesPrimitive"
@@ -17,6 +17,7 @@ export interface EnumLike {
17
17
  */
18
18
  export function enumValues(e: EnumLike): (string | number)[] {
19
19
  const vals: (string | number)[] = []
20
+ const seen = new Set<string | number>()
20
21
  for (const k of Object.keys(e)) {
21
22
  const v = e[k]
22
23
  // we have to do this since TS does something weird
@@ -24,7 +25,8 @@ export function enumValues(e: EnumLike): (string | number)[] {
24
25
  // Hi = 0 -> { Hi: 0, 0: "Hi" }
25
26
  // and SWC currently generates enum code inconsistent with TS/Babel
26
27
  // https://github.com/swc-project/swc/issues/3711
27
- if (!vals.includes(v) && ((typeof v !== "string" && v !== +k) || e[v] !== +k)) {
28
+ if (!seen.has(v) && ((typeof v !== "string" && v !== +k) || !isEqualOrBothNaN(e[v], +k))) {
29
+ seen.add(v)
28
30
  vals.push(v)
29
31
  }
30
32
  }