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/snapshot/internal.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface SnapshotData {
|
|
|
19
19
|
atom: IAtom | undefined
|
|
20
20
|
// Source of truth for copy-on-write of this snapshot's untransformed data.
|
|
21
21
|
untransformedFrozen: boolean
|
|
22
|
+
// When defined while `untransformed` is not frozen, every value in it except
|
|
23
|
+
// (possibly) these is already frozen, so freezing it only needs to visit them.
|
|
24
|
+
// Values listed here may no longer be in `untransformed`.
|
|
25
|
+
unfrozenValues: object[] | undefined
|
|
22
26
|
dirtyChild: object | undefined
|
|
23
27
|
dirtyChildSnapshot: SnapshotData | undefined
|
|
24
28
|
dirtyChildPath: PathElement | undefined
|
|
@@ -94,6 +98,7 @@ export const setNewInternalSnapshot = action(
|
|
|
94
98
|
transformed,
|
|
95
99
|
atom: undefined, // will be created when first observed
|
|
96
100
|
untransformedFrozen: markAsFrozen,
|
|
101
|
+
unfrozenValues: undefined,
|
|
97
102
|
dirtyChild: undefined,
|
|
98
103
|
dirtyChildSnapshot: undefined,
|
|
99
104
|
dirtyChildPath: undefined,
|
|
@@ -108,12 +113,13 @@ export const setNewInternalSnapshot = action(
|
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
getOrCreateTreeNodeMetadata(value).snapshot = sn
|
|
111
|
-
|
|
112
|
-
sn.atom?.reportChanged()
|
|
113
116
|
}
|
|
114
117
|
)
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Mutates a snapshot container in place and returns the values it wrote.
|
|
121
|
+
*/
|
|
122
|
+
type MutateInternalSnapshotFn<T> = (prevSn: T) => readonly unknown[]
|
|
117
123
|
|
|
118
124
|
function makeSnapshotMutable(sn: SnapshotData): any {
|
|
119
125
|
let untransformed = sn.untransformed
|
|
@@ -127,7 +133,39 @@ function makeSnapshotMutable(sn: SnapshotData): any {
|
|
|
127
133
|
return untransformed
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
function
|
|
136
|
+
function trackUnfrozenValues(
|
|
137
|
+
sn: SnapshotData,
|
|
138
|
+
untransformed: any,
|
|
139
|
+
wasFrozen: boolean,
|
|
140
|
+
writtenValues: readonly unknown[]
|
|
141
|
+
): void {
|
|
142
|
+
// a cloned frozen container starts with only frozen values
|
|
143
|
+
let unfrozenValues = wasFrozen ? [] : sn.unfrozenValues
|
|
144
|
+
if (unfrozenValues === undefined) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
for (let i = 0; i < writtenValues.length; i++) {
|
|
148
|
+
const value = writtenValues[i]
|
|
149
|
+
if (!isPrimitive(value)) {
|
|
150
|
+
unfrozenValues.push(value)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const size = Array.isArray(untransformed)
|
|
154
|
+
? untransformed.length
|
|
155
|
+
: Object.keys(untransformed).length
|
|
156
|
+
if (unfrozenValues.length > size) {
|
|
157
|
+
// cheaper to visit the whole container again
|
|
158
|
+
unfrozenValues = undefined
|
|
159
|
+
}
|
|
160
|
+
sn.unfrozenValues = unfrozenValues
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setSnapshotData(
|
|
164
|
+
sn: SnapshotData,
|
|
165
|
+
untransformed: any,
|
|
166
|
+
writtenValues: readonly unknown[],
|
|
167
|
+
freezeIfCloned = false
|
|
168
|
+
): void {
|
|
131
169
|
// `makeSnapshotMutable` returns a new container exactly when this snapshot
|
|
132
170
|
// was frozen. All callers obtain `untransformed` through that helper.
|
|
133
171
|
const wasFrozen = untransformed !== sn.untransformed
|
|
@@ -138,6 +176,11 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
138
176
|
|
|
139
177
|
const keepFrozen = freezeIfCloned && wasFrozen && !sn.transformFn
|
|
140
178
|
sn.untransformedFrozen = keepFrozen
|
|
179
|
+
if (keepFrozen) {
|
|
180
|
+
sn.unfrozenValues = undefined
|
|
181
|
+
} else {
|
|
182
|
+
trackUnfrozenValues(sn, untransformed, wasFrozen, writtenValues)
|
|
183
|
+
}
|
|
141
184
|
if (wasFrozen && !keepFrozen) {
|
|
142
185
|
snapshotStates.set(untransformed, sn)
|
|
143
186
|
}
|
|
@@ -152,8 +195,8 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
152
195
|
|
|
153
196
|
function updateOwnSnapshot<T>(sn: SnapshotData, mutate: MutateInternalSnapshotFn<T>): void {
|
|
154
197
|
const untransformed = makeSnapshotMutable(sn)
|
|
155
|
-
mutate(untransformed)
|
|
156
|
-
setSnapshotData(sn, untransformed)
|
|
198
|
+
const writtenValues = mutate(untransformed)
|
|
199
|
+
setSnapshotData(sn, untransformed, writtenValues)
|
|
157
200
|
sn.atom?.reportChanged()
|
|
158
201
|
}
|
|
159
202
|
|
|
@@ -164,7 +207,7 @@ function updateParentSlot(parentSn: SnapshotData, path: PathElement, value: any)
|
|
|
164
207
|
} else {
|
|
165
208
|
untransformed[path] = value
|
|
166
209
|
}
|
|
167
|
-
setSnapshotData(parentSn, untransformed)
|
|
210
|
+
setSnapshotData(parentSn, untransformed, [value])
|
|
168
211
|
parentSn.atom?.reportChanged()
|
|
169
212
|
}
|
|
170
213
|
|
|
@@ -175,7 +218,8 @@ function flushDirtyChild(
|
|
|
175
218
|
childSn: SnapshotData,
|
|
176
219
|
childPath: PathElement,
|
|
177
220
|
freezeAfterFlush: boolean,
|
|
178
|
-
freezeClonedContainer: boolean
|
|
221
|
+
freezeClonedContainer: boolean,
|
|
222
|
+
writtenValues: unknown[]
|
|
179
223
|
): void {
|
|
180
224
|
if (childSn.isUnset) {
|
|
181
225
|
return
|
|
@@ -200,6 +244,7 @@ function flushDirtyChild(
|
|
|
200
244
|
} else {
|
|
201
245
|
untransformed[childPath] = childSn.transformed
|
|
202
246
|
}
|
|
247
|
+
writtenValues.push(childSn.transformed)
|
|
203
248
|
}
|
|
204
249
|
|
|
205
250
|
function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: boolean): boolean {
|
|
@@ -218,6 +263,7 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
218
263
|
const untransformed = makeSnapshotMutable(sn)
|
|
219
264
|
const freezeClonedContainer =
|
|
220
265
|
freezeAfterFlush && untransformed !== sn.untransformed && !sn.transformFn
|
|
266
|
+
const writtenValues: unknown[] = []
|
|
221
267
|
|
|
222
268
|
flushDirtyChild(
|
|
223
269
|
node,
|
|
@@ -226,7 +272,8 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
226
272
|
dirtyChildSnapshot,
|
|
227
273
|
dirtyChildPath,
|
|
228
274
|
freezeAfterFlush,
|
|
229
|
-
freezeClonedContainer
|
|
275
|
+
freezeClonedContainer,
|
|
276
|
+
writtenValues
|
|
230
277
|
)
|
|
231
278
|
additionalDirtyChildren?.forEach((dirtyChild, child) => {
|
|
232
279
|
flushDirtyChild(
|
|
@@ -236,11 +283,12 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
236
283
|
dirtyChild.snapshot,
|
|
237
284
|
dirtyChild.path,
|
|
238
285
|
freezeAfterFlush,
|
|
239
|
-
freezeClonedContainer
|
|
286
|
+
freezeClonedContainer,
|
|
287
|
+
writtenValues
|
|
240
288
|
)
|
|
241
289
|
})
|
|
242
290
|
|
|
243
|
-
setSnapshotData(sn, untransformed, freezeClonedContainer)
|
|
291
|
+
setSnapshotData(sn, untransformed, writtenValues, freezeClonedContainer)
|
|
244
292
|
return freezeClonedContainer
|
|
245
293
|
}
|
|
246
294
|
|
|
@@ -383,7 +431,13 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
383
431
|
freezeInternalSnapshot(owner.untransformed)
|
|
384
432
|
}
|
|
385
433
|
|
|
386
|
-
|
|
434
|
+
const unfrozenValues =
|
|
435
|
+
owner !== undefined && owner.untransformed === data ? owner.unfrozenValues : undefined
|
|
436
|
+
if (unfrozenValues !== undefined) {
|
|
437
|
+
for (let i = 0; i < unfrozenValues.length; i++) {
|
|
438
|
+
freezeInternalSnapshot(unfrozenValues[i])
|
|
439
|
+
}
|
|
440
|
+
} else if (Array.isArray(data)) {
|
|
387
441
|
for (let i = 0; i < data.length; i++) {
|
|
388
442
|
freezeInternalSnapshot(data[i])
|
|
389
443
|
}
|
|
@@ -398,6 +452,7 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
398
452
|
|
|
399
453
|
if (owner !== undefined && owner.untransformed === data) {
|
|
400
454
|
owner.untransformedFrozen = true
|
|
455
|
+
owner.unfrozenValues = undefined
|
|
401
456
|
}
|
|
402
457
|
|
|
403
458
|
return data
|
|
@@ -156,7 +156,9 @@ function reconcileModelSnapshot(
|
|
|
156
156
|
if (!isReservedModelKey(k)) {
|
|
157
157
|
const v = processedSn[k]
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
// an inherited member such as `toString` is no old value (but `__proto__`
|
|
160
|
+
// still reads the prototype, which is rejected as it is no tree node)
|
|
161
|
+
const oldValue = k === "__proto__" || hasOwnProp(data, k) ? data[k] : undefined
|
|
160
162
|
// Non-null primitives need neither reconciliation nor detachment.
|
|
161
163
|
// Nullish values must still pass through default handling below.
|
|
162
164
|
if (v != null && isPrimitive(v)) {
|
|
@@ -216,10 +218,10 @@ function snapshotSlotsAreEqual(
|
|
|
216
218
|
for (let i = 0; i < currentKeys.length; i++) {
|
|
217
219
|
const key = currentKeys[i]
|
|
218
220
|
// a matching key at the same index proves the key is an own property of
|
|
219
|
-
// the incoming snapshot without an
|
|
221
|
+
// the incoming snapshot without an own-property check; canonical snapshots
|
|
220
222
|
// keep key order, so the fallback is rare (never trust inherited values,
|
|
221
223
|
// e.g. a crafted __proto__ chain, without an own-property proof)
|
|
222
|
-
if (incomingKeys[i] !== key && !
|
|
224
|
+
if (incomingKeys[i] !== key && !hasOwnProp(incomingSnapshot, key)) {
|
|
223
225
|
return false
|
|
224
226
|
}
|
|
225
227
|
if (currentSnapshot[key] !== incomingSnapshot[key]) {
|
|
@@ -48,7 +48,9 @@ function reconcilePlainObjectSnapshot(
|
|
|
48
48
|
const k = snKeys[i]
|
|
49
49
|
const v = sn[k]
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// an inherited member such as `toString` is no old value (but `__proto__`
|
|
52
|
+
// still reads the prototype, which is rejected as it is no tree node)
|
|
53
|
+
const oldValue = k === "__proto__" || hasOwnProp(plainObj, k) ? plainObj[k] : undefined
|
|
52
54
|
const newValue = withErrorPathSegment(k, () =>
|
|
53
55
|
reconcileSnapshot(oldValue, v, modelPool, plainObj)
|
|
54
56
|
)
|
package/src/treeUtils/draft.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { applySnapshot } from "../snapshot/applySnapshot"
|
|
|
6
6
|
import { fromSnapshot } from "../snapshot/fromSnapshot"
|
|
7
7
|
import { getSnapshot } from "../snapshot/getSnapshot"
|
|
8
8
|
import { assertTweakedObject } from "../tweaker/core"
|
|
9
|
-
import { failure, mobxAction, mobxComputed } from "../utils"
|
|
9
|
+
import { failure, getMobxVersion, makeObservableCompat, mobxAction, mobxComputed } from "../utils"
|
|
10
10
|
import { deepEquals } from "./deepEquals"
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -157,6 +157,11 @@ export class Draft<T extends object> {
|
|
|
157
157
|
assertTweakedObject(original, "original")
|
|
158
158
|
|
|
159
159
|
this.originalData = original
|
|
160
|
+
|
|
161
|
+
if (getMobxVersion() >= 6) {
|
|
162
|
+
makeObservableCompat(this)
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
this.data = fromSnapshot(this.originalSnapshot, { generateNewIds: false })
|
|
161
166
|
}
|
|
162
167
|
}
|
package/src/treeUtils/sandbox.ts
CHANGED
|
@@ -233,9 +233,11 @@ export class SandboxManager {
|
|
|
233
233
|
// see the reverted intermediate state.
|
|
234
234
|
runInAction(() => {
|
|
235
235
|
this.allowWrite(() => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
applyPatches(
|
|
237
|
+
this.subtreeRootClone,
|
|
238
|
+
events.map((event) => event.inversePatches),
|
|
239
|
+
true
|
|
240
|
+
)
|
|
239
241
|
})
|
|
240
242
|
applyPatches(
|
|
241
243
|
this.subtreeRoot,
|
|
@@ -249,10 +251,11 @@ export class SandboxManager {
|
|
|
249
251
|
const wasRecording = recorder.recording
|
|
250
252
|
recorder.recording = false
|
|
251
253
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
applyPatches(
|
|
255
|
+
this.subtreeRootClone,
|
|
256
|
+
recorder.events.slice(numRecorderEvents).map((event) => event.inversePatches),
|
|
257
|
+
true
|
|
258
|
+
)
|
|
256
259
|
recorder.events.splice(numRecorderEvents)
|
|
257
260
|
} finally {
|
|
258
261
|
recorder.recording = wasRecording
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { type IArrayDidChange, type IObjectDidChange, remove, set } from "mobx"
|
|
2
|
+
import type { AnyModel } from "../model/BaseModel"
|
|
3
|
+
import type { DelayedError } from "../utils/forEachWithDelayedThrow"
|
|
4
|
+
import { forEachTypedModelAncestor, isTypeCheckingAfterChangeEnabled } from "./typeChecking"
|
|
5
|
+
import { withoutTypeChecking } from "./withoutTypeChecking"
|
|
6
|
+
|
|
7
|
+
// Rolling back tree changes that failed partway (not to be confused with the
|
|
8
|
+
// undo manager, which records user-level history): a change batch makes
|
|
9
|
+
// `applySnapshot` / `applyPatches` all or nothing, and rollback recorders make
|
|
10
|
+
// transactions revertible. Both restore the live values each change replaced.
|
|
11
|
+
|
|
12
|
+
interface ChangeBatch {
|
|
13
|
+
/** Whether the changed models are type checked when the batch completes. */
|
|
14
|
+
validate: boolean
|
|
15
|
+
models: Set<AnyModel>
|
|
16
|
+
/** How to revert each recorded change, oldest first. */
|
|
17
|
+
reverts: (() => void)[]
|
|
18
|
+
recording: boolean
|
|
19
|
+
/** Last recorded change target, so a run of changes to it walks ancestors once. */
|
|
20
|
+
previousTarget?: object
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let changeBatch: ChangeBatch | undefined
|
|
24
|
+
|
|
25
|
+
const noListenerError = Symbol("noListenerError")
|
|
26
|
+
let lastListenerError: unknown = noListenerError
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @internal
|
|
30
|
+
* Rethrows the errors thrown by the listeners of an applied change. A change
|
|
31
|
+
* batch does not roll back for them: the change itself was valid.
|
|
32
|
+
*/
|
|
33
|
+
export function throwDelayedListenerError(delayedError: DelayedError): void {
|
|
34
|
+
if (delayedError) {
|
|
35
|
+
lastListenerError = delayedError.value
|
|
36
|
+
throw delayedError.value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
* Leaves the type check of a new model to the validating change batch being
|
|
43
|
+
* recorded, if any. Returns whether it did.
|
|
44
|
+
*/
|
|
45
|
+
export function deferModelTypeCheckToBatch(model: AnyModel): boolean {
|
|
46
|
+
if (changeBatch?.recording && changeBatch.validate) {
|
|
47
|
+
changeBatch.models.add(model)
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @internal
|
|
55
|
+
* Records how to revert every change made while `recording` is set, wherever
|
|
56
|
+
* it happens, until disposed. Used to roll back failed transactions.
|
|
57
|
+
*/
|
|
58
|
+
export class ChangeRollbackRecorder {
|
|
59
|
+
recording = false
|
|
60
|
+
readonly reverts: (() => void)[] = []
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
rollbackRecorders.add(this)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
dispose(): void {
|
|
67
|
+
rollbackRecorders.delete(this)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rollbackRecorders = new Set<ChangeRollbackRecorder>()
|
|
72
|
+
|
|
73
|
+
// (a suspended transaction keeps its recorder without recording)
|
|
74
|
+
function isAnyRollbackRecorderRecording(): boolean {
|
|
75
|
+
for (const recorder of rollbackRecorders) {
|
|
76
|
+
if (recorder.recording) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @internal
|
|
85
|
+
* Records how to revert a change for the change batch and the rollback
|
|
86
|
+
* recorders. It captures stored values before any listener can mutate them again. Restoring
|
|
87
|
+
* live values preserves identity and literal data, without snapshot processors
|
|
88
|
+
* or model defaults changing the state being restored.
|
|
89
|
+
*/
|
|
90
|
+
export function recordChangeForRollback(change: IObjectDidChange | IArrayDidChange): void {
|
|
91
|
+
const batch = changeBatch?.recording ? changeBatch : undefined
|
|
92
|
+
if (!batch && !isAnyRollbackRecorderRecording()) return
|
|
93
|
+
if (
|
|
94
|
+
change.type === "splice" &&
|
|
95
|
+
change.addedCount === change.removedCount &&
|
|
96
|
+
change.added.every((value, index) => value === change.removed[index])
|
|
97
|
+
)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
const target = change.object
|
|
101
|
+
if (batch?.validate && target !== batch.previousTarget) {
|
|
102
|
+
forEachTypedModelAncestor(target, (model) => batch.models.add(model))
|
|
103
|
+
batch.previousTarget = target
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const revert = createRevert(change)
|
|
107
|
+
batch?.reverts.push(revert)
|
|
108
|
+
for (const recorder of rollbackRecorders) {
|
|
109
|
+
if (recorder.recording) {
|
|
110
|
+
recorder.reverts.push(revert)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createRevert(change: IObjectDidChange | IArrayDidChange): () => void {
|
|
116
|
+
if (change.type === "splice") {
|
|
117
|
+
const { object, index, addedCount } = change
|
|
118
|
+
const removed = change.removed.slice()
|
|
119
|
+
return () => object.spliceWithArray(index, addedCount, removed)
|
|
120
|
+
}
|
|
121
|
+
if ("index" in change) {
|
|
122
|
+
const { object, index, oldValue } = change
|
|
123
|
+
return () => {
|
|
124
|
+
object[index] = oldValue
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const { object } = change
|
|
128
|
+
const key = change.name as string
|
|
129
|
+
if (change.type === "add") {
|
|
130
|
+
return () => remove(object, key)
|
|
131
|
+
}
|
|
132
|
+
const { oldValue } = change
|
|
133
|
+
return () => set(object, key, oldValue)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reverts the changes recorded since `start`, newest first. Rollback publishes
|
|
138
|
+
* compensating changes, just like snapshot reconciliation.
|
|
139
|
+
*/
|
|
140
|
+
function rollBackBatch(batch: ChangeBatch, start: number): void {
|
|
141
|
+
batch.recording = false
|
|
142
|
+
try {
|
|
143
|
+
withoutTypeChecking(() => {
|
|
144
|
+
for (let i = batch.reverts.length - 1; i >= start; i--) {
|
|
145
|
+
try {
|
|
146
|
+
batch.reverts[i]()
|
|
147
|
+
} catch {
|
|
148
|
+
// Keep restoring after a rollback listener throws. The original
|
|
149
|
+
// failure takes precedence over errors from compensating changes.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
} finally {
|
|
154
|
+
batch.reverts.length = start
|
|
155
|
+
batch.recording = true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @internal
|
|
161
|
+
* Applies the changes made by `fn` as a whole: when it throws, or when the
|
|
162
|
+
* changed models fail automatic type checking afterwards, the changes are
|
|
163
|
+
* rolled back before the error is rethrown. Errors thrown by the listeners of
|
|
164
|
+
* a change do not roll back, since the change itself was valid.
|
|
165
|
+
* A nested call only rolls back its own changes, while validation runs once
|
|
166
|
+
* the outermost call completes. Synchronous listener edits join the batch.
|
|
167
|
+
*/
|
|
168
|
+
export function withChangeBatch(fn: () => void): void {
|
|
169
|
+
const outer = changeBatch
|
|
170
|
+
if (outer) {
|
|
171
|
+
if (!outer.recording) {
|
|
172
|
+
// (rolling back)
|
|
173
|
+
fn()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
const start = outer.reverts.length
|
|
177
|
+
try {
|
|
178
|
+
fn()
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error !== lastListenerError) {
|
|
181
|
+
rollBackBatch(outer, start)
|
|
182
|
+
}
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const batch: ChangeBatch = {
|
|
189
|
+
validate: isTypeCheckingAfterChangeEnabled(),
|
|
190
|
+
models: new Set(),
|
|
191
|
+
reverts: [],
|
|
192
|
+
recording: true,
|
|
193
|
+
}
|
|
194
|
+
changeBatch = batch
|
|
195
|
+
lastListenerError = noListenerError
|
|
196
|
+
let validating = false
|
|
197
|
+
try {
|
|
198
|
+
if (batch.validate) {
|
|
199
|
+
withoutTypeChecking(fn)
|
|
200
|
+
// MobX has now invalidated dependencies, including added/removed keys.
|
|
201
|
+
validating = true
|
|
202
|
+
for (const model of batch.models) {
|
|
203
|
+
const error = model.typeCheck()
|
|
204
|
+
if (error) error.throw()
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
fn()
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (validating || error !== lastListenerError) {
|
|
211
|
+
rollBackBatch(batch, 0)
|
|
212
|
+
}
|
|
213
|
+
throw error
|
|
214
|
+
} finally {
|
|
215
|
+
changeBatch = undefined
|
|
216
|
+
lastListenerError = noListenerError
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/tweaker/core.ts
CHANGED
|
@@ -1,43 +1,56 @@
|
|
|
1
1
|
import { runInAction } from "mobx"
|
|
2
2
|
import { hasDataObjectParent } from "../parent/core"
|
|
3
3
|
import { failure, isPrimitive } from "../utils"
|
|
4
|
-
import { treeNodeMetadata } from "./treeNodeMetadata"
|
|
4
|
+
import { type TreeNodeMetadata, treeNodeMetadata } from "./treeNodeMetadata"
|
|
5
|
+
import { resyncTweakedChangeListeners } from "./tweakedChangeListeners"
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// untweaked objects that keep the MobX handlers they got when tweaked, which
|
|
8
|
+
// do nothing until they are tweaked again
|
|
9
|
+
const objectsWithIdleHandlers = new WeakSet<object>()
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Activates the MobX handlers of a fully tweaked plain object or array, and
|
|
13
|
+
* returns whether they still have to be installed. Untweaking keeps them idle
|
|
14
|
+
* rather than disposing them, so no disposers are kept for every node. When
|
|
15
|
+
* idle handlers are reused, the tweaked change listeners are resynced, since
|
|
16
|
+
* they missed the changes made while the handlers were idle.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function activateHandlers(value: object): boolean {
|
|
21
|
+
treeNodeMetadata.get(value)!.handlersActive = true
|
|
22
|
+
if (!objectsWithIdleHandlers.delete(value)) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
resyncTweakedChangeListeners(value)
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
/**
|
|
13
|
-
*
|
|
14
|
-
* close over them. The primary function is internal and never exposed.
|
|
30
|
+
* Makes the MobX handlers of an untweaked plain object or array idle.
|
|
15
31
|
*
|
|
16
32
|
* @internal
|
|
17
33
|
*/
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
secondaryUntweaker: () => void
|
|
22
|
-
): void {
|
|
23
|
-
primaryUntweaker[secondaryUntweakerSymbol] = secondaryUntweaker
|
|
24
|
-
treeNodeMetadata.get(value)!.untweaker = primaryUntweaker
|
|
34
|
+
export function deactivateHandlers(value: object, metadata: TreeNodeMetadata): void {
|
|
35
|
+
metadata.handlersActive = false
|
|
36
|
+
objectsWithIdleHandlers.add(value)
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/**
|
|
40
|
+
* Whether the MobX handlers of a plain object or array are active.
|
|
41
|
+
*
|
|
28
42
|
* @internal
|
|
29
43
|
*/
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
return metadata?.tweaked === true && (canBeDataObject || metadata.dataObjectParent === undefined)
|
|
44
|
+
export function areHandlersActive(value: object): boolean {
|
|
45
|
+
return treeNodeMetadata.get(value)?.handlersActive === true
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
/**
|
|
36
49
|
* @internal
|
|
37
50
|
*/
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
export function isTweakedObject(value: unknown, canBeDataObject: boolean): value is object {
|
|
52
|
+
const metadata = treeNodeMetadata.get(value as object)
|
|
53
|
+
return metadata?.tweaked === true && (canBeDataObject || metadata.dataObjectParent === undefined)
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
/**
|
|
@@ -5,11 +5,20 @@ import type { SnapshotData } from "../snapshot/internal"
|
|
|
5
5
|
/** @internal */
|
|
6
6
|
export interface TreeNodeMetadata {
|
|
7
7
|
tweaked: boolean
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Set once a plain object or array is fully tweaked, and unset when it is
|
|
10
|
+
* untweaked. Its MobX handlers do nothing while unset.
|
|
11
|
+
*/
|
|
12
|
+
handlersActive: boolean
|
|
9
13
|
parentPath: ParentPath<object> | undefined
|
|
10
14
|
dataObjectParent: object | undefined
|
|
11
15
|
objectChildren: ObjectChildrenData | undefined
|
|
12
16
|
snapshot: SnapshotData | undefined
|
|
17
|
+
/**
|
|
18
|
+
* Increases every time the node is added to a parent's children, so it
|
|
19
|
+
* orders siblings like the parent's children set does.
|
|
20
|
+
*/
|
|
21
|
+
attachOrder: number
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
/** @internal */
|
|
@@ -21,11 +30,12 @@ export function getOrCreateTreeNodeMetadata(value: object): TreeNodeMetadata {
|
|
|
21
30
|
if (!metadata) {
|
|
22
31
|
metadata = {
|
|
23
32
|
tweaked: false,
|
|
24
|
-
|
|
33
|
+
handlersActive: false,
|
|
25
34
|
parentPath: undefined,
|
|
26
35
|
dataObjectParent: undefined,
|
|
27
36
|
objectChildren: undefined,
|
|
28
37
|
snapshot: undefined,
|
|
38
|
+
attachOrder: 0,
|
|
29
39
|
}
|
|
30
40
|
treeNodeMetadata.set(value, metadata)
|
|
31
41
|
}
|