mobx-keystone 1.25.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/dist/mobx-keystone.esm.js +3627 -1540
- package/dist/mobx-keystone.esm.mjs +3627 -1540
- package/dist/mobx-keystone.umd.js +3638 -1543
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +3 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
- package/dist/types/ref/nodesById.d.ts +1 -0
- package/dist/types/ref/refIdIndex.d.ts +1 -0
- package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
- package/dist/types/snapshot/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
- package/dist/types/utils/index.d.ts +13 -0
- package/dist/types/utils/mapUtils.d.ts +2 -2
- package/dist/types/utils/setLike.d.ts +1 -0
- package/package.json +2 -2
- package/src/action/applyAction.ts +1 -1
- package/src/action/middleware.ts +17 -36
- package/src/action/modelFlow.ts +4 -5
- package/src/action/mutationBatch.ts +10 -10
- package/src/action/pendingActions.ts +3 -4
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
- package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
- package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
- package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
- package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
- package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
- package/src/actionMiddlewares/undoMiddleware.ts +112 -59
- package/src/actionMiddlewares/utils.ts +2 -1
- package/src/computedTree/computedTree.ts +14 -3
- package/src/context/context.ts +17 -45
- package/src/dataModel/BaseDataModel.ts +20 -16
- package/src/dataModel/dataModelInstanceCache.ts +8 -0
- package/src/deepChange/onDeepChange.ts +179 -24
- package/src/frozen/Frozen.ts +50 -33
- package/src/model/BaseModel.ts +10 -13
- package/src/model/newModel.ts +21 -24
- package/src/model/utils.ts +2 -2
- package/src/modelShared/modelClassInitializer.ts +3 -2
- package/src/modelShared/modelDecorator.ts +39 -39
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +157 -55
- package/src/parent/path.ts +15 -5
- package/src/parent/setParent.ts +34 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +12 -101
- package/src/patch/applyPatches.ts +74 -34
- package/src/patch/emitPatch.ts +142 -26
- package/src/patch/jsonPatch.ts +5 -0
- package/src/patch/patchRecorder.ts +18 -14
- package/src/redux/connectReduxDevTools.ts +24 -18
- package/src/redux/redux.ts +5 -21
- package/src/ref/core.ts +81 -29
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/attachDetach.ts +27 -16
- package/src/rootStore/rootStore.ts +10 -6
- package/src/snapshot/SnapshotOf.ts +2 -2
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +89 -19
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +6 -6
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/standardActions/arrayActions.ts +12 -2
- package/src/standardActions/objectActions.ts +3 -2
- package/src/transforms/ImmutableDate.ts +4 -0
- package/src/transforms/asMap.ts +3 -1
- package/src/treeUtils/deepEquals.ts +15 -13
- package/src/treeUtils/draft.ts +8 -0
- package/src/treeUtils/sandbox.ts +31 -27
- package/src/tweaker/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -22
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +183 -19
- package/src/tweaker/tweakArray.ts +176 -33
- package/src/tweaker/tweakPlainObject.ts +117 -39
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +25 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +541 -88
- package/src/types/objectBased/typesDataModelData.ts +4 -2
- package/src/types/objectBased/typesModel.ts +4 -2
- package/src/types/objectBased/typesObject.ts +20 -16
- package/src/types/objectBased/typesRecord.ts +8 -18
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesEnum.ts +4 -2
- package/src/types/primitiveBased/typesPrimitive.ts +9 -4
- package/src/types/typeCheckErrorUtils.ts +1 -0
- package/src/types/utility/typesCodecSupport.ts +97 -56
- package/src/types/utility/typesMap.ts +24 -41
- package/src/types/utility/typesRefinement.ts +3 -2
- package/src/types/utility/typesSet.ts +24 -0
- package/src/utils/ModelPool.ts +78 -22
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +1 -1
- package/src/utils/errorDiagnostics.ts +6 -2
- package/src/utils/forEachWithDelayedThrow.ts +83 -0
- package/src/utils/index.ts +71 -2
- package/src/utils/mapUtils.ts +3 -7
- package/src/utils/setIfDifferent.ts +3 -2
- package/src/utils/setLike.ts +134 -0
- package/src/wrappers/ArraySet.ts +5 -9
- package/src/wrappers/ObjectMap.ts +15 -11
- package/src/wrappers/asMap.ts +30 -18
- package/src/wrappers/asSet.ts +36 -12
- package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
- package/src/action/modelFlowPromiseGenerator.ts +0 -127
package/src/treeUtils/sandbox.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { createContext } from "../context"
|
|
|
7
7
|
import { getParentToChildPath, resolvePath } from "../parent/path"
|
|
8
8
|
import { applyPatches } from "../patch/applyPatches"
|
|
9
9
|
import { onPatches } from "../patch/emitPatch"
|
|
10
|
-
import type { Patch } from "../patch/Patch"
|
|
11
10
|
import { type PatchRecorder, patchRecorder } from "../patch/patchRecorder"
|
|
12
11
|
import {
|
|
13
12
|
fastIsRootStoreNoAtom,
|
|
@@ -78,11 +77,6 @@ export class SandboxManager {
|
|
|
78
77
|
*/
|
|
79
78
|
private allowWrite: ReadonlyMiddlewareReturn["allowWrite"]
|
|
80
79
|
|
|
81
|
-
/**
|
|
82
|
-
* Whether changes made in the sandbox are currently being committed to the original subtree.
|
|
83
|
-
*/
|
|
84
|
-
private isCommitting = false
|
|
85
|
-
|
|
86
80
|
private readonly subtreeRoot: object
|
|
87
81
|
|
|
88
82
|
/**
|
|
@@ -127,11 +121,9 @@ export class SandboxManager {
|
|
|
127
121
|
if (this.withSandboxPatchRecorder) {
|
|
128
122
|
throw failure("original subtree must not change while 'withSandbox' executes")
|
|
129
123
|
}
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
}
|
|
124
|
+
this.allowWrite(() => {
|
|
125
|
+
applyPatches(this.subtreeRootClone, patches)
|
|
126
|
+
})
|
|
135
127
|
})
|
|
136
128
|
|
|
137
129
|
const { allowWrite, dispose: disposeReadonlyMW } = readonlyMiddleware(this.subtreeRootClone)
|
|
@@ -232,26 +224,38 @@ export class SandboxManager {
|
|
|
232
224
|
}
|
|
233
225
|
if (commit) {
|
|
234
226
|
if (!isNestedWithSandboxCall) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
227
|
+
const events = recorder.events
|
|
228
|
+
// The clone already holds these changes, so revert them and let the regular
|
|
229
|
+
// patch sync mirror the original's final state back in. That way anything a
|
|
230
|
+
// listener changes on the original during the commit reaches the clone too,
|
|
231
|
+
// at a cost proportional to the changes rather than to the tree size.
|
|
232
|
+
// Both halves run in a single action so that observers of the sandbox never
|
|
233
|
+
// see the reverted intermediate state.
|
|
234
|
+
runInAction(() => {
|
|
235
|
+
this.allowWrite(() => {
|
|
236
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
237
|
+
applyPatches(this.subtreeRootClone, events[i].inversePatches, true)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
applyPatches(
|
|
241
|
+
this.subtreeRoot,
|
|
242
|
+
events.map((event) => event.patches)
|
|
243
|
+
)
|
|
244
|
+
})
|
|
248
245
|
}
|
|
249
246
|
} else {
|
|
250
247
|
this.allowWrite(() => {
|
|
251
248
|
runInAction(() => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
249
|
+
const wasRecording = recorder.recording
|
|
250
|
+
recorder.recording = false
|
|
251
|
+
try {
|
|
252
|
+
let i = recorder.events.length
|
|
253
|
+
while (i-- > numRecorderEvents) {
|
|
254
|
+
applyPatches(this.subtreeRootClone, recorder.events[i].inversePatches, true)
|
|
255
|
+
}
|
|
256
|
+
recorder.events.splice(numRecorderEvents)
|
|
257
|
+
} finally {
|
|
258
|
+
recorder.recording = wasRecording
|
|
255
259
|
}
|
|
256
260
|
})
|
|
257
261
|
})
|
|
@@ -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,45 +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 false
|
|
33
|
-
}
|
|
34
|
-
return treeNodeMetadata.get(value as object)?.tweaked === true
|
|
44
|
+
export function areHandlersActive(value: object): boolean {
|
|
45
|
+
return treeNodeMetadata.get(value)?.handlersActive === true
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
49
|
* @internal
|
|
39
50
|
*/
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
|
|
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)
|
|
43
54
|
}
|
|
44
55
|
|
|
45
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
|
}
|
package/src/tweaker/tweak.ts
CHANGED
|
@@ -1,14 +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 {
|
|
11
|
-
|
|
14
|
+
import {
|
|
15
|
+
failure,
|
|
16
|
+
inDevMode,
|
|
17
|
+
isArray,
|
|
18
|
+
isMap,
|
|
19
|
+
isObject,
|
|
20
|
+
isPlainObject,
|
|
21
|
+
isPrimitive,
|
|
22
|
+
isSet,
|
|
23
|
+
} from "../utils"
|
|
24
|
+
import { getSafeErrorValuePreview } from "../utils/errorDiagnostics"
|
|
25
|
+
import { deactivateHandlers, isTweakedObject } from "./core"
|
|
12
26
|
import { registerDefaultTweakers } from "./registerDefaultTweakers"
|
|
13
27
|
import { treeNodeMetadata } from "./treeNodeMetadata"
|
|
14
28
|
|
|
@@ -99,9 +113,7 @@ function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T
|
|
|
99
113
|
|
|
100
114
|
// unsupported (must go before plain object tweaker)
|
|
101
115
|
if (isDataModel(value)) {
|
|
102
|
-
throw
|
|
103
|
-
"data models are not directly supported. you may insert the data in the tree instead ('$' property)."
|
|
104
|
-
)
|
|
116
|
+
throw dataModelsNotSupportedError()
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
registerDefaultTweakers()
|
|
@@ -115,18 +127,24 @@ function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T
|
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
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) {
|
|
119
140
|
if (isMap(value)) {
|
|
120
|
-
|
|
141
|
+
return failure("maps are not directly supported. consider using 'ObjectMap' / 'asMap' instead.")
|
|
121
142
|
}
|
|
122
|
-
|
|
123
|
-
// unsupported
|
|
124
143
|
if (isSet(value)) {
|
|
125
|
-
|
|
144
|
+
return failure("sets are not directly supported. consider using 'ArraySet' / 'asSet' instead.")
|
|
126
145
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`tweak can only work over models, observable objects/arrays, or primitives, but got ${value} instead`
|
|
146
|
+
return failure(
|
|
147
|
+
`tweak can only work over models, observable objects/arrays, or primitives, but got ${getSafeErrorValuePreview(value)} instead`
|
|
130
148
|
)
|
|
131
149
|
}
|
|
132
150
|
|
|
@@ -140,6 +158,154 @@ export function tweak<T>(value: T, parentPath: ParentPath<any> | undefined): T {
|
|
|
140
158
|
return isPrimitive(value) ? value : tweakNonPrimitive(value, parentPath)
|
|
141
159
|
}
|
|
142
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
|
+
|
|
143
309
|
/**
|
|
144
310
|
* @internal
|
|
145
311
|
*/
|
|
@@ -155,8 +321,7 @@ export function tryUntweak(value: any): (() => void) | undefined {
|
|
|
155
321
|
}
|
|
156
322
|
|
|
157
323
|
const metadata = treeNodeMetadata.get(value)
|
|
158
|
-
|
|
159
|
-
if (!untweaker) {
|
|
324
|
+
if (!metadata?.handlersActive) {
|
|
160
325
|
return undefined
|
|
161
326
|
}
|
|
162
327
|
|
|
@@ -176,14 +341,13 @@ export function tryUntweak(value: any): (() => void) | undefined {
|
|
|
176
341
|
}
|
|
177
342
|
|
|
178
343
|
return () => {
|
|
179
|
-
// post-untweaking
|
|
180
|
-
|
|
344
|
+
// post-untweaking
|
|
345
|
+
deactivateHandlers(value, metadata)
|
|
181
346
|
|
|
182
347
|
// Match the former independent-map ordering: the node stops being tweaked
|
|
183
348
|
// before its snapshot is unset, but the shared record must stay alive until
|
|
184
349
|
// snapshot cleanup has consumed it.
|
|
185
350
|
metadata.tweaked = false
|
|
186
|
-
metadata.untweaker = undefined
|
|
187
351
|
unsetInternalSnapshot(value)
|
|
188
352
|
// These associations previously lived in independent WeakMaps and remain
|
|
189
353
|
// valid across untweak/retweak, so keep their shared record when present.
|