mobx-keystone 2.0.0 → 2.2.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 +33 -0
- package/README.md +1 -1
- package/dist/mobx-keystone.esm.js +2561 -1214
- package/dist/mobx-keystone.esm.mjs +2561 -1214
- package/dist/mobx-keystone.umd.js +2566 -1220
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
- package/dist/types/modelShared/modelDecorator.d.ts +5 -2
- package/dist/types/modelShared/prop.d.ts +42 -29
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +2 -1
- package/dist/types/ref/Ref.d.ts +1 -4
- package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
- package/dist/types/ref/nodesById.d.ts +1 -0
- package/dist/types/ref/refIdIndex.d.ts +1 -0
- package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
- package/dist/types/snapshot/applySnapshot.d.ts +2 -1
- package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
- package/dist/types/snapshot/getSnapshot.d.ts +1 -1
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/schemas.d.ts +41 -37
- package/dist/types/types/tProp.d.ts +12 -6
- package/dist/types/types/types.d.ts +6 -6
- package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
- package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
- package/dist/types/utils/ModelPool.d.ts +7 -2
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/types.d.ts +6 -0
- package/dist/types/wrappers/ArraySet.d.ts +1 -0
- package/package.json +5 -3
- package/src/action/modelFlow.ts +35 -66
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
- package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
- package/src/actionMiddlewares/undoMiddleware.ts +63 -35
- package/src/context/context.ts +15 -36
- package/src/dataModel/utils.ts +5 -28
- package/src/deepChange/onDeepChange.ts +44 -126
- package/src/model/BaseModel.ts +7 -5
- package/src/model/utils.ts +5 -10
- package/src/modelShared/modelDecorator.ts +16 -30
- package/src/modelShared/prop.ts +115 -111
- package/src/modelShared/sharedInternalModel.ts +2 -2
- package/src/parent/arrayPaths.ts +239 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/onChildAttachedTo.ts +130 -44
- package/src/parent/path.ts +10 -12
- package/src/parent/setParent.ts +27 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +0 -73
- package/src/patch/applyPatches.ts +31 -26
- package/src/patch/patchRecorder.ts +13 -28
- package/src/ref/core.ts +69 -46
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/rootStore.ts +1 -3
- package/src/snapshot/SnapshotOf.ts +24 -48
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +12 -9
- package/src/snapshot/getSnapshot.ts +1 -1
- package/src/snapshot/internal.ts +67 -12
- package/src/snapshot/reconcileModelSnapshot.ts +5 -3
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
- package/src/treeUtils/draft.ts +6 -1
- package/src/treeUtils/sandbox.ts +10 -7
- package/src/tweaker/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -20
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +181 -18
- package/src/tweaker/tweakArray.ts +127 -77
- package/src/tweaker/tweakPlainObject.ts +65 -46
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +5 -104
- package/src/types/arrayBased/typesArray.ts +23 -30
- package/src/types/arrayBased/typesTuple.ts +16 -25
- package/src/types/createCachedTypeCheck.ts +509 -94
- package/src/types/index.ts +1 -0
- package/src/types/objectBased/typesDataModelData.ts +7 -8
- package/src/types/objectBased/typesObject.ts +11 -15
- package/src/types/objectBased/typesRecord.ts +12 -19
- package/src/types/schemas.ts +59 -71
- package/src/types/tProp.ts +29 -29
- package/src/types/utility/typeToStoredData.ts +22 -21
- package/src/types/utility/typesCodecCore.ts +5 -0
- package/src/types/utility/typesCodecSupport.ts +3 -3
- package/src/types/utility/typesMap.ts +43 -51
- package/src/types/utility/typesRefinement.ts +7 -8
- package/src/types/utility/typesSkipCheck.ts +7 -8
- package/src/types/utility/typesTag.ts +7 -8
- package/src/utils/ModelPool.ts +76 -37
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +5 -5
- package/src/utils/index.ts +38 -20
- package/src/utils/types.ts +5 -0
- package/src/wrappers/ArraySet.ts +27 -14
- package/src/wrappers/ObjectMap.ts +1 -6
- package/src/wrappers/asMap.ts +1 -21
- package/src/wrappers/asSet.ts +0 -8
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
emitObjectUpdateDeepChange,
|
|
14
14
|
} from "../deepChange/onDeepChange"
|
|
15
15
|
import type { AnyModel } from "../model/BaseModel"
|
|
16
|
+
import { getModelIdPropertyName } from "../model/getModelMetadata"
|
|
16
17
|
import { modelTypeKey } from "../model/metadata"
|
|
17
18
|
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
18
19
|
import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
|
|
19
20
|
import { dataToModelNode } from "../parent/core"
|
|
21
|
+
import { onModelIdChanged } from "../parent/coreObjectChildren"
|
|
20
22
|
import type { ParentPath } from "../parent/path"
|
|
21
23
|
import { setParent } from "../parent/setParent"
|
|
22
24
|
import {
|
|
@@ -36,22 +38,16 @@ import {
|
|
|
36
38
|
} from "../snapshot/internal"
|
|
37
39
|
import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
|
|
38
40
|
import { withoutCachedTypeChecking } from "../types/createCachedTypeCheck"
|
|
39
|
-
import { failure, isPlainObject, isPrimitive, setProtoProp } from "../utils"
|
|
40
|
-
import {
|
|
41
|
-
addDelayedError,
|
|
42
|
-
type DelayedError,
|
|
43
|
-
throwDelayedError,
|
|
44
|
-
} from "../utils/forEachWithDelayedThrow"
|
|
41
|
+
import { failure, isPlainObject, isPrimitive, nodeObservableOptions, setProtoProp } from "../utils"
|
|
42
|
+
import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
|
|
45
43
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
46
|
-
import {
|
|
44
|
+
import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
|
|
45
|
+
import { activateHandlers, runningWithoutSnapshotOrPatches } from "./core"
|
|
47
46
|
import { TweakerPriority } from "./TweakerPriority"
|
|
48
|
-
import { markAsTweakedObject } from "./treeNodeMetadata"
|
|
49
|
-
import { registerTweaker, tweak } from "./tweak"
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
recordTypeCheckingBatchChange,
|
|
53
|
-
runTypeCheckingAfterChange,
|
|
54
|
-
} from "./typeChecking"
|
|
47
|
+
import { markAsTweakedObject, treeNodeMetadata } from "./treeNodeMetadata"
|
|
48
|
+
import { assertCanAttachValue, registerTweaker, tweak } from "./tweak"
|
|
49
|
+
import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
|
|
50
|
+
import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
|
|
55
51
|
|
|
56
52
|
/**
|
|
57
53
|
* @internal
|
|
@@ -67,7 +63,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
|
|
|
67
63
|
const originalIsObservableObject = isObservableObject(originalObj)
|
|
68
64
|
const tweakedObj = originalIsObservableObject
|
|
69
65
|
? originalObj
|
|
70
|
-
: observable.object({}, undefined,
|
|
66
|
+
: observable.object({}, undefined, nodeObservableOptions)
|
|
71
67
|
|
|
72
68
|
// Mark it as tweaked before processing children. The cleanup functions are
|
|
73
69
|
// installed after the MobX handlers have been registered.
|
|
@@ -150,38 +146,57 @@ export function tweakPlainObject<T extends Record<string, any>>(
|
|
|
150
146
|
transformFn
|
|
151
147
|
)
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
if (activateHandlers(tweakedObj)) {
|
|
150
|
+
intercept(tweakedObj, interceptObjectMutation)
|
|
151
|
+
observe(tweakedObj, objectDidChange)
|
|
152
|
+
}
|
|
156
153
|
|
|
157
154
|
return tweakedObj as any
|
|
158
155
|
}
|
|
159
156
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
function mutateSet(
|
|
158
|
+
k: PropertyKey,
|
|
159
|
+
v: unknown,
|
|
160
|
+
sn: Record<PropertyKey, unknown>
|
|
161
|
+
): readonly unknown[] {
|
|
165
162
|
if (k === "__proto__") {
|
|
166
163
|
setProtoProp(sn, v)
|
|
167
164
|
} else {
|
|
168
165
|
sn[k] = v
|
|
169
166
|
}
|
|
167
|
+
return [v]
|
|
170
168
|
}
|
|
171
169
|
|
|
172
|
-
|
|
170
|
+
const noValues: readonly unknown[] = []
|
|
171
|
+
|
|
172
|
+
function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>): readonly unknown[] {
|
|
173
173
|
delete sn[k]
|
|
174
|
+
return noValues
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
const patchRecorder = new InternalPatchRecorder()
|
|
177
178
|
|
|
178
179
|
function objectDidChange(change: IObjectDidChange): void {
|
|
180
|
+
const obj = change.object
|
|
181
|
+
const metadata = treeNodeMetadata.get(obj)
|
|
182
|
+
if (!metadata?.handlersActive) {
|
|
183
|
+
// idle handler of an untweaked object
|
|
184
|
+
return
|
|
185
|
+
}
|
|
179
186
|
if (typeof change.name !== "string") {
|
|
180
187
|
throw failure("change.name is not a string")
|
|
181
188
|
}
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
notifyTweakedChangeListeners(change)
|
|
191
|
+
|
|
192
|
+
const actualNode = metadata.dataObjectParent ?? obj
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
actualNode !== obj &&
|
|
196
|
+
change.name === getModelIdPropertyName(actualNode.constructor as ModelClass<AnyModel>)
|
|
197
|
+
) {
|
|
198
|
+
onModelIdChanged(actualNode)
|
|
199
|
+
}
|
|
185
200
|
|
|
186
201
|
patchRecorder.reset()
|
|
187
202
|
|
|
@@ -189,13 +204,11 @@ function objectDidChange(change: IObjectDidChange): void {
|
|
|
189
204
|
return
|
|
190
205
|
}
|
|
191
206
|
|
|
192
|
-
recordTypeCheckingBatchChange(change)
|
|
193
|
-
|
|
194
207
|
const oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed
|
|
195
208
|
const shouldEmitPatches = hasPatchListenersFor(actualNode)
|
|
196
209
|
const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
|
|
197
210
|
|
|
198
|
-
let mutate: ((sn: any) =>
|
|
211
|
+
let mutate: ((sn: any) => readonly unknown[]) | undefined
|
|
199
212
|
switch (change.type) {
|
|
200
213
|
case "add":
|
|
201
214
|
case "update":
|
|
@@ -226,6 +239,8 @@ function objectDidChange(change: IObjectDidChange): void {
|
|
|
226
239
|
} else {
|
|
227
240
|
runTypeCheckingAfterChange(obj, patchRecorder)
|
|
228
241
|
}
|
|
242
|
+
// (after type checking, which undoes a rejected change, but before listeners)
|
|
243
|
+
recordChangeForRollback(change)
|
|
229
244
|
updateInternalSnapshot(actualNode, mutate)
|
|
230
245
|
|
|
231
246
|
// Listeners may mutate another node and reuse the shared recorder.
|
|
@@ -270,7 +285,7 @@ function objectDidChange(change: IObjectDidChange): void {
|
|
|
270
285
|
} catch (error) {
|
|
271
286
|
delayedError = addDelayedError(delayedError, error)
|
|
272
287
|
}
|
|
273
|
-
|
|
288
|
+
throwDelayedListenerError(delayedError)
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
function objectDidChangeRemove(
|
|
@@ -280,16 +295,14 @@ function objectDidChangeRemove(
|
|
|
280
295
|
shouldBuildInversePatches: boolean
|
|
281
296
|
) {
|
|
282
297
|
const k = change.name
|
|
283
|
-
const oldVal = oldUntransformedSn[k]
|
|
284
298
|
const mutate = mutateDelete.bind(undefined, k)
|
|
285
299
|
|
|
286
|
-
if (
|
|
300
|
+
if (shouldBuildInversePatches) {
|
|
287
301
|
const path = [k as string]
|
|
288
302
|
const patches = shouldEmitPatches ? [{ op: "remove" as const, path }] : undefined
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
patchRecorder.record(patches, invPatches)
|
|
303
|
+
patchRecorder.record(patches, [
|
|
304
|
+
{ op: "add" as const, path, value: freezeInternalSnapshot(oldUntransformedSn[k]) },
|
|
305
|
+
])
|
|
293
306
|
}
|
|
294
307
|
return mutate
|
|
295
308
|
}
|
|
@@ -303,8 +316,6 @@ function objectDidChangeAddOrUpdate(
|
|
|
303
316
|
const k = change.name
|
|
304
317
|
const val = change.newValue
|
|
305
318
|
|
|
306
|
-
const oldVal = oldUntransformedSn[k]
|
|
307
|
-
|
|
308
319
|
let newVal: any
|
|
309
320
|
if (isPrimitive(val)) {
|
|
310
321
|
newVal = val
|
|
@@ -315,29 +326,34 @@ function objectDidChangeAddOrUpdate(
|
|
|
315
326
|
|
|
316
327
|
const mutate = mutateSet.bind(undefined, k, newVal)
|
|
317
328
|
|
|
318
|
-
if (
|
|
329
|
+
if (shouldBuildInversePatches) {
|
|
319
330
|
const path = [k as string]
|
|
320
331
|
if (change.type === "add") {
|
|
321
332
|
const patches = shouldEmitPatches
|
|
322
333
|
? [{ op: "add" as const, path, value: freezeInternalSnapshot(newVal) }]
|
|
323
334
|
: undefined
|
|
324
|
-
|
|
325
|
-
patchRecorder.record(patches, invPatches)
|
|
335
|
+
patchRecorder.record(patches, [{ op: "remove" as const, path }])
|
|
326
336
|
} else {
|
|
327
337
|
const patches = shouldEmitPatches
|
|
328
338
|
? [{ op: "replace" as const, path, value: freezeInternalSnapshot(newVal) }]
|
|
329
339
|
: undefined
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
patchRecorder.record(patches, invPatches)
|
|
340
|
+
patchRecorder.record(patches, [
|
|
341
|
+
{ op: "replace" as const, path, value: freezeInternalSnapshot(oldUntransformedSn[k]) },
|
|
342
|
+
])
|
|
334
343
|
}
|
|
335
344
|
}
|
|
336
345
|
|
|
337
346
|
return mutate
|
|
338
347
|
}
|
|
339
348
|
|
|
349
|
+
const noneDetached = () => false
|
|
350
|
+
|
|
340
351
|
function interceptObjectMutation(change: IObjectWillChange) {
|
|
352
|
+
const metadata = treeNodeMetadata.get(change.object)
|
|
353
|
+
if (!metadata?.handlersActive) {
|
|
354
|
+
// idle handler of an untweaked object
|
|
355
|
+
return change
|
|
356
|
+
}
|
|
341
357
|
assertCanWrite()
|
|
342
358
|
|
|
343
359
|
if (typeof change.name === "symbol") {
|
|
@@ -347,10 +363,11 @@ function interceptObjectMutation(change: IObjectWillChange) {
|
|
|
347
363
|
// This must happen before detach/reparent work below. It keeps the old value
|
|
348
364
|
// available for inverse patches and folds a departing dirty child into its
|
|
349
365
|
// parent snapshot while its parent path still exists.
|
|
350
|
-
flushInternalSnapshot(
|
|
366
|
+
flushInternalSnapshot(metadata.dataObjectParent ?? change.object, false)
|
|
351
367
|
|
|
352
368
|
switch (change.type) {
|
|
353
369
|
case "add":
|
|
370
|
+
assertCanAttachValue(change.newValue, noneDetached)
|
|
354
371
|
change.newValue = tweak(change.newValue, {
|
|
355
372
|
parent: change.object,
|
|
356
373
|
path: String(change.name),
|
|
@@ -366,6 +383,8 @@ function interceptObjectMutation(change: IObjectWillChange) {
|
|
|
366
383
|
const oldVal = change.object[change.name]
|
|
367
384
|
const newVal = change.newValue
|
|
368
385
|
if (newVal !== oldVal) {
|
|
386
|
+
// (first, so a value that cannot be attached leaves the old one in place)
|
|
387
|
+
assertCanAttachValue(newVal, (node) => node === oldVal)
|
|
369
388
|
tweak(oldVal, undefined)
|
|
370
389
|
|
|
371
390
|
change.newValue = tweak(newVal, {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { IArrayDidChange, IObjectDidChange } from "mobx"
|
|
2
|
+
|
|
3
|
+
type TweakedChange = IArrayDidChange | IObjectDidChange
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface TweakedChangeListener<C extends TweakedChange> {
|
|
9
|
+
applyChange(change: C): void
|
|
10
|
+
/**
|
|
11
|
+
* Called when the target is tweaked again after being untweaked, since the
|
|
12
|
+
* changes made in between were not received.
|
|
13
|
+
*/
|
|
14
|
+
resync(): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const changeListeners = new WeakMap<object, TweakedChangeListener<any>[]>()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Listens to the changes of a tweaked array or plain object, in order, before
|
|
21
|
+
* anything else reacts to them (including the type checks after the change).
|
|
22
|
+
* Unlike `observe`, changes made while reacting to a change (such as a
|
|
23
|
+
* type-check rollback) are received after it.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function addTweakedChangeListener<T extends object>(
|
|
28
|
+
target: T,
|
|
29
|
+
listener: TweakedChangeListener<T extends readonly unknown[] ? IArrayDidChange : IObjectDidChange>
|
|
30
|
+
): void {
|
|
31
|
+
let listeners = changeListeners.get(target)
|
|
32
|
+
if (!listeners) {
|
|
33
|
+
listeners = []
|
|
34
|
+
changeListeners.set(target, listeners)
|
|
35
|
+
}
|
|
36
|
+
listeners.push(listener)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function notifyTweakedChangeListeners(change: TweakedChange): void {
|
|
43
|
+
const listeners = changeListeners.get(change.object)
|
|
44
|
+
if (listeners) {
|
|
45
|
+
for (const listener of listeners) {
|
|
46
|
+
listener.applyChange(change)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export function resyncTweakedChangeListeners(target: object): void {
|
|
55
|
+
const listeners = changeListeners.get(target)
|
|
56
|
+
if (listeners) {
|
|
57
|
+
for (const listener of listeners) {
|
|
58
|
+
listener.resync()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type IArrayDidChange, type IObjectDidChange, remove, set } from "mobx"
|
|
2
1
|
import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
|
|
3
2
|
import type { AnyModel } from "../model/BaseModel"
|
|
4
3
|
import { getModelMetadata } from "../model/getModelMetadata"
|
|
@@ -8,8 +7,9 @@ import { fastGetParent } from "../parent/path"
|
|
|
8
7
|
import { internalApplyPatches } from "../patch/applyPatches"
|
|
9
8
|
import type { InternalPatchRecorder } from "../patch/emitPatch"
|
|
10
9
|
import { internalApplySnapshot } from "../snapshot/applySnapshot"
|
|
10
|
+
import { deferModelTypeCheckToBatch } from "./changeRollback"
|
|
11
11
|
import { runWithoutSnapshotOrPatches } from "./core"
|
|
12
|
-
import { isTypeCheckingAllowed
|
|
12
|
+
import { isTypeCheckingAllowed } from "./withoutTypeChecking"
|
|
13
13
|
|
|
14
14
|
// Re-entrancy guard: when true, we are inside a type-check rollback.
|
|
15
15
|
// During rollback, observe callbacks still fire, but we skip the auto
|
|
@@ -37,7 +37,7 @@ function isModelWithTypeChecker(obj: object): obj is AnyModel {
|
|
|
37
37
|
* Walks up parent chain from `obj`, calling `callback` for each typed model
|
|
38
38
|
* including the target itself (nearest-first / bottom-up). Stops if the callback throws.
|
|
39
39
|
*/
|
|
40
|
-
function forEachTypedModelAncestor(obj: object, callback: (model: AnyModel) => void): void {
|
|
40
|
+
export function forEachTypedModelAncestor(obj: object, callback: (model: AnyModel) => void): void {
|
|
41
41
|
// obj might be a $ data object, so resolve to the model if applicable
|
|
42
42
|
const start = dataToModelNode(obj)
|
|
43
43
|
|
|
@@ -100,110 +100,11 @@ export function runTypeCheckingAfterChange(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
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
103
|
/** @internal Called only when automatic type checking is enabled. */
|
|
114
104
|
export function typeCheckAfterCreation(model: AnyModel): void {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} else {
|
|
105
|
+
// a change batch validates it once it completes
|
|
106
|
+
if (!deferModelTypeCheckToBatch(model)) {
|
|
118
107
|
const error = model.typeCheck()
|
|
119
108
|
if (error) error.throw()
|
|
120
109
|
}
|
|
121
110
|
}
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import type { Path } from "../../parent/pathTypes"
|
|
2
2
|
import { isArray } from "../../utils"
|
|
3
3
|
import { withErrorPathSegment } from "../../utils/errorDiagnostics"
|
|
4
|
-
import {
|
|
4
|
+
import { createChunkedArrayCachedCheck } from "../createCachedTypeCheck"
|
|
5
5
|
import { getTypeInfo } from "../getTypeInfo"
|
|
6
6
|
import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
|
|
7
7
|
import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
|
|
@@ -38,18 +38,27 @@ export function typesArray<T extends AnyType>(itemType: T): ArrayType<T[]> {
|
|
|
38
38
|
const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) =>
|
|
39
39
|
`Array<${itemChecker.getTypeName(...recursiveTypeCheckers, itemChecker)}>`
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
|
|
41
|
+
const checkArrayItems = createChunkedArrayCachedCheck(
|
|
42
|
+
(items, firstIndex, path, typeCheckedValue) => {
|
|
43
|
+
for (let i = 0; i < items.length; i++) {
|
|
44
|
+
const error = itemChecker.check(items[i], emptyChildPath, typeCheckedValue)
|
|
45
|
+
if (error) {
|
|
46
|
+
return prependPathElementToTypeCheckError(error, path, firstIndex + i, typeCheckedValue)
|
|
47
|
+
}
|
|
49
48
|
}
|
|
49
|
+
return null
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const processorPlan = snapshotProcessorPlan(
|
|
54
|
+
() => [itemChecker],
|
|
55
|
+
([itemProcessor]) =>
|
|
56
|
+
itemProcessor
|
|
57
|
+
? (sn: unknown[]) => {
|
|
58
|
+
return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
|
|
59
|
+
}
|
|
60
|
+
: undefined
|
|
61
|
+
)
|
|
53
62
|
|
|
54
63
|
const thisTc: TypeChecker = new TypeChecker(
|
|
55
64
|
TypeCheckerBaseType.Array,
|
|
@@ -90,25 +99,9 @@ export function typesArray<T extends AnyType>(itemType: T): ArrayType<T[]> {
|
|
|
90
99
|
return thisTc
|
|
91
100
|
},
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
itemProcessor
|
|
97
|
-
? (sn: unknown[]) => {
|
|
98
|
-
return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
|
|
99
|
-
}
|
|
100
|
-
: undefined
|
|
101
|
-
),
|
|
102
|
-
|
|
103
|
-
snapshotProcessorPlan(
|
|
104
|
-
() => [itemChecker],
|
|
105
|
-
([itemProcessor]) =>
|
|
106
|
-
itemProcessor
|
|
107
|
-
? (sn: unknown[]) => {
|
|
108
|
-
return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
|
|
109
|
-
}
|
|
110
|
-
: undefined
|
|
111
|
-
)
|
|
102
|
+
processorPlan,
|
|
103
|
+
|
|
104
|
+
processorPlan
|
|
112
105
|
)
|
|
113
106
|
|
|
114
107
|
return thisTc
|
|
@@ -68,6 +68,19 @@ export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
|
|
|
68
68
|
// computeds cannot isolate tuple-item changes and only add allocation overhead.
|
|
69
69
|
const checkTupleItems = createWholeContainerCachedCheck(checkTupleItemsImperatively)
|
|
70
70
|
|
|
71
|
+
const processorPlan = snapshotProcessorPlan(
|
|
72
|
+
() => checkers,
|
|
73
|
+
(processors) =>
|
|
74
|
+
processors.some(Boolean)
|
|
75
|
+
? (array: unknown[]) => {
|
|
76
|
+
return array.map((item, i) => {
|
|
77
|
+
const processor = processors[i]
|
|
78
|
+
return processor ? withErrorPathSegment(i, () => processor(item)) : item
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
: undefined
|
|
82
|
+
)
|
|
83
|
+
|
|
71
84
|
const thisTc: TypeChecker = new TypeChecker(
|
|
72
85
|
TypeCheckerBaseType.Array,
|
|
73
86
|
|
|
@@ -102,31 +115,9 @@ export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
|
|
|
102
115
|
return thisTc
|
|
103
116
|
},
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
processors.some(Boolean)
|
|
109
|
-
? (array: unknown[]) => {
|
|
110
|
-
return array.map((item, i) => {
|
|
111
|
-
const processor = processors[i]
|
|
112
|
-
return processor ? withErrorPathSegment(i, () => processor(item)) : item
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
: undefined
|
|
116
|
-
),
|
|
117
|
-
|
|
118
|
-
snapshotProcessorPlan(
|
|
119
|
-
() => checkers,
|
|
120
|
-
(processors) =>
|
|
121
|
-
processors.some(Boolean)
|
|
122
|
-
? (array: unknown[]) => {
|
|
123
|
-
return array.map((item, i) => {
|
|
124
|
-
const processor = processors[i]
|
|
125
|
-
return processor ? withErrorPathSegment(i, () => processor(item)) : item
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
: undefined
|
|
129
|
-
)
|
|
118
|
+
processorPlan,
|
|
119
|
+
|
|
120
|
+
processorPlan
|
|
130
121
|
)
|
|
131
122
|
|
|
132
123
|
return thisTc
|