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.
- package/CHANGELOG.md +123 -0
- package/dist/mobx-keystone.esm.js +1478 -952
- package/dist/mobx-keystone.esm.mjs +1478 -952
- package/dist/mobx-keystone.umd.js +1483 -952
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/patch/applyPatches.d.ts +2 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
- package/dist/types/utils/index.d.ts +13 -0
- package/dist/types/utils/mapUtils.d.ts +2 -2
- package/dist/types/utils/setLike.d.ts +1 -0
- package/package.json +1 -1
- package/src/action/applyAction.ts +1 -1
- package/src/action/middleware.ts +17 -36
- package/src/action/modelFlow.ts +4 -5
- package/src/action/mutationBatch.ts +10 -10
- package/src/action/pendingActions.ts +3 -4
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
- package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
- package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
- package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
- package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
- package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
- package/src/actionMiddlewares/undoMiddleware.ts +65 -46
- package/src/actionMiddlewares/utils.ts +2 -1
- package/src/computedTree/computedTree.ts +14 -3
- package/src/context/context.ts +17 -45
- package/src/dataModel/BaseDataModel.ts +20 -16
- package/src/dataModel/dataModelInstanceCache.ts +8 -0
- package/src/deepChange/onDeepChange.ts +179 -24
- package/src/frozen/Frozen.ts +50 -33
- package/src/model/BaseModel.ts +4 -9
- package/src/model/newModel.ts +21 -24
- package/src/model/utils.ts +2 -2
- package/src/modelShared/modelClassInitializer.ts +3 -2
- package/src/modelShared/modelDecorator.ts +34 -14
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +44 -28
- package/src/parent/path.ts +5 -3
- package/src/parent/setParent.ts +7 -0
- package/src/parent/walkTree.ts +21 -37
- package/src/patch/applyPatches.ts +69 -34
- package/src/patch/emitPatch.ts +142 -26
- package/src/patch/jsonPatch.ts +5 -0
- package/src/patch/patchRecorder.ts +18 -14
- package/src/redux/connectReduxDevTools.ts +24 -18
- package/src/redux/redux.ts +5 -21
- package/src/ref/core.ts +18 -2
- package/src/rootStore/attachDetach.ts +27 -16
- package/src/rootStore/rootStore.ts +10 -6
- package/src/snapshot/SnapshotOf.ts +2 -2
- package/src/snapshot/applySnapshot.ts +1 -1
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/internal.ts +22 -9
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +6 -6
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/standardActions/arrayActions.ts +12 -2
- package/src/standardActions/objectActions.ts +3 -2
- package/src/transforms/ImmutableDate.ts +4 -0
- package/src/transforms/asMap.ts +3 -1
- package/src/treeUtils/deepEquals.ts +15 -13
- package/src/treeUtils/draft.ts +8 -0
- package/src/treeUtils/sandbox.ts +31 -27
- package/src/tweaker/core.ts +2 -4
- package/src/tweaker/tweak.ts +2 -1
- package/src/tweaker/tweakArray.ts +83 -20
- package/src/tweaker/tweakPlainObject.ts +76 -23
- package/src/tweaker/typeChecking.ts +124 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/createCachedTypeCheck.ts +27 -2
- package/src/types/objectBased/typesDataModelData.ts +4 -2
- package/src/types/objectBased/typesModel.ts +4 -2
- package/src/types/objectBased/typesObject.ts +20 -16
- package/src/types/objectBased/typesRecord.ts +6 -13
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesEnum.ts +4 -2
- package/src/types/primitiveBased/typesPrimitive.ts +9 -4
- package/src/types/typeCheckErrorUtils.ts +1 -0
- package/src/types/utility/typesCodecSupport.ts +97 -56
- package/src/types/utility/typesMap.ts +24 -41
- package/src/types/utility/typesRefinement.ts +3 -2
- package/src/types/utility/typesSet.ts +24 -0
- package/src/utils/ModelPool.ts +3 -3
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/decorators.ts +1 -1
- package/src/utils/errorDiagnostics.ts +6 -2
- package/src/utils/forEachWithDelayedThrow.ts +83 -0
- package/src/utils/index.ts +34 -2
- package/src/utils/mapUtils.ts +3 -7
- package/src/utils/setIfDifferent.ts +3 -2
- package/src/utils/setLike.ts +134 -0
- package/src/wrappers/ArraySet.ts +5 -9
- package/src/wrappers/ObjectMap.ts +15 -11
- package/src/wrappers/asMap.ts +30 -18
- package/src/wrappers/asSet.ts +36 -12
- package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
- 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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
44
|
-
|
|
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
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
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
|
-
|
|
87
|
-
|
|
87
|
+
if (patchRecorder) {
|
|
88
|
+
runWithoutSnapshotOrPatches(() => {
|
|
88
89
|
internalApplyPatches.call(obj, patchRecorder.invPatches, true)
|
|
89
|
-
}
|
|
90
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
rootPath
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
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
|
|
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 (!
|
|
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
|
}
|