mobx-keystone 1.25.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/dist/mobx-keystone.esm.js +3627 -1540
- package/dist/mobx-keystone.esm.mjs +3627 -1540
- package/dist/mobx-keystone.umd.js +3638 -1543
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +3 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- 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 +2 -2
- package/dist/types/snapshot/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- 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 +2 -2
- 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 +55 -25
- package/src/actionMiddlewares/undoMiddleware.ts +112 -59
- 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 +10 -13
- 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 +39 -39
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +157 -55
- package/src/parent/path.ts +15 -5
- package/src/parent/setParent.ts +34 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +12 -101
- package/src/patch/applyPatches.ts +74 -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 +81 -29
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- 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 +27 -19
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +89 -19
- 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/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -22
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +183 -19
- package/src/tweaker/tweakArray.ts +176 -33
- package/src/tweaker/tweakPlainObject.ts +117 -39
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +25 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +541 -88
- 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 +8 -18
- 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 +78 -22
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/chunks.ts +271 -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 +71 -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
|
@@ -11,10 +11,20 @@ import {
|
|
|
11
11
|
import { assertCanWrite } from "../action/protection"
|
|
12
12
|
import { emitArraySpliceDeepChange, emitArrayUpdateDeepChange } from "../deepChange/onDeepChange"
|
|
13
13
|
import { getGlobalConfig } from "../globalConfig"
|
|
14
|
-
import
|
|
14
|
+
import { getModelMetadata } from "../model/getModelMetadata"
|
|
15
|
+
import { isModel } from "../model/utils"
|
|
16
|
+
import { getArrayPaths, setLazyArrayParentPath } from "../parent/arrayPaths"
|
|
17
|
+
import { fastGetParentPath, type ParentPath } from "../parent/path"
|
|
15
18
|
import { reindexArrayChildren, setParent } from "../parent/setParent"
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
beginPatchEmission,
|
|
21
|
+
emitPatches,
|
|
22
|
+
endPatchEmission,
|
|
23
|
+
hasPatchListenersFor,
|
|
24
|
+
InternalPatchRecorder,
|
|
25
|
+
} from "../patch/emitPatch"
|
|
17
26
|
import type { Patch } from "../patch/Patch"
|
|
27
|
+
import { clone } from "../snapshot/clone"
|
|
18
28
|
import {
|
|
19
29
|
flushInternalSnapshot,
|
|
20
30
|
freezeInternalSnapshot,
|
|
@@ -23,10 +33,19 @@ import {
|
|
|
23
33
|
updateInternalSnapshot,
|
|
24
34
|
} from "../snapshot/internal"
|
|
25
35
|
import { failure, inDevMode, isArray, isPrimitive } from "../utils"
|
|
26
|
-
import {
|
|
36
|
+
import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
|
|
37
|
+
import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
|
|
38
|
+
import { activateHandlers, areHandlersActive, runningWithoutSnapshotOrPatches } from "./core"
|
|
27
39
|
import { TweakerPriority } from "./TweakerPriority"
|
|
28
40
|
import { markAsTweakedObject } from "./treeNodeMetadata"
|
|
29
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
assertCanAttach,
|
|
43
|
+
assertCanAttachValue,
|
|
44
|
+
type IsDetachedByChange,
|
|
45
|
+
registerTweaker,
|
|
46
|
+
tweak,
|
|
47
|
+
} from "./tweak"
|
|
48
|
+
import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
|
|
30
49
|
import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
|
|
31
50
|
|
|
32
51
|
/**
|
|
@@ -124,45 +143,57 @@ export function tweakArray<T extends any[]>(
|
|
|
124
143
|
}
|
|
125
144
|
setNewInternalSnapshot(tweakedArr, untransformedSn, undefined)
|
|
126
145
|
|
|
127
|
-
|
|
128
|
-
tweakedArr,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const observeDisposer = observe(tweakedArr, arrayDidChange)
|
|
132
|
-
setTweakedObjectUntweakers(tweakedArr, interceptDisposer, observeDisposer)
|
|
146
|
+
if (activateHandlers(tweakedArr)) {
|
|
147
|
+
intercept(tweakedArr, interceptArrayMutation.bind(undefined, tweakedArr))
|
|
148
|
+
observe(tweakedArr, arrayDidChange)
|
|
149
|
+
}
|
|
133
150
|
|
|
134
151
|
return tweakedArr as any
|
|
135
152
|
}
|
|
136
153
|
|
|
137
|
-
function mutateSet(k: number, v: unknown, sn: unknown[]) {
|
|
154
|
+
function mutateSet(k: number, v: unknown, sn: unknown[]): readonly unknown[] {
|
|
138
155
|
sn[k] = v
|
|
156
|
+
return [v]
|
|
139
157
|
}
|
|
140
158
|
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
// Conservative bound below every engine's argument-count limit for spread calls.
|
|
160
|
+
const maxSpreadableItems = 8192
|
|
161
|
+
|
|
162
|
+
function mutateSplice(
|
|
163
|
+
index: number,
|
|
164
|
+
removedCount: number,
|
|
165
|
+
addedItems: any[],
|
|
166
|
+
sn: any[]
|
|
167
|
+
): readonly unknown[] {
|
|
168
|
+
if (addedItems.length < maxSpreadableItems) {
|
|
169
|
+
sn.splice(index, removedCount, ...addedItems)
|
|
170
|
+
return addedItems
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Splice the tail in a single pass instead of spreading the added items as
|
|
174
|
+
// arguments, which would overflow the engine's limit. copyWithin handles overlap.
|
|
175
|
+
const oldLength = sn.length
|
|
176
|
+
const newLength = oldLength - removedCount + addedItems.length
|
|
177
|
+
if (newLength > oldLength) sn.length = newLength
|
|
178
|
+
sn.copyWithin(index + addedItems.length, index + removedCount, oldLength)
|
|
179
|
+
for (let i = 0; i < addedItems.length; i++) sn[index + i] = addedItems[i]
|
|
180
|
+
sn.length = newLength
|
|
181
|
+
return addedItems
|
|
143
182
|
}
|
|
144
183
|
|
|
145
184
|
const patchRecorder = new InternalPatchRecorder()
|
|
146
185
|
|
|
147
186
|
function arrayDidChange(change: IArrayDidChange) {
|
|
187
|
+
if (!areHandlersActive(change.object)) {
|
|
188
|
+
// idle handler of an untweaked array
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
notifyTweakedChangeListeners(change)
|
|
192
|
+
|
|
148
193
|
const arr = change.object
|
|
149
194
|
|
|
150
195
|
patchRecorder.reset()
|
|
151
196
|
|
|
152
|
-
switch (change.type) {
|
|
153
|
-
case "splice":
|
|
154
|
-
emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
|
|
155
|
-
break
|
|
156
|
-
|
|
157
|
-
case "update":
|
|
158
|
-
emitArrayUpdateDeepChange(arr, arr, change.index as number, change.newValue, change.oldValue)
|
|
159
|
-
|
|
160
|
-
break
|
|
161
|
-
|
|
162
|
-
default:
|
|
163
|
-
break
|
|
164
|
-
}
|
|
165
|
-
|
|
166
197
|
if (runningWithoutSnapshotOrPatches) {
|
|
167
198
|
return
|
|
168
199
|
}
|
|
@@ -171,7 +202,7 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
171
202
|
const shouldEmitPatches = hasPatchListenersFor(arr)
|
|
172
203
|
const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
|
|
173
204
|
|
|
174
|
-
let mutate: ((sn: any[]) =>
|
|
205
|
+
let mutate: ((sn: any[]) => readonly unknown[]) | undefined
|
|
175
206
|
switch (change.type) {
|
|
176
207
|
case "splice":
|
|
177
208
|
mutate = arrayDidChangeSplice(
|
|
@@ -194,10 +225,57 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
194
225
|
}
|
|
195
226
|
|
|
196
227
|
runTypeCheckingAfterChange(arr, patchRecorder)
|
|
228
|
+
// (after type checking, which undoes a rejected change, but before listeners)
|
|
229
|
+
recordChangeForRollback(change)
|
|
197
230
|
if (mutate) {
|
|
198
231
|
updateInternalSnapshot(arr, mutate)
|
|
199
|
-
patchRecorder.emit(arr)
|
|
200
232
|
}
|
|
233
|
+
// Listeners may mutate another node and reuse the shared recorder.
|
|
234
|
+
// Keep this change's patches before publishing the validated deep change.
|
|
235
|
+
const { patches, invPatches } = patchRecorder
|
|
236
|
+
patchRecorder.reset()
|
|
237
|
+
|
|
238
|
+
// Reserve chronological patch notifications, then finish deep-change delivery
|
|
239
|
+
// before publishing them. Reentrant edits append behind this mutation.
|
|
240
|
+
// Every step below runs even when an earlier one throws, so a failing patch recorder
|
|
241
|
+
// cannot keep the mutation from reaching the deep change and public patch listeners,
|
|
242
|
+
// and no failure is lost to a later one.
|
|
243
|
+
beginPatchEmission()
|
|
244
|
+
let delayedError: DelayedError
|
|
245
|
+
try {
|
|
246
|
+
emitPatches(arr, patches, invPatches)
|
|
247
|
+
} catch (error) {
|
|
248
|
+
delayedError = addDelayedError(delayedError, error)
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
switch (change.type) {
|
|
252
|
+
case "splice":
|
|
253
|
+
emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
case "update":
|
|
257
|
+
emitArrayUpdateDeepChange(
|
|
258
|
+
arr,
|
|
259
|
+
arr,
|
|
260
|
+
change.index as number,
|
|
261
|
+
change.newValue,
|
|
262
|
+
change.oldValue
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
default:
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
delayedError = addDelayedError(delayedError, error)
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
endPatchEmission()
|
|
275
|
+
} catch (error) {
|
|
276
|
+
delayedError = addDelayedError(delayedError, error)
|
|
277
|
+
}
|
|
278
|
+
throwDelayedListenerError(delayedError)
|
|
201
279
|
}
|
|
202
280
|
|
|
203
281
|
const undefinedInsideArrayErrorMsg =
|
|
@@ -317,10 +395,10 @@ function arrayDidChangeSplice(
|
|
|
317
395
|
}
|
|
318
396
|
|
|
319
397
|
if (patches && readdPatches && readdPatches.length > 0) {
|
|
320
|
-
patches.push(
|
|
398
|
+
for (const patch of readdPatches) patches.push(patch)
|
|
321
399
|
}
|
|
322
400
|
if (invPatches && readdInvPatches && readdInvPatches.length > 0) {
|
|
323
|
-
invPatches.push(
|
|
401
|
+
for (const patch of readdInvPatches) invPatches.push(patch)
|
|
324
402
|
}
|
|
325
403
|
// We need to reverse once since inverse patches are applied in reverse.
|
|
326
404
|
// Repeated unshift calls here would make a large splice quadratic.
|
|
@@ -410,6 +488,10 @@ function interceptArrayMutation(
|
|
|
410
488
|
array: IObservableArray,
|
|
411
489
|
change: IArrayWillChange | IArrayWillSplice
|
|
412
490
|
) {
|
|
491
|
+
if (!areHandlersActive(array)) {
|
|
492
|
+
// idle handler of an untweaked array
|
|
493
|
+
return change
|
|
494
|
+
}
|
|
413
495
|
assertCanWrite()
|
|
414
496
|
|
|
415
497
|
switch (change.type) {
|
|
@@ -446,9 +528,18 @@ function validateArrayMutationUpdate(change: IArrayWillChange) {
|
|
|
446
528
|
function interceptArrayMutationUpdate(change: IArrayWillChange, array: IObservableArray) {
|
|
447
529
|
// TODO: should be change.object, but mobx is bugged and doesn't send the proxy
|
|
448
530
|
const oldVal = array[change.index]
|
|
531
|
+
if (change.newValue === oldVal) {
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
assertCanAttachValue(change.newValue, (node) => node === oldVal)
|
|
449
535
|
tweak(oldVal, undefined) // set old prop obj parent to undefined
|
|
450
536
|
|
|
451
|
-
|
|
537
|
+
const paths = getArrayPaths(array, 0)
|
|
538
|
+
const entry = paths?.set(change.index, change.newValue)
|
|
539
|
+
change.newValue = tweak(change.newValue, entry ?? { parent: array, path: change.index })
|
|
540
|
+
if (entry) {
|
|
541
|
+
setLazyArrayParentPath(entry, change.newValue)
|
|
542
|
+
}
|
|
452
543
|
}
|
|
453
544
|
|
|
454
545
|
function validateArrayMutationSplice(change: IArrayWillSplice) {
|
|
@@ -463,12 +554,65 @@ function validateArrayMutationSplice(change: IArrayWillSplice) {
|
|
|
463
554
|
}
|
|
464
555
|
}
|
|
465
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Checks the added items can be attached before the splice detaches anything,
|
|
559
|
+
* so a rejected splice leaves every item as it was. Value types that are added
|
|
560
|
+
* to the array at the index they already have get cloned here, since
|
|
561
|
+
* `setParent` cannot tell they are being added again (it clones them when they
|
|
562
|
+
* are added anywhere else).
|
|
563
|
+
*/
|
|
564
|
+
function checkAddedItems(change: IArrayWillSplice) {
|
|
565
|
+
const array = change.object
|
|
566
|
+
const added = change.added
|
|
567
|
+
const removedEnd = change.index + change.removedCount
|
|
568
|
+
const isDetachedByChange: IsDetachedByChange = (_node, parentPath) =>
|
|
569
|
+
parentPath.parent === array &&
|
|
570
|
+
(parentPath.path as number) >= change.index &&
|
|
571
|
+
(parentPath.path as number) < removedEnd
|
|
572
|
+
|
|
573
|
+
assertCanAttach(added, isDetachedByChange, (value) => {
|
|
574
|
+
if (!isPrimitive(value) && isModel(value) && getModelMetadata(value).valueType) {
|
|
575
|
+
const parentPath = fastGetParentPath(value, false)
|
|
576
|
+
if (parentPath?.parent === array && !isDetachedByChange(value, parentPath)) {
|
|
577
|
+
return clone(value, { generateNewIds: true })
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return value
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
466
584
|
function interceptArrayMutationSplice(change: IArrayWillSplice) {
|
|
585
|
+
checkAddedItems(change)
|
|
586
|
+
|
|
587
|
+
const array = change.object
|
|
588
|
+
const oldNextIndex = change.index + change.removedCount
|
|
589
|
+
const paths =
|
|
590
|
+
change.removedCount !== change.added.length
|
|
591
|
+
? getArrayPaths(array, array.length - oldNextIndex)
|
|
592
|
+
: getArrayPaths(array, 0)
|
|
593
|
+
|
|
467
594
|
for (let i = 0; i < change.removedCount; i++) {
|
|
468
595
|
const removedValue = change.object[change.index + i]
|
|
469
596
|
tweak(removedValue, undefined)
|
|
470
597
|
}
|
|
471
598
|
|
|
599
|
+
if (paths) {
|
|
600
|
+
const addedPaths = paths.createPaths(change.index, change.added)
|
|
601
|
+
for (let i = 0; i < change.added.length; i++) {
|
|
602
|
+
const addedPath = addedPaths[i]
|
|
603
|
+
change.added[i] = tweak(
|
|
604
|
+
change.added[i],
|
|
605
|
+
addedPath ?? { parent: array, path: change.index + i }
|
|
606
|
+
)
|
|
607
|
+
if (addedPath) {
|
|
608
|
+
setLazyArrayParentPath(addedPath, change.added[i])
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// no reindexing: the items after the splice follow their chunks
|
|
612
|
+
paths.applySplice(change.index, change.removedCount, addedPaths)
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
|
|
472
616
|
for (let i = 0; i < change.added.length; i++) {
|
|
473
617
|
change.added[i] = tweak(change.added[i], {
|
|
474
618
|
parent: change.object,
|
|
@@ -477,7 +621,6 @@ function interceptArrayMutationSplice(change: IArrayWillSplice) {
|
|
|
477
621
|
}
|
|
478
622
|
|
|
479
623
|
// we might also need to update the parent of the next indexes
|
|
480
|
-
const oldNextIndex = change.index + change.removedCount
|
|
481
624
|
const newNextIndex = change.index + change.added.length
|
|
482
625
|
|
|
483
626
|
if (oldNextIndex !== newNextIndex && oldNextIndex < change.object.length) {
|
|
@@ -13,13 +13,21 @@ 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
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
beginPatchEmission,
|
|
26
|
+
emitPatches,
|
|
27
|
+
endPatchEmission,
|
|
28
|
+
hasPatchListenersFor,
|
|
29
|
+
InternalPatchRecorder,
|
|
30
|
+
} from "../patch/emitPatch"
|
|
23
31
|
import {
|
|
24
32
|
flushInternalSnapshot,
|
|
25
33
|
freezeInternalSnapshot,
|
|
@@ -29,12 +37,16 @@ import {
|
|
|
29
37
|
updateInternalSnapshot,
|
|
30
38
|
} from "../snapshot/internal"
|
|
31
39
|
import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
|
|
32
|
-
import {
|
|
40
|
+
import { withoutCachedTypeChecking } from "../types/createCachedTypeCheck"
|
|
41
|
+
import { failure, isPlainObject, isPrimitive, nodeObservableOptions, setProtoProp } from "../utils"
|
|
42
|
+
import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
|
|
33
43
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
34
|
-
import {
|
|
44
|
+
import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
|
|
45
|
+
import { activateHandlers, runningWithoutSnapshotOrPatches } from "./core"
|
|
35
46
|
import { TweakerPriority } from "./TweakerPriority"
|
|
36
|
-
import { markAsTweakedObject } from "./treeNodeMetadata"
|
|
37
|
-
import { registerTweaker, tweak } from "./tweak"
|
|
47
|
+
import { markAsTweakedObject, treeNodeMetadata } from "./treeNodeMetadata"
|
|
48
|
+
import { assertCanAttachValue, registerTweaker, tweak } from "./tweak"
|
|
49
|
+
import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
|
|
38
50
|
import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
|
|
39
51
|
|
|
40
52
|
/**
|
|
@@ -51,7 +63,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
|
|
|
51
63
|
const originalIsObservableObject = isObservableObject(originalObj)
|
|
52
64
|
const tweakedObj = originalIsObservableObject
|
|
53
65
|
? originalObj
|
|
54
|
-
: observable.object({}, undefined,
|
|
66
|
+
: observable.object({}, undefined, nodeObservableOptions)
|
|
55
67
|
|
|
56
68
|
// Mark it as tweaked before processing children. The cleanup functions are
|
|
57
69
|
// installed after the MobX handlers have been registered.
|
|
@@ -65,7 +77,9 @@ export function tweakPlainObject<T extends Record<string, any>>(
|
|
|
65
77
|
)
|
|
66
78
|
|
|
67
79
|
const initialDataSnapshot =
|
|
68
|
-
isDataObject && snapshotModelType
|
|
80
|
+
isDataObject && snapshotModelType !== undefined
|
|
81
|
+
? takeModelInitialDataSnapshot(tweakedObj)
|
|
82
|
+
: undefined
|
|
69
83
|
const untransformedSn: any = initialDataSnapshot ?? {}
|
|
70
84
|
|
|
71
85
|
// substitute initial values by tweaked values
|
|
@@ -112,7 +126,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
|
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
let transformFn: SnapshotTransformFn | undefined
|
|
115
|
-
if (snapshotModelType) {
|
|
129
|
+
if (snapshotModelType !== undefined) {
|
|
116
130
|
untransformedSn[modelTypeKey] = snapshotModelType
|
|
117
131
|
|
|
118
132
|
const modelInfo = getModelInfoForName(snapshotModelType)
|
|
@@ -132,58 +146,60 @@ export function tweakPlainObject<T extends Record<string, any>>(
|
|
|
132
146
|
transformFn
|
|
133
147
|
)
|
|
134
148
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
if (activateHandlers(tweakedObj)) {
|
|
150
|
+
intercept(tweakedObj, interceptObjectMutation)
|
|
151
|
+
observe(tweakedObj, objectDidChange)
|
|
152
|
+
}
|
|
138
153
|
|
|
139
154
|
return tweakedObj as any
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
function mutateSet(
|
|
158
|
+
k: PropertyKey,
|
|
159
|
+
v: unknown,
|
|
160
|
+
sn: Record<PropertyKey, unknown>
|
|
161
|
+
): readonly unknown[] {
|
|
147
162
|
if (k === "__proto__") {
|
|
148
163
|
setProtoProp(sn, v)
|
|
149
164
|
} else {
|
|
150
165
|
sn[k] = v
|
|
151
166
|
}
|
|
167
|
+
return [v]
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
|
|
170
|
+
const noValues: readonly unknown[] = []
|
|
171
|
+
|
|
172
|
+
function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>): readonly unknown[] {
|
|
155
173
|
delete sn[k]
|
|
174
|
+
return noValues
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
const patchRecorder = new InternalPatchRecorder()
|
|
159
178
|
|
|
160
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
|
+
}
|
|
161
186
|
if (typeof change.name !== "string") {
|
|
162
187
|
throw failure("change.name is not a string")
|
|
163
188
|
}
|
|
164
189
|
|
|
165
|
-
|
|
166
|
-
const actualNode = dataToModelNode(obj)
|
|
167
|
-
|
|
168
|
-
patchRecorder.reset()
|
|
169
|
-
|
|
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
|
|
190
|
+
notifyTweakedChangeListeners(change)
|
|
178
191
|
|
|
179
|
-
|
|
180
|
-
emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
|
|
181
|
-
break
|
|
192
|
+
const actualNode = metadata.dataObjectParent ?? obj
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
if (
|
|
195
|
+
actualNode !== obj &&
|
|
196
|
+
change.name === getModelIdPropertyName(actualNode.constructor as ModelClass<AnyModel>)
|
|
197
|
+
) {
|
|
198
|
+
onModelIdChanged(actualNode)
|
|
185
199
|
}
|
|
186
200
|
|
|
201
|
+
patchRecorder.reset()
|
|
202
|
+
|
|
187
203
|
if (runningWithoutSnapshotOrPatches) {
|
|
188
204
|
return
|
|
189
205
|
}
|
|
@@ -192,7 +208,7 @@ function objectDidChange(change: IObjectDidChange): void {
|
|
|
192
208
|
const shouldEmitPatches = hasPatchListenersFor(actualNode)
|
|
193
209
|
const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
|
|
194
210
|
|
|
195
|
-
let mutate: ((sn: any) =>
|
|
211
|
+
let mutate: ((sn: any) => readonly unknown[]) | undefined
|
|
196
212
|
switch (change.type) {
|
|
197
213
|
case "add":
|
|
198
214
|
case "update":
|
|
@@ -215,9 +231,61 @@ function objectDidChange(change: IObjectDidChange): void {
|
|
|
215
231
|
throw failure("assertion error: unsupported object change type")
|
|
216
232
|
}
|
|
217
233
|
|
|
218
|
-
|
|
234
|
+
if (change.type !== "update" && isTypeCheckingAfterChangeEnabled()) {
|
|
235
|
+
// MobX notifies object observers before invalidating dependencies on missing
|
|
236
|
+
// keys (and removed keys in MobX 4). Cached ancestor checks may still
|
|
237
|
+
// describe the previous shape.
|
|
238
|
+
withoutCachedTypeChecking(obj, () => runTypeCheckingAfterChange(obj, patchRecorder))
|
|
239
|
+
} else {
|
|
240
|
+
runTypeCheckingAfterChange(obj, patchRecorder)
|
|
241
|
+
}
|
|
242
|
+
// (after type checking, which undoes a rejected change, but before listeners)
|
|
243
|
+
recordChangeForRollback(change)
|
|
219
244
|
updateInternalSnapshot(actualNode, mutate)
|
|
220
|
-
|
|
245
|
+
|
|
246
|
+
// Listeners may mutate another node and reuse the shared recorder.
|
|
247
|
+
// Keep this change's patches before publishing the validated deep change.
|
|
248
|
+
const { patches, invPatches } = patchRecorder
|
|
249
|
+
patchRecorder.reset()
|
|
250
|
+
|
|
251
|
+
// Reserve chronological patch notifications, then finish deep-change delivery
|
|
252
|
+
// before publishing them. Reentrant edits append behind this mutation.
|
|
253
|
+
// Every step below runs even when an earlier one throws, so a failing patch recorder
|
|
254
|
+
// cannot keep the mutation from reaching the deep change and public patch listeners,
|
|
255
|
+
// and no failure is lost to a later one.
|
|
256
|
+
beginPatchEmission()
|
|
257
|
+
let delayedError: DelayedError
|
|
258
|
+
try {
|
|
259
|
+
emitPatches(actualNode, patches, invPatches)
|
|
260
|
+
} catch (error) {
|
|
261
|
+
delayedError = addDelayedError(delayedError, error)
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
switch (change.type) {
|
|
265
|
+
case "add":
|
|
266
|
+
emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
case "update":
|
|
270
|
+
emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
case "remove":
|
|
274
|
+
emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
default:
|
|
278
|
+
throw failure("assertion error: unsupported object change type")
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
delayedError = addDelayedError(delayedError, error)
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
endPatchEmission()
|
|
285
|
+
} catch (error) {
|
|
286
|
+
delayedError = addDelayedError(delayedError, error)
|
|
287
|
+
}
|
|
288
|
+
throwDelayedListenerError(delayedError)
|
|
221
289
|
}
|
|
222
290
|
|
|
223
291
|
function objectDidChangeRemove(
|
|
@@ -284,7 +352,14 @@ function objectDidChangeAddOrUpdate(
|
|
|
284
352
|
return mutate
|
|
285
353
|
}
|
|
286
354
|
|
|
355
|
+
const noneDetached = () => false
|
|
356
|
+
|
|
287
357
|
function interceptObjectMutation(change: IObjectWillChange) {
|
|
358
|
+
const metadata = treeNodeMetadata.get(change.object)
|
|
359
|
+
if (!metadata?.handlersActive) {
|
|
360
|
+
// idle handler of an untweaked object
|
|
361
|
+
return change
|
|
362
|
+
}
|
|
288
363
|
assertCanWrite()
|
|
289
364
|
|
|
290
365
|
if (typeof change.name === "symbol") {
|
|
@@ -294,10 +369,11 @@ function interceptObjectMutation(change: IObjectWillChange) {
|
|
|
294
369
|
// This must happen before detach/reparent work below. It keeps the old value
|
|
295
370
|
// available for inverse patches and folds a departing dirty child into its
|
|
296
371
|
// parent snapshot while its parent path still exists.
|
|
297
|
-
flushInternalSnapshot(
|
|
372
|
+
flushInternalSnapshot(metadata.dataObjectParent ?? change.object, false)
|
|
298
373
|
|
|
299
374
|
switch (change.type) {
|
|
300
375
|
case "add":
|
|
376
|
+
assertCanAttachValue(change.newValue, noneDetached)
|
|
301
377
|
change.newValue = tweak(change.newValue, {
|
|
302
378
|
parent: change.object,
|
|
303
379
|
path: String(change.name),
|
|
@@ -313,6 +389,8 @@ function interceptObjectMutation(change: IObjectWillChange) {
|
|
|
313
389
|
const oldVal = change.object[change.name]
|
|
314
390
|
const newVal = change.newValue
|
|
315
391
|
if (newVal !== oldVal) {
|
|
392
|
+
// (first, so a value that cannot be attached leaves the old one in place)
|
|
393
|
+
assertCanAttachValue(newVal, (node) => node === oldVal)
|
|
316
394
|
tweak(oldVal, undefined)
|
|
317
395
|
|
|
318
396
|
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
|
+
}
|