mobx-keystone 2.0.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 +22 -0
- package/dist/mobx-keystone.esm.js +2341 -780
- package/dist/mobx-keystone.esm.mjs +2341 -780
- package/dist/mobx-keystone.umd.js +2346 -782
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- 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/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/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- package/package.json +2 -2
- package/src/actionMiddlewares/transactionMiddleware.ts +40 -41
- package/src/actionMiddlewares/undoMiddleware.ts +47 -13
- package/src/model/BaseModel.ts +6 -4
- package/src/modelShared/modelDecorator.ts +5 -25
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/onChildAttachedTo.ts +130 -44
- package/src/parent/path.ts +10 -2
- 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/ref/core.ts +63 -27
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/snapshot/applySnapshot.ts +26 -18
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +67 -10
- 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 +107 -27
- package/src/tweaker/tweakPlainObject.ts +56 -31
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +5 -104
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +522 -94
- package/src/types/objectBased/typesRecord.ts +2 -5
- package/src/utils/ModelPool.ts +75 -19
- package/src/utils/chunks.ts +271 -0
- package/src/utils/index.ts +37 -0
package/src/tweaker/tweak.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import { action } from "mobx"
|
|
1
|
+
import { action, isObservableArray, isObservableObject } from "mobx"
|
|
2
2
|
import { isDataModel } from "../dataModel/utils"
|
|
3
|
+
import { Frozen } from "../frozen/Frozen"
|
|
3
4
|
import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
|
|
5
|
+
import { getModelMetadata } from "../model/getModelMetadata"
|
|
6
|
+
import { isModel } from "../model/utils"
|
|
4
7
|
import { getObjectChildren } from "../parent/coreObjectChildren"
|
|
5
8
|
import { fastGetParent, type ParentPath } from "../parent/path"
|
|
6
9
|
import { setParent } from "../parent/setParent"
|
|
10
|
+
import { fastIsRootStoreNoAtom } from "../rootStore/rootStore"
|
|
7
11
|
import { unsetInternalSnapshot } from "../snapshot/internal"
|
|
8
12
|
import type { AnyStandardType, TypeToData } from "../types/schemas"
|
|
9
13
|
import { typeCheck } from "../types/typeCheck"
|
|
10
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
failure,
|
|
16
|
+
inDevMode,
|
|
17
|
+
isArray,
|
|
18
|
+
isMap,
|
|
19
|
+
isObject,
|
|
20
|
+
isPlainObject,
|
|
21
|
+
isPrimitive,
|
|
22
|
+
isSet,
|
|
23
|
+
} from "../utils"
|
|
11
24
|
import { getSafeErrorValuePreview } from "../utils/errorDiagnostics"
|
|
12
|
-
import {
|
|
25
|
+
import { deactivateHandlers, isTweakedObject } from "./core"
|
|
13
26
|
import { registerDefaultTweakers } from "./registerDefaultTweakers"
|
|
14
27
|
import { treeNodeMetadata } from "./treeNodeMetadata"
|
|
15
28
|
|
|
@@ -100,9 +113,7 @@ function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T
|
|
|
100
113
|
|
|
101
114
|
// unsupported (must go before plain object tweaker)
|
|
102
115
|
if (isDataModel(value)) {
|
|
103
|
-
throw
|
|
104
|
-
"data models are not directly supported. you may insert the data in the tree instead ('$' property)."
|
|
105
|
-
)
|
|
116
|
+
throw dataModelsNotSupportedError()
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
registerDefaultTweakers()
|
|
@@ -116,17 +127,23 @@ function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T
|
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
|
|
119
|
-
|
|
130
|
+
throw unsupportedValueError(value)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function dataModelsNotSupportedError() {
|
|
134
|
+
return failure(
|
|
135
|
+
"data models are not directly supported. you may insert the data in the tree instead ('$' property)."
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function unsupportedValueError(value: unknown) {
|
|
120
140
|
if (isMap(value)) {
|
|
121
|
-
|
|
141
|
+
return failure("maps are not directly supported. consider using 'ObjectMap' / 'asMap' instead.")
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
// unsupported
|
|
125
143
|
if (isSet(value)) {
|
|
126
|
-
|
|
144
|
+
return failure("sets are not directly supported. consider using 'ArraySet' / 'asSet' instead.")
|
|
127
145
|
}
|
|
128
|
-
|
|
129
|
-
throw failure(
|
|
146
|
+
return failure(
|
|
130
147
|
`tweak can only work over models, observable objects/arrays, or primitives, but got ${getSafeErrorValuePreview(value)} instead`
|
|
131
148
|
)
|
|
132
149
|
}
|
|
@@ -141,6 +158,154 @@ export function tweak<T>(value: T, parentPath: ParentPath<any> | undefined): T {
|
|
|
141
158
|
return isPrimitive(value) ? value : tweakNonPrimitive(value, parentPath)
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Tells whether a node that has a parent gets detached by the change being
|
|
163
|
+
* intercepted before the new values are attached.
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export type IsDetachedByChange = (node: object, parentPath: ParentPath<any>) => boolean
|
|
167
|
+
|
|
168
|
+
// nodes to be attached by the change being checked, since a node cannot be
|
|
169
|
+
// attached twice
|
|
170
|
+
const seenNodes = new Set<object>()
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Throws the error `tweak` or `setParent` would throw when attaching `values`
|
|
174
|
+
* (and whatever they contain) after the change being intercepted detaches its
|
|
175
|
+
* values, without changing anything. Interceptors call it before detaching, so
|
|
176
|
+
* a rejected change leaves the tree as it was.
|
|
177
|
+
* `prepareValue`, when given, replaces each value before it is checked.
|
|
178
|
+
* @internal
|
|
179
|
+
*/
|
|
180
|
+
export function assertCanAttach(
|
|
181
|
+
values: unknown[],
|
|
182
|
+
isDetachedByChange: IsDetachedByChange,
|
|
183
|
+
prepareValue?: (value: unknown) => unknown
|
|
184
|
+
): void {
|
|
185
|
+
try {
|
|
186
|
+
for (let i = 0; i < values.length; i++) {
|
|
187
|
+
if (prepareValue) {
|
|
188
|
+
values[i] = prepareValue(values[i])
|
|
189
|
+
}
|
|
190
|
+
checkAttach(values[i], isDetachedByChange)
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
seenNodes.clear()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `assertCanAttach` for a single value.
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
export function assertCanAttachValue(value: unknown, isDetachedByChange: IsDetachedByChange): void {
|
|
202
|
+
if (isPrimitive(value)) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
checkAttach(value, isDetachedByChange)
|
|
207
|
+
} finally {
|
|
208
|
+
seenNodes.clear()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function checkAttach(value: unknown, isDetachedByChange: IsDetachedByChange): void {
|
|
213
|
+
if (isPrimitive(value)) {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// (one metadata lookup instead of one per helper)
|
|
218
|
+
const metadata = treeNodeMetadata.get(value)
|
|
219
|
+
if (metadata?.tweaked) {
|
|
220
|
+
if (metadata.dataObjectParent !== undefined) {
|
|
221
|
+
throw failure("value must be the model object instance instead of the '$' sub-object")
|
|
222
|
+
}
|
|
223
|
+
if (fastIsRootStoreNoAtom(value)) {
|
|
224
|
+
throw failure("root stores cannot be attached to any parents")
|
|
225
|
+
}
|
|
226
|
+
if (isModel(value) && getModelMetadata(value).valueType) {
|
|
227
|
+
// setParent clones it when it has a parent
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
const parentPath = metadata.parentPath
|
|
231
|
+
if (
|
|
232
|
+
parentPath &&
|
|
233
|
+
!isDetachedByChange(value, parentPath) &&
|
|
234
|
+
!isUntweakedByChange(parentPath.parent, isDetachedByChange)
|
|
235
|
+
) {
|
|
236
|
+
throw failure("an object cannot be assigned a new parent when it already has one")
|
|
237
|
+
}
|
|
238
|
+
assertNotSeen(value)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// the values internalTweak accepts, most common first
|
|
243
|
+
if (isPlainObject(value)) {
|
|
244
|
+
if (isObservableObject(value)) {
|
|
245
|
+
// (it becomes the tree node itself)
|
|
246
|
+
assertNotSeen(value)
|
|
247
|
+
}
|
|
248
|
+
checkAttachProps(value, isDetachedByChange)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (isArray(value)) {
|
|
252
|
+
if (isObservableArray(value)) {
|
|
253
|
+
assertNotSeen(value)
|
|
254
|
+
}
|
|
255
|
+
for (let i = 0; i < value.length; i++) {
|
|
256
|
+
checkAttach(value[i], isDetachedByChange)
|
|
257
|
+
}
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
// (data models, frozen values and models can be observable objects too)
|
|
261
|
+
if (isDataModel(value)) {
|
|
262
|
+
throw dataModelsNotSupportedError()
|
|
263
|
+
}
|
|
264
|
+
if (value instanceof Frozen || isModel(value)) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if (isObservableObject(value)) {
|
|
268
|
+
assertNotSeen(value)
|
|
269
|
+
checkAttachProps(value as Record<string, unknown>, isDetachedByChange)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
throw unsupportedValueError(value)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function checkAttachProps(obj: Record<string, unknown>, isDetachedByChange: IsDetachedByChange) {
|
|
276
|
+
const keys = Object.keys(obj)
|
|
277
|
+
for (let i = 0; i < keys.length; i++) {
|
|
278
|
+
checkAttach(obj[keys[i]], isDetachedByChange)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function assertNotSeen(value: object) {
|
|
283
|
+
if (seenNodes.has(value)) {
|
|
284
|
+
throw failure("an object cannot be assigned a new parent when it already has one")
|
|
285
|
+
}
|
|
286
|
+
seenNodes.add(value)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Detaching a plain object or array untweaks it, which detaches its children.
|
|
291
|
+
*/
|
|
292
|
+
function isUntweakedByChange(node: object, isDetachedByChange: IsDetachedByChange): boolean {
|
|
293
|
+
for (;;) {
|
|
294
|
+
const metadata = treeNodeMetadata.get(node)
|
|
295
|
+
if (!metadata?.handlersActive) {
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
const parentPath = metadata.parentPath
|
|
299
|
+
if (!parentPath) {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
if (isDetachedByChange(node, parentPath)) {
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
node = parentPath.parent
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
144
309
|
/**
|
|
145
310
|
* @internal
|
|
146
311
|
*/
|
|
@@ -156,8 +321,7 @@ export function tryUntweak(value: any): (() => void) | undefined {
|
|
|
156
321
|
}
|
|
157
322
|
|
|
158
323
|
const metadata = treeNodeMetadata.get(value)
|
|
159
|
-
|
|
160
|
-
if (!untweaker) {
|
|
324
|
+
if (!metadata?.handlersActive) {
|
|
161
325
|
return undefined
|
|
162
326
|
}
|
|
163
327
|
|
|
@@ -177,14 +341,13 @@ export function tryUntweak(value: any): (() => void) | undefined {
|
|
|
177
341
|
}
|
|
178
342
|
|
|
179
343
|
return () => {
|
|
180
|
-
// post-untweaking
|
|
181
|
-
|
|
344
|
+
// post-untweaking
|
|
345
|
+
deactivateHandlers(value, metadata)
|
|
182
346
|
|
|
183
347
|
// Match the former independent-map ordering: the node stops being tweaked
|
|
184
348
|
// before its snapshot is unset, but the shared record must stay alive until
|
|
185
349
|
// snapshot cleanup has consumed it.
|
|
186
350
|
metadata.tweaked = false
|
|
187
|
-
metadata.untweaker = undefined
|
|
188
351
|
unsetInternalSnapshot(value)
|
|
189
352
|
// These associations previously lived in independent WeakMaps and remain
|
|
190
353
|
// valid across untweak/retweak, so keep their shared record when present.
|
|
@@ -11,7 +11,10 @@ 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
19
|
import {
|
|
17
20
|
beginPatchEmission,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
InternalPatchRecorder,
|
|
22
25
|
} from "../patch/emitPatch"
|
|
23
26
|
import type { Patch } from "../patch/Patch"
|
|
27
|
+
import { clone } from "../snapshot/clone"
|
|
24
28
|
import {
|
|
25
29
|
flushInternalSnapshot,
|
|
26
30
|
freezeInternalSnapshot,
|
|
@@ -29,20 +33,20 @@ import {
|
|
|
29
33
|
updateInternalSnapshot,
|
|
30
34
|
} from "../snapshot/internal"
|
|
31
35
|
import { failure, inDevMode, isArray, isPrimitive } from "../utils"
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throwDelayedError,
|
|
36
|
-
} from "../utils/forEachWithDelayedThrow"
|
|
37
|
-
import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
|
|
36
|
+
import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
|
|
37
|
+
import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
|
|
38
|
+
import { activateHandlers, areHandlersActive, runningWithoutSnapshotOrPatches } from "./core"
|
|
38
39
|
import { TweakerPriority } from "./TweakerPriority"
|
|
39
40
|
import { markAsTweakedObject } from "./treeNodeMetadata"
|
|
40
|
-
import { registerTweaker, tweak } from "./tweak"
|
|
41
41
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
assertCanAttach,
|
|
43
|
+
assertCanAttachValue,
|
|
44
|
+
type IsDetachedByChange,
|
|
45
|
+
registerTweaker,
|
|
46
|
+
tweak,
|
|
47
|
+
} from "./tweak"
|
|
48
|
+
import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
|
|
49
|
+
import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* @internal
|
|
@@ -139,27 +143,31 @@ export function tweakArray<T extends any[]>(
|
|
|
139
143
|
}
|
|
140
144
|
setNewInternalSnapshot(tweakedArr, untransformedSn, undefined)
|
|
141
145
|
|
|
142
|
-
|
|
143
|
-
tweakedArr,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const observeDisposer = observe(tweakedArr, arrayDidChange)
|
|
147
|
-
setTweakedObjectUntweakers(tweakedArr, interceptDisposer, observeDisposer)
|
|
146
|
+
if (activateHandlers(tweakedArr)) {
|
|
147
|
+
intercept(tweakedArr, interceptArrayMutation.bind(undefined, tweakedArr))
|
|
148
|
+
observe(tweakedArr, arrayDidChange)
|
|
149
|
+
}
|
|
148
150
|
|
|
149
151
|
return tweakedArr as any
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
function mutateSet(k: number, v: unknown, sn: unknown[]) {
|
|
154
|
+
function mutateSet(k: number, v: unknown, sn: unknown[]): readonly unknown[] {
|
|
153
155
|
sn[k] = v
|
|
156
|
+
return [v]
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
// Conservative bound below every engine's argument-count limit for spread calls.
|
|
157
160
|
const maxSpreadableItems = 8192
|
|
158
161
|
|
|
159
|
-
function mutateSplice(
|
|
162
|
+
function mutateSplice(
|
|
163
|
+
index: number,
|
|
164
|
+
removedCount: number,
|
|
165
|
+
addedItems: any[],
|
|
166
|
+
sn: any[]
|
|
167
|
+
): readonly unknown[] {
|
|
160
168
|
if (addedItems.length < maxSpreadableItems) {
|
|
161
169
|
sn.splice(index, removedCount, ...addedItems)
|
|
162
|
-
return
|
|
170
|
+
return addedItems
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
// Splice the tail in a single pass instead of spreading the added items as
|
|
@@ -170,11 +178,18 @@ function mutateSplice(index: number, removedCount: number, addedItems: any[], sn
|
|
|
170
178
|
sn.copyWithin(index + addedItems.length, index + removedCount, oldLength)
|
|
171
179
|
for (let i = 0; i < addedItems.length; i++) sn[index + i] = addedItems[i]
|
|
172
180
|
sn.length = newLength
|
|
181
|
+
return addedItems
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
const patchRecorder = new InternalPatchRecorder()
|
|
176
185
|
|
|
177
186
|
function arrayDidChange(change: IArrayDidChange) {
|
|
187
|
+
if (!areHandlersActive(change.object)) {
|
|
188
|
+
// idle handler of an untweaked array
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
notifyTweakedChangeListeners(change)
|
|
192
|
+
|
|
178
193
|
const arr = change.object
|
|
179
194
|
|
|
180
195
|
patchRecorder.reset()
|
|
@@ -183,13 +198,11 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
183
198
|
return
|
|
184
199
|
}
|
|
185
200
|
|
|
186
|
-
recordTypeCheckingBatchChange(change)
|
|
187
|
-
|
|
188
201
|
const oldSnapshot = getInternalSnapshot(arr as Array<unknown>)!.untransformed
|
|
189
202
|
const shouldEmitPatches = hasPatchListenersFor(arr)
|
|
190
203
|
const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
|
|
191
204
|
|
|
192
|
-
let mutate: ((sn: any[]) =>
|
|
205
|
+
let mutate: ((sn: any[]) => readonly unknown[]) | undefined
|
|
193
206
|
switch (change.type) {
|
|
194
207
|
case "splice":
|
|
195
208
|
mutate = arrayDidChangeSplice(
|
|
@@ -212,6 +225,8 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
runTypeCheckingAfterChange(arr, patchRecorder)
|
|
228
|
+
// (after type checking, which undoes a rejected change, but before listeners)
|
|
229
|
+
recordChangeForRollback(change)
|
|
215
230
|
if (mutate) {
|
|
216
231
|
updateInternalSnapshot(arr, mutate)
|
|
217
232
|
}
|
|
@@ -260,7 +275,7 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
260
275
|
} catch (error) {
|
|
261
276
|
delayedError = addDelayedError(delayedError, error)
|
|
262
277
|
}
|
|
263
|
-
|
|
278
|
+
throwDelayedListenerError(delayedError)
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
const undefinedInsideArrayErrorMsg =
|
|
@@ -473,6 +488,10 @@ function interceptArrayMutation(
|
|
|
473
488
|
array: IObservableArray,
|
|
474
489
|
change: IArrayWillChange | IArrayWillSplice
|
|
475
490
|
) {
|
|
491
|
+
if (!areHandlersActive(array)) {
|
|
492
|
+
// idle handler of an untweaked array
|
|
493
|
+
return change
|
|
494
|
+
}
|
|
476
495
|
assertCanWrite()
|
|
477
496
|
|
|
478
497
|
switch (change.type) {
|
|
@@ -509,9 +528,18 @@ function validateArrayMutationUpdate(change: IArrayWillChange) {
|
|
|
509
528
|
function interceptArrayMutationUpdate(change: IArrayWillChange, array: IObservableArray) {
|
|
510
529
|
// TODO: should be change.object, but mobx is bugged and doesn't send the proxy
|
|
511
530
|
const oldVal = array[change.index]
|
|
531
|
+
if (change.newValue === oldVal) {
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
assertCanAttachValue(change.newValue, (node) => node === oldVal)
|
|
512
535
|
tweak(oldVal, undefined) // set old prop obj parent to undefined
|
|
513
536
|
|
|
514
|
-
|
|
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
|
+
}
|
|
515
543
|
}
|
|
516
544
|
|
|
517
545
|
function validateArrayMutationSplice(change: IArrayWillSplice) {
|
|
@@ -526,12 +554,65 @@ function validateArrayMutationSplice(change: IArrayWillSplice) {
|
|
|
526
554
|
}
|
|
527
555
|
}
|
|
528
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
|
+
|
|
529
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
|
+
|
|
530
594
|
for (let i = 0; i < change.removedCount; i++) {
|
|
531
595
|
const removedValue = change.object[change.index + i]
|
|
532
596
|
tweak(removedValue, undefined)
|
|
533
597
|
}
|
|
534
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
|
+
|
|
535
616
|
for (let i = 0; i < change.added.length; i++) {
|
|
536
617
|
change.added[i] = tweak(change.added[i], {
|
|
537
618
|
parent: change.object,
|
|
@@ -540,7 +621,6 @@ function interceptArrayMutationSplice(change: IArrayWillSplice) {
|
|
|
540
621
|
}
|
|
541
622
|
|
|
542
623
|
// we might also need to update the parent of the next indexes
|
|
543
|
-
const oldNextIndex = change.index + change.removedCount
|
|
544
624
|
const newNextIndex = change.index + change.added.length
|
|
545
625
|
|
|
546
626
|
if (oldNextIndex !== newNextIndex && oldNextIndex < change.object.length) {
|
|
@@ -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(
|
|
@@ -337,7 +352,14 @@ function objectDidChangeAddOrUpdate(
|
|
|
337
352
|
return mutate
|
|
338
353
|
}
|
|
339
354
|
|
|
355
|
+
const noneDetached = () => false
|
|
356
|
+
|
|
340
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
|
+
}
|
|
341
363
|
assertCanWrite()
|
|
342
364
|
|
|
343
365
|
if (typeof change.name === "symbol") {
|
|
@@ -347,10 +369,11 @@ function interceptObjectMutation(change: IObjectWillChange) {
|
|
|
347
369
|
// This must happen before detach/reparent work below. It keeps the old value
|
|
348
370
|
// available for inverse patches and folds a departing dirty child into its
|
|
349
371
|
// parent snapshot while its parent path still exists.
|
|
350
|
-
flushInternalSnapshot(
|
|
372
|
+
flushInternalSnapshot(metadata.dataObjectParent ?? change.object, false)
|
|
351
373
|
|
|
352
374
|
switch (change.type) {
|
|
353
375
|
case "add":
|
|
376
|
+
assertCanAttachValue(change.newValue, noneDetached)
|
|
354
377
|
change.newValue = tweak(change.newValue, {
|
|
355
378
|
parent: change.object,
|
|
356
379
|
path: String(change.name),
|
|
@@ -366,6 +389,8 @@ function interceptObjectMutation(change: IObjectWillChange) {
|
|
|
366
389
|
const oldVal = change.object[change.name]
|
|
367
390
|
const newVal = change.newValue
|
|
368
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)
|
|
369
394
|
tweak(oldVal, undefined)
|
|
370
395
|
|
|
371
396
|
change.newValue = tweak(newVal, {
|