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
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 =
|
|
@@ -274,7 +289,6 @@ function arrayDidChangeUpdate(
|
|
|
274
289
|
) {
|
|
275
290
|
const k = change.index
|
|
276
291
|
const val = change.newValue
|
|
277
|
-
const oldVal = oldSnapshot[k]
|
|
278
292
|
let newVal: any
|
|
279
293
|
if (isPrimitive(val)) {
|
|
280
294
|
newVal = val
|
|
@@ -284,15 +298,14 @@ function arrayDidChangeUpdate(
|
|
|
284
298
|
}
|
|
285
299
|
const mutate = mutateSet.bind(undefined, k, newVal)
|
|
286
300
|
|
|
287
|
-
if (
|
|
301
|
+
if (shouldBuildInversePatches) {
|
|
288
302
|
const path = [k]
|
|
289
303
|
const patches = shouldEmitPatches
|
|
290
304
|
? [{ op: "replace" as const, path, value: freezeInternalSnapshot(newVal) }]
|
|
291
305
|
: undefined
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
patchRecorder.record(patches, invPatches)
|
|
306
|
+
patchRecorder.record(patches, [
|
|
307
|
+
{ op: "replace" as const, path, value: freezeInternalSnapshot(oldSnapshot[k]) },
|
|
308
|
+
])
|
|
296
309
|
}
|
|
297
310
|
return mutate
|
|
298
311
|
}
|
|
@@ -320,17 +333,16 @@ function arrayDidChangeSplice(
|
|
|
320
333
|
|
|
321
334
|
const mutate = mutateSplice.bind(undefined, index, removedCount, addedItems)
|
|
322
335
|
|
|
323
|
-
// With neither patch listeners nor type checking, there is no
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
if (!shouldEmitPatches && !shouldBuildInversePatches) {
|
|
336
|
+
// With neither patch listeners nor type checking, there is no patch data to
|
|
337
|
+
// build. The snapshot mutation above is all the post-change work we need.
|
|
338
|
+
if (!shouldBuildInversePatches) {
|
|
327
339
|
return mutate
|
|
328
340
|
}
|
|
329
341
|
|
|
330
342
|
const oldLen = oldSnapshot.length
|
|
331
343
|
|
|
332
344
|
const patches: Patch[] | undefined = shouldEmitPatches ? [] : undefined
|
|
333
|
-
const invPatches: Patch[]
|
|
345
|
+
const invPatches: Patch[] = []
|
|
334
346
|
|
|
335
347
|
// optimization: if we add as many as we remove then remove/readd instead
|
|
336
348
|
|
|
@@ -342,13 +354,13 @@ function arrayDidChangeSplice(
|
|
|
342
354
|
|
|
343
355
|
if (addedCount === removedCount) {
|
|
344
356
|
const readdPatches: Patch[] | undefined = shouldEmitPatches ? [] : undefined
|
|
345
|
-
const readdInvPatches: Patch[]
|
|
357
|
+
const readdInvPatches: Patch[] = []
|
|
346
358
|
let removed = 0
|
|
347
359
|
|
|
348
360
|
for (let i = 0; i < addedCount; i++) {
|
|
349
361
|
const realIndex = index + i
|
|
350
362
|
|
|
351
|
-
const newVal =
|
|
363
|
+
const newVal = addedItems[i]
|
|
352
364
|
const oldVal = oldSnapshot[realIndex]
|
|
353
365
|
|
|
354
366
|
if (newVal !== oldVal) {
|
|
@@ -357,7 +369,7 @@ function arrayDidChangeSplice(
|
|
|
357
369
|
op: "remove",
|
|
358
370
|
path: removePath,
|
|
359
371
|
})
|
|
360
|
-
invPatches
|
|
372
|
+
invPatches.push({
|
|
361
373
|
op: "remove",
|
|
362
374
|
path: removePath,
|
|
363
375
|
})
|
|
@@ -371,7 +383,7 @@ function arrayDidChangeSplice(
|
|
|
371
383
|
value: freezeInternalSnapshot(newVal),
|
|
372
384
|
})
|
|
373
385
|
|
|
374
|
-
readdInvPatches
|
|
386
|
+
readdInvPatches.push({
|
|
375
387
|
op: "add",
|
|
376
388
|
path: readdPath,
|
|
377
389
|
value: freezeInternalSnapshot(oldVal),
|
|
@@ -379,17 +391,13 @@ function arrayDidChangeSplice(
|
|
|
379
391
|
}
|
|
380
392
|
}
|
|
381
393
|
|
|
382
|
-
if (patches && readdPatches
|
|
394
|
+
if (patches && readdPatches) {
|
|
383
395
|
for (const patch of readdPatches) patches.push(patch)
|
|
384
396
|
}
|
|
385
|
-
|
|
386
|
-
for (const patch of readdInvPatches) invPatches.push(patch)
|
|
387
|
-
}
|
|
397
|
+
for (const patch of readdInvPatches) invPatches.push(patch)
|
|
388
398
|
// We need to reverse once since inverse patches are applied in reverse.
|
|
389
399
|
// Repeated unshift calls here would make a large splice quadratic.
|
|
390
|
-
|
|
391
|
-
invPatches.reverse()
|
|
392
|
-
}
|
|
400
|
+
invPatches.reverse()
|
|
393
401
|
} else {
|
|
394
402
|
const interimLen = oldLen - removedCount
|
|
395
403
|
|
|
@@ -418,7 +426,7 @@ function arrayDidChangeSplice(
|
|
|
418
426
|
}
|
|
419
427
|
|
|
420
428
|
// add 0, 1, 2... since inverse patches are applied in reverse
|
|
421
|
-
invPatches
|
|
429
|
+
invPatches.push({
|
|
422
430
|
op: "add",
|
|
423
431
|
path,
|
|
424
432
|
value: freezeInternalSnapshot(oldSnapshot[realIndex]),
|
|
@@ -431,7 +439,7 @@ function arrayDidChangeSplice(
|
|
|
431
439
|
// optimization, for inverse patches, when adding from the end set the length to restore instead
|
|
432
440
|
const restoreUsingSetLength = index >= interimLen
|
|
433
441
|
if (restoreUsingSetLength) {
|
|
434
|
-
invPatches
|
|
442
|
+
invPatches.push({
|
|
435
443
|
op: "replace",
|
|
436
444
|
path: ["length"],
|
|
437
445
|
value: interimLen,
|
|
@@ -446,14 +454,12 @@ function arrayDidChangeSplice(
|
|
|
446
454
|
patches?.push({
|
|
447
455
|
op: "add",
|
|
448
456
|
path,
|
|
449
|
-
value: freezeInternalSnapshot(
|
|
450
|
-
getValueAfterSplice(oldSnapshot, realIndex, index, removedCount, addedItems)
|
|
451
|
-
),
|
|
457
|
+
value: freezeInternalSnapshot(addedItems[i]),
|
|
452
458
|
})
|
|
453
459
|
|
|
454
460
|
// remove ...2, 1, 0 since inverse patches are applied in reverse
|
|
455
461
|
if (!restoreUsingSetLength) {
|
|
456
|
-
invPatches
|
|
462
|
+
invPatches.push({
|
|
457
463
|
op: "remove",
|
|
458
464
|
path,
|
|
459
465
|
})
|
|
@@ -462,9 +468,7 @@ function arrayDidChangeSplice(
|
|
|
462
468
|
}
|
|
463
469
|
}
|
|
464
470
|
|
|
465
|
-
|
|
466
|
-
patchRecorder.record(patches, invPatches)
|
|
467
|
-
}
|
|
471
|
+
patchRecorder.record(patches, invPatches)
|
|
468
472
|
return mutate
|
|
469
473
|
}
|
|
470
474
|
|
|
@@ -473,6 +477,10 @@ function interceptArrayMutation(
|
|
|
473
477
|
array: IObservableArray,
|
|
474
478
|
change: IArrayWillChange | IArrayWillSplice
|
|
475
479
|
) {
|
|
480
|
+
if (!areHandlersActive(array)) {
|
|
481
|
+
// idle handler of an untweaked array
|
|
482
|
+
return change
|
|
483
|
+
}
|
|
476
484
|
assertCanWrite()
|
|
477
485
|
|
|
478
486
|
switch (change.type) {
|
|
@@ -509,9 +517,18 @@ function validateArrayMutationUpdate(change: IArrayWillChange) {
|
|
|
509
517
|
function interceptArrayMutationUpdate(change: IArrayWillChange, array: IObservableArray) {
|
|
510
518
|
// TODO: should be change.object, but mobx is bugged and doesn't send the proxy
|
|
511
519
|
const oldVal = array[change.index]
|
|
520
|
+
if (change.newValue === oldVal) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
assertCanAttachValue(change.newValue, (node) => node === oldVal)
|
|
512
524
|
tweak(oldVal, undefined) // set old prop obj parent to undefined
|
|
513
525
|
|
|
514
|
-
|
|
526
|
+
const paths = getArrayPaths(array, 0)
|
|
527
|
+
const entry = paths?.set(change.index, change.newValue)
|
|
528
|
+
change.newValue = tweak(change.newValue, entry ?? { parent: array, path: change.index })
|
|
529
|
+
if (entry) {
|
|
530
|
+
setLazyArrayParentPath(entry, change.newValue)
|
|
531
|
+
}
|
|
515
532
|
}
|
|
516
533
|
|
|
517
534
|
function validateArrayMutationSplice(change: IArrayWillSplice) {
|
|
@@ -526,12 +543,65 @@ function validateArrayMutationSplice(change: IArrayWillSplice) {
|
|
|
526
543
|
}
|
|
527
544
|
}
|
|
528
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Checks the added items can be attached before the splice detaches anything,
|
|
548
|
+
* so a rejected splice leaves every item as it was. Value types that are added
|
|
549
|
+
* to the array at the index they already have get cloned here, since
|
|
550
|
+
* `setParent` cannot tell they are being added again (it clones them when they
|
|
551
|
+
* are added anywhere else).
|
|
552
|
+
*/
|
|
553
|
+
function checkAddedItems(change: IArrayWillSplice) {
|
|
554
|
+
const array = change.object
|
|
555
|
+
const added = change.added
|
|
556
|
+
const removedEnd = change.index + change.removedCount
|
|
557
|
+
const isDetachedByChange: IsDetachedByChange = (_node, parentPath) =>
|
|
558
|
+
parentPath.parent === array &&
|
|
559
|
+
(parentPath.path as number) >= change.index &&
|
|
560
|
+
(parentPath.path as number) < removedEnd
|
|
561
|
+
|
|
562
|
+
assertCanAttach(added, isDetachedByChange, (value) => {
|
|
563
|
+
if (!isPrimitive(value) && isModel(value) && getModelMetadata(value).valueType) {
|
|
564
|
+
const parentPath = fastGetParentPath(value, false)
|
|
565
|
+
if (parentPath?.parent === array && !isDetachedByChange(value, parentPath)) {
|
|
566
|
+
return clone(value, { generateNewIds: true })
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return value
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
529
573
|
function interceptArrayMutationSplice(change: IArrayWillSplice) {
|
|
574
|
+
checkAddedItems(change)
|
|
575
|
+
|
|
576
|
+
const array = change.object
|
|
577
|
+
const oldNextIndex = change.index + change.removedCount
|
|
578
|
+
const paths =
|
|
579
|
+
change.removedCount !== change.added.length
|
|
580
|
+
? getArrayPaths(array, array.length - oldNextIndex)
|
|
581
|
+
: getArrayPaths(array, 0)
|
|
582
|
+
|
|
530
583
|
for (let i = 0; i < change.removedCount; i++) {
|
|
531
584
|
const removedValue = change.object[change.index + i]
|
|
532
585
|
tweak(removedValue, undefined)
|
|
533
586
|
}
|
|
534
587
|
|
|
588
|
+
if (paths) {
|
|
589
|
+
const addedPaths = paths.createPaths(change.index, change.added)
|
|
590
|
+
for (let i = 0; i < change.added.length; i++) {
|
|
591
|
+
const addedPath = addedPaths[i]
|
|
592
|
+
change.added[i] = tweak(
|
|
593
|
+
change.added[i],
|
|
594
|
+
addedPath ?? { parent: array, path: change.index + i }
|
|
595
|
+
)
|
|
596
|
+
if (addedPath) {
|
|
597
|
+
setLazyArrayParentPath(addedPath, change.added[i])
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// no reindexing: the items after the splice follow their chunks
|
|
601
|
+
paths.applySplice(change.index, change.removedCount, addedPaths)
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
535
605
|
for (let i = 0; i < change.added.length; i++) {
|
|
536
606
|
change.added[i] = tweak(change.added[i], {
|
|
537
607
|
parent: change.object,
|
|
@@ -540,7 +610,6 @@ function interceptArrayMutationSplice(change: IArrayWillSplice) {
|
|
|
540
610
|
}
|
|
541
611
|
|
|
542
612
|
// we might also need to update the parent of the next indexes
|
|
543
|
-
const oldNextIndex = change.index + change.removedCount
|
|
544
613
|
const newNextIndex = change.index + change.added.length
|
|
545
614
|
|
|
546
615
|
if (oldNextIndex !== newNextIndex && oldNextIndex < change.object.length) {
|
|
@@ -596,22 +665,3 @@ class ObservableArrayReplacementQueue {
|
|
|
596
665
|
}
|
|
597
666
|
}
|
|
598
667
|
}
|
|
599
|
-
|
|
600
|
-
function getValueAfterSplice<T>(
|
|
601
|
-
array: readonly T[],
|
|
602
|
-
i: number,
|
|
603
|
-
index: number,
|
|
604
|
-
remove: number,
|
|
605
|
-
addedItems: readonly T[]
|
|
606
|
-
) {
|
|
607
|
-
const base = i - index
|
|
608
|
-
if (base < 0) {
|
|
609
|
-
return array[i]
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (base < addedItems.length) {
|
|
613
|
-
return addedItems[base]
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return array[i - addedItems.length + remove]
|
|
617
|
-
}
|