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/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,
|
|
@@ -113,7 +118,10 @@ export const setNewInternalSnapshot = action(
|
|
|
113
118
|
}
|
|
114
119
|
)
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Mutates a snapshot container in place and returns the values it wrote.
|
|
123
|
+
*/
|
|
124
|
+
type MutateInternalSnapshotFn<T> = (prevSn: T) => readonly unknown[]
|
|
117
125
|
|
|
118
126
|
function makeSnapshotMutable(sn: SnapshotData): any {
|
|
119
127
|
let untransformed = sn.untransformed
|
|
@@ -127,7 +135,39 @@ function makeSnapshotMutable(sn: SnapshotData): any {
|
|
|
127
135
|
return untransformed
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
function
|
|
138
|
+
function trackUnfrozenValues(
|
|
139
|
+
sn: SnapshotData,
|
|
140
|
+
untransformed: any,
|
|
141
|
+
wasFrozen: boolean,
|
|
142
|
+
writtenValues: readonly unknown[]
|
|
143
|
+
): void {
|
|
144
|
+
// a cloned frozen container starts with only frozen values
|
|
145
|
+
let unfrozenValues = wasFrozen ? [] : sn.unfrozenValues
|
|
146
|
+
if (unfrozenValues === undefined) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < writtenValues.length; i++) {
|
|
150
|
+
const value = writtenValues[i]
|
|
151
|
+
if (!isPrimitive(value)) {
|
|
152
|
+
unfrozenValues.push(value)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const size = Array.isArray(untransformed)
|
|
156
|
+
? untransformed.length
|
|
157
|
+
: Object.keys(untransformed).length
|
|
158
|
+
if (unfrozenValues.length > size) {
|
|
159
|
+
// cheaper to visit the whole container again
|
|
160
|
+
unfrozenValues = undefined
|
|
161
|
+
}
|
|
162
|
+
sn.unfrozenValues = unfrozenValues
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setSnapshotData(
|
|
166
|
+
sn: SnapshotData,
|
|
167
|
+
untransformed: any,
|
|
168
|
+
writtenValues: readonly unknown[],
|
|
169
|
+
freezeIfCloned = false
|
|
170
|
+
): void {
|
|
131
171
|
// `makeSnapshotMutable` returns a new container exactly when this snapshot
|
|
132
172
|
// was frozen. All callers obtain `untransformed` through that helper.
|
|
133
173
|
const wasFrozen = untransformed !== sn.untransformed
|
|
@@ -138,6 +178,11 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
138
178
|
|
|
139
179
|
const keepFrozen = freezeIfCloned && wasFrozen && !sn.transformFn
|
|
140
180
|
sn.untransformedFrozen = keepFrozen
|
|
181
|
+
if (keepFrozen) {
|
|
182
|
+
sn.unfrozenValues = undefined
|
|
183
|
+
} else {
|
|
184
|
+
trackUnfrozenValues(sn, untransformed, wasFrozen, writtenValues)
|
|
185
|
+
}
|
|
141
186
|
if (wasFrozen && !keepFrozen) {
|
|
142
187
|
snapshotStates.set(untransformed, sn)
|
|
143
188
|
}
|
|
@@ -152,8 +197,8 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
152
197
|
|
|
153
198
|
function updateOwnSnapshot<T>(sn: SnapshotData, mutate: MutateInternalSnapshotFn<T>): void {
|
|
154
199
|
const untransformed = makeSnapshotMutable(sn)
|
|
155
|
-
mutate(untransformed)
|
|
156
|
-
setSnapshotData(sn, untransformed)
|
|
200
|
+
const writtenValues = mutate(untransformed)
|
|
201
|
+
setSnapshotData(sn, untransformed, writtenValues)
|
|
157
202
|
sn.atom?.reportChanged()
|
|
158
203
|
}
|
|
159
204
|
|
|
@@ -164,7 +209,7 @@ function updateParentSlot(parentSn: SnapshotData, path: PathElement, value: any)
|
|
|
164
209
|
} else {
|
|
165
210
|
untransformed[path] = value
|
|
166
211
|
}
|
|
167
|
-
setSnapshotData(parentSn, untransformed)
|
|
212
|
+
setSnapshotData(parentSn, untransformed, [value])
|
|
168
213
|
parentSn.atom?.reportChanged()
|
|
169
214
|
}
|
|
170
215
|
|
|
@@ -175,7 +220,8 @@ function flushDirtyChild(
|
|
|
175
220
|
childSn: SnapshotData,
|
|
176
221
|
childPath: PathElement,
|
|
177
222
|
freezeAfterFlush: boolean,
|
|
178
|
-
freezeClonedContainer: boolean
|
|
223
|
+
freezeClonedContainer: boolean,
|
|
224
|
+
writtenValues: unknown[]
|
|
179
225
|
): void {
|
|
180
226
|
if (childSn.isUnset) {
|
|
181
227
|
return
|
|
@@ -200,6 +246,7 @@ function flushDirtyChild(
|
|
|
200
246
|
} else {
|
|
201
247
|
untransformed[childPath] = childSn.transformed
|
|
202
248
|
}
|
|
249
|
+
writtenValues.push(childSn.transformed)
|
|
203
250
|
}
|
|
204
251
|
|
|
205
252
|
function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: boolean): boolean {
|
|
@@ -218,6 +265,7 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
218
265
|
const untransformed = makeSnapshotMutable(sn)
|
|
219
266
|
const freezeClonedContainer =
|
|
220
267
|
freezeAfterFlush && untransformed !== sn.untransformed && !sn.transformFn
|
|
268
|
+
const writtenValues: unknown[] = []
|
|
221
269
|
|
|
222
270
|
flushDirtyChild(
|
|
223
271
|
node,
|
|
@@ -226,7 +274,8 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
226
274
|
dirtyChildSnapshot,
|
|
227
275
|
dirtyChildPath,
|
|
228
276
|
freezeAfterFlush,
|
|
229
|
-
freezeClonedContainer
|
|
277
|
+
freezeClonedContainer,
|
|
278
|
+
writtenValues
|
|
230
279
|
)
|
|
231
280
|
additionalDirtyChildren?.forEach((dirtyChild, child) => {
|
|
232
281
|
flushDirtyChild(
|
|
@@ -236,11 +285,12 @@ function flushDirtyChildren(node: object, sn: SnapshotData, freezeAfterFlush: bo
|
|
|
236
285
|
dirtyChild.snapshot,
|
|
237
286
|
dirtyChild.path,
|
|
238
287
|
freezeAfterFlush,
|
|
239
|
-
freezeClonedContainer
|
|
288
|
+
freezeClonedContainer,
|
|
289
|
+
writtenValues
|
|
240
290
|
)
|
|
241
291
|
})
|
|
242
292
|
|
|
243
|
-
setSnapshotData(sn, untransformed, freezeClonedContainer)
|
|
293
|
+
setSnapshotData(sn, untransformed, writtenValues, freezeClonedContainer)
|
|
244
294
|
return freezeClonedContainer
|
|
245
295
|
}
|
|
246
296
|
|
|
@@ -383,7 +433,13 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
383
433
|
freezeInternalSnapshot(owner.untransformed)
|
|
384
434
|
}
|
|
385
435
|
|
|
386
|
-
|
|
436
|
+
const unfrozenValues =
|
|
437
|
+
owner !== undefined && owner.untransformed === data ? owner.unfrozenValues : undefined
|
|
438
|
+
if (unfrozenValues !== undefined) {
|
|
439
|
+
for (let i = 0; i < unfrozenValues.length; i++) {
|
|
440
|
+
freezeInternalSnapshot(unfrozenValues[i])
|
|
441
|
+
}
|
|
442
|
+
} else if (Array.isArray(data)) {
|
|
387
443
|
for (let i = 0; i < data.length; i++) {
|
|
388
444
|
freezeInternalSnapshot(data[i])
|
|
389
445
|
}
|
|
@@ -398,6 +454,7 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
398
454
|
|
|
399
455
|
if (owner !== undefined && owner.untransformed === data) {
|
|
400
456
|
owner.untransformedFrozen = true
|
|
457
|
+
owner.unfrozenValues = undefined
|
|
401
458
|
}
|
|
402
459
|
|
|
403
460
|
return data
|
|
@@ -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
|
}
|