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/ref/core.ts
CHANGED
|
@@ -2,16 +2,14 @@ import { action, type ObservableSet, observable, reaction, when } from "mobx"
|
|
|
2
2
|
import { isModel } from "../model/utils"
|
|
3
3
|
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
4
4
|
import { model } from "../modelShared/modelDecorator"
|
|
5
|
-
import {
|
|
6
|
-
getDeepObjectChildren,
|
|
7
|
-
registerDeepObjectChildrenExtension,
|
|
8
|
-
} from "../parent/coreObjectChildren"
|
|
5
|
+
import { registerDeepObjectChildrenExtension } from "../parent/coreObjectChildren"
|
|
9
6
|
import { fastGetRoot } from "../parent/path"
|
|
10
|
-
import { type ComputedWalkTreeAggregate, computedWalkTreeAggregate } from "../parent/walkTree"
|
|
11
7
|
import { assertTweakedObject } from "../tweaker/core"
|
|
12
8
|
import { assertIsObject, failure, mobxAction } from "../utils"
|
|
13
9
|
import { getOrCreate } from "../utils/mapUtils"
|
|
10
|
+
import { modelRefIdNotIndexed, resolveModelRefId } from "./modelRefIdIndex"
|
|
14
11
|
import { Ref, type RefConstructor } from "./Ref"
|
|
12
|
+
import { resolveRefIdByIndex } from "./refIdIndex"
|
|
15
13
|
|
|
16
14
|
interface BackRefs<T extends object> {
|
|
17
15
|
all: ObservableSet<Ref<T>>
|
|
@@ -88,10 +86,26 @@ export function internalCustomRef<T extends object>(
|
|
|
88
86
|
trackedRefs.add(this as Ref<object>)
|
|
89
87
|
this.internalForceUpdateBackRefs(initialTarget)
|
|
90
88
|
|
|
89
|
+
// MobX 4/5 do not pass the previous value to reaction effects.
|
|
90
|
+
// The reaction is usually created inside an action, so its first tracking run is deferred
|
|
91
|
+
// to the end of the batch and `initialTarget` may be stale by then; capture the value of
|
|
92
|
+
// that first run instead, since that is the one the effect will be compared against.
|
|
93
|
+
let previousTarget = initialTarget
|
|
94
|
+
let firstTrackingRun = true
|
|
91
95
|
// according to mwestrate this won't leak as long as we don't keep the disposer around
|
|
92
96
|
reaction(
|
|
93
|
-
() =>
|
|
94
|
-
|
|
97
|
+
() => {
|
|
98
|
+
const target = this.maybeCurrent
|
|
99
|
+
if (firstTrackingRun) {
|
|
100
|
+
firstTrackingRun = false
|
|
101
|
+
previousTarget = target
|
|
102
|
+
this.internalForceUpdateBackRefs(target)
|
|
103
|
+
}
|
|
104
|
+
return target
|
|
105
|
+
},
|
|
106
|
+
(newTarget) => {
|
|
107
|
+
const oldTarget = previousTarget
|
|
108
|
+
previousTarget = newTarget
|
|
95
109
|
this.internalForceUpdateBackRefs(newTarget)
|
|
96
110
|
|
|
97
111
|
if (onResolvedValueChange && newTarget !== oldTarget) {
|
|
@@ -176,12 +190,6 @@ export function getModelRefId(target: object): string | undefined {
|
|
|
176
190
|
return undefined
|
|
177
191
|
}
|
|
178
192
|
|
|
179
|
-
// one computed id tree per id function
|
|
180
|
-
const computedIdTrees = new WeakMap<
|
|
181
|
-
(node: object) => string | undefined,
|
|
182
|
-
ComputedWalkTreeAggregate<string>
|
|
183
|
-
>()
|
|
184
|
-
|
|
185
193
|
/**
|
|
186
194
|
* Resolves a node given its ID.
|
|
187
195
|
*
|
|
@@ -196,13 +204,20 @@ export function resolveId<T extends object>(
|
|
|
196
204
|
id: string,
|
|
197
205
|
getId: RefIdResolver = getModelRefId
|
|
198
206
|
): T | undefined {
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
207
|
+
// parent resolutions have priority over child ones
|
|
208
|
+
if (getId(root) === id) {
|
|
209
|
+
return root as T
|
|
210
|
+
}
|
|
203
211
|
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
if (getId === getModelRefId) {
|
|
213
|
+
const found = resolveModelRefId(root, id)
|
|
214
|
+
if (found !== modelRefIdNotIndexed) {
|
|
215
|
+
return found as T | undefined
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// custom ids (or models with a custom `getRefId`)
|
|
220
|
+
return resolveRefIdByIndex(root, id, getId) as T | undefined
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
function getBackRefs<T extends object>(
|
|
@@ -269,14 +284,25 @@ export function getRefsResolvingTo<T extends object>(
|
|
|
269
284
|
oldBackRefs.forEach(updateRef)
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
const refsChildrenOfRoot = getDeepChildrenRefs(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
refs
|
|
287
|
+
const refsChildrenOfRoot = getDeepChildrenRefs.get(fastGetRoot(target, true))
|
|
288
|
+
if (shouldForceUpdateTrackedRefs) {
|
|
289
|
+
const refs = refType ? refsChildrenOfRoot.byType.get(refType.refClass) : refsChildrenOfRoot.all
|
|
290
|
+
refs?.forEach(updateRef)
|
|
276
291
|
} else {
|
|
277
|
-
refs
|
|
292
|
+
// tracked refs keep their back-references up to date by themselves
|
|
293
|
+
const untrackedRefs = refType
|
|
294
|
+
? refsChildrenOfRoot.untrackedByType.get(refType.refClass)
|
|
295
|
+
: refsChildrenOfRoot.untracked
|
|
296
|
+
if (untrackedRefs && untrackedRefs.size > 0) {
|
|
297
|
+
// copy, since updating back-references might change the tree
|
|
298
|
+
for (const ref of Array.from(untrackedRefs)) {
|
|
299
|
+
updateRef(ref)
|
|
300
|
+
if (trackedRefs.has(ref)) {
|
|
301
|
+
deleteRef(refsChildrenOfRoot.untracked, refsChildrenOfRoot.untrackedByType, ref)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
278
305
|
}
|
|
279
|
-
refs?.forEach(updateRef)
|
|
280
306
|
|
|
281
307
|
return getBackRefs(target, refType)
|
|
282
308
|
}
|
|
@@ -316,9 +342,24 @@ function isReactionDelayed() {
|
|
|
316
342
|
return reactionDelayed
|
|
317
343
|
}
|
|
318
344
|
|
|
345
|
+
type RefsByType = WeakMap<ModelClass<Ref<any>>, Set<Ref<any>>>
|
|
346
|
+
|
|
319
347
|
interface DeepChildrenRefs {
|
|
320
348
|
all: Set<Ref<any>>
|
|
321
|
-
byType:
|
|
349
|
+
byType: RefsByType
|
|
350
|
+
/** Refs whose back-references might not be tracked yet. */
|
|
351
|
+
untracked: Set<Ref<any>>
|
|
352
|
+
untrackedByType: RefsByType
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function addRef(all: Set<Ref<any>>, byType: RefsByType, ref: Ref<any>) {
|
|
356
|
+
all.add(ref)
|
|
357
|
+
getOrCreate(byType, ref.constructor as ModelClass<Ref<any>>, () => new Set()).add(ref)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function deleteRef(all: Set<Ref<any>>, byType: RefsByType, ref: Ref<any>) {
|
|
361
|
+
all.delete(ref)
|
|
362
|
+
byType.get(ref.constructor as ModelClass<Ref<any>>)?.delete(ref)
|
|
322
363
|
}
|
|
323
364
|
|
|
324
365
|
const getDeepChildrenRefs = registerDeepObjectChildrenExtension<DeepChildrenRefs>({
|
|
@@ -326,14 +367,25 @@ const getDeepChildrenRefs = registerDeepObjectChildrenExtension<DeepChildrenRefs
|
|
|
326
367
|
return {
|
|
327
368
|
all: new Set(),
|
|
328
369
|
byType: new WeakMap(),
|
|
370
|
+
untracked: new Set(),
|
|
371
|
+
untrackedByType: new WeakMap(),
|
|
329
372
|
}
|
|
330
373
|
},
|
|
331
374
|
|
|
332
375
|
addNode(node, data) {
|
|
333
376
|
if (node instanceof Ref) {
|
|
334
|
-
data.all.
|
|
335
|
-
|
|
336
|
-
|
|
377
|
+
addRef(data.all, data.byType, node)
|
|
378
|
+
if (!trackedRefs.has(node)) {
|
|
379
|
+
addRef(data.untracked, data.untrackedByType, node)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
removeNode(node, data) {
|
|
385
|
+
if (node instanceof Ref) {
|
|
386
|
+
deleteRef(data.all, data.byType, node)
|
|
387
|
+
deleteRef(data.untracked, data.untrackedByType, node)
|
|
337
388
|
}
|
|
389
|
+
return true
|
|
338
390
|
},
|
|
339
391
|
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createAtom, type IAtom } from "mobx"
|
|
2
|
+
import { type AnyModel, BaseModel } from "../model/BaseModel"
|
|
3
|
+
import { modelIdKey } from "../model/metadata"
|
|
4
|
+
import { registerDeepObjectChildrenExtension } from "../parent/coreObjectChildren"
|
|
5
|
+
import { NodesById } from "./nodesById"
|
|
6
|
+
|
|
7
|
+
interface ModelRefIdIndex {
|
|
8
|
+
readonly byId: NodesById<AnyModel>
|
|
9
|
+
/** Id each model is indexed under, since ids can change later. */
|
|
10
|
+
readonly indexedIds: Map<AnyModel, string>
|
|
11
|
+
/** Models with a custom `getRefId`, which cannot be indexed. */
|
|
12
|
+
customRefIdModels: number
|
|
13
|
+
/** Changed when models with a custom `getRefId` appear or disappear. */
|
|
14
|
+
readonly customRefIdAtom: IAtom
|
|
15
|
+
/** Changed when the models with a given id change. Only kept while observed. */
|
|
16
|
+
readonly idAtoms: Map<string, IAtom>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasCustomRefId(model: AnyModel) {
|
|
20
|
+
return model.getRefId !== BaseModel.prototype.getRefId
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function reportIdChanged(index: ModelRefIdIndex, id: string) {
|
|
24
|
+
index.idAtoms.get(id)?.reportChanged()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const modelRefIdIndex = registerDeepObjectChildrenExtension<ModelRefIdIndex>({
|
|
28
|
+
initData() {
|
|
29
|
+
return {
|
|
30
|
+
byId: new NodesById(),
|
|
31
|
+
indexedIds: new Map(),
|
|
32
|
+
customRefIdModels: 0,
|
|
33
|
+
customRefIdAtom: createAtom("customRefIdModels"),
|
|
34
|
+
idAtoms: new Map(),
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
addNode(node, index) {
|
|
39
|
+
if (!(node instanceof BaseModel)) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (hasCustomRefId(node)) {
|
|
43
|
+
index.customRefIdModels++
|
|
44
|
+
if (index.customRefIdModels === 1) {
|
|
45
|
+
index.customRefIdAtom.reportChanged()
|
|
46
|
+
}
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const id = node[modelIdKey]
|
|
51
|
+
if (id === undefined) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
index.byId.add(id, node)
|
|
55
|
+
index.indexedIds.set(node, id)
|
|
56
|
+
reportIdChanged(index, id)
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
removeNode(node, index) {
|
|
60
|
+
if (!(node instanceof BaseModel)) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
if (hasCustomRefId(node)) {
|
|
64
|
+
index.customRefIdModels--
|
|
65
|
+
if (index.customRefIdModels === 0) {
|
|
66
|
+
index.customRefIdAtom.reportChanged()
|
|
67
|
+
}
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const id = index.indexedIds.get(node)
|
|
72
|
+
if (id === undefined) {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
index.indexedIds.delete(node)
|
|
76
|
+
index.byId.delete(id, node)
|
|
77
|
+
reportIdChanged(index, id)
|
|
78
|
+
return true
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
dependsOnModelIds: true,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function observeId(index: ModelRefIdIndex, id: string) {
|
|
85
|
+
const existing = index.idAtoms.get(id)
|
|
86
|
+
if (existing) {
|
|
87
|
+
existing.reportObserved()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const atom = createAtom("modelRefId", undefined, () => {
|
|
91
|
+
if (index.idAtoms.get(id) === atom) {
|
|
92
|
+
index.idAtoms.delete(id)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
// only keep atoms something observes, since nothing else would remove them
|
|
96
|
+
if (atom.reportObserved()) {
|
|
97
|
+
index.idAtoms.set(id, atom)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Result of `resolveModelRefId` when the tree has models with a custom
|
|
103
|
+
* `getRefId`, which it cannot index.
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export const modelRefIdNotIndexed = Symbol("modelRefIdNotIndexed")
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolves a model below the root (excluding it) by its default reference id
|
|
111
|
+
* (`getModelRefId`), reactively, in O(1), or O(models × depth) when several
|
|
112
|
+
* models have that id.
|
|
113
|
+
* Returns `modelRefIdNotIndexed` when the tree has models with a custom
|
|
114
|
+
* `getRefId`.
|
|
115
|
+
*
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
export function resolveModelRefId(
|
|
119
|
+
root: object,
|
|
120
|
+
id: string
|
|
121
|
+
): object | undefined | typeof modelRefIdNotIndexed {
|
|
122
|
+
const index = modelRefIdIndex.get(root, false)
|
|
123
|
+
|
|
124
|
+
index.customRefIdAtom.reportObserved()
|
|
125
|
+
if (index.customRefIdModels > 0) {
|
|
126
|
+
return modelRefIdNotIndexed
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
observeId(index, id)
|
|
130
|
+
return index.byId.resolve(root, id)
|
|
131
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { compareTreePaths, getPathFromAncestor } from "../parent/treeOrder"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The nodes below a root indexed by id, where several nodes may share an id.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export class NodesById<T extends object> {
|
|
9
|
+
// (tree nodes are never sets)
|
|
10
|
+
private readonly byId = new Map<string, T | Set<T>>()
|
|
11
|
+
|
|
12
|
+
add(id: string, node: T): void {
|
|
13
|
+
const existing = this.byId.get(id)
|
|
14
|
+
if (existing === undefined) {
|
|
15
|
+
this.byId.set(id, node)
|
|
16
|
+
} else if (existing instanceof Set) {
|
|
17
|
+
existing.add(node)
|
|
18
|
+
} else {
|
|
19
|
+
this.byId.set(id, new Set([existing, node]))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
delete(id: string, node: T): void {
|
|
24
|
+
const existing = this.byId.get(id)
|
|
25
|
+
if (existing instanceof Set) {
|
|
26
|
+
existing.delete(node)
|
|
27
|
+
if (existing.size === 1) {
|
|
28
|
+
this.byId.set(id, existing.values().next().value!)
|
|
29
|
+
}
|
|
30
|
+
} else if (existing === node) {
|
|
31
|
+
this.byId.delete(id)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolves the node with the id, picking one when several share it. */
|
|
36
|
+
resolve(root: object, id: string): T | undefined {
|
|
37
|
+
const found = this.byId.get(id)
|
|
38
|
+
return found instanceof Set ? resolveDuplicatedId(root, found) : found
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Picks which of several nodes below the root sharing an id it resolves to:
|
|
44
|
+
* an ancestor wins over its descendants, and otherwise the node below the
|
|
45
|
+
* later attached child of their closest common ancestor wins. That is the last
|
|
46
|
+
* one a post-order walk of the tree would find. Reactive, in
|
|
47
|
+
* O(nodes × depth).
|
|
48
|
+
*/
|
|
49
|
+
function resolveDuplicatedId<T extends object>(root: object, nodes: Iterable<T>): T {
|
|
50
|
+
let winner: T | undefined
|
|
51
|
+
let winnerPath: object[] | undefined
|
|
52
|
+
for (const node of nodes) {
|
|
53
|
+
const path = getPathFromAncestor(root, node, true)!
|
|
54
|
+
if (!winnerPath || compareTreePaths(path, winnerPath, false) > 0) {
|
|
55
|
+
winner = node
|
|
56
|
+
winnerPath = path
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return winner!
|
|
60
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { computed, createAtom, type IComputedValue, Reaction } from "mobx"
|
|
2
|
+
import {
|
|
3
|
+
type DeepObjectChildrenExtensionAccessor,
|
|
4
|
+
registerDeepObjectChildrenExtension,
|
|
5
|
+
} from "../parent/coreObjectChildren"
|
|
6
|
+
import { ChunkedSet, type SequenceChunk } from "../utils/chunks"
|
|
7
|
+
import { getOrCreate } from "../utils/mapUtils"
|
|
8
|
+
import { NodesById } from "./nodesById"
|
|
9
|
+
|
|
10
|
+
type GetId = (target: object) => string | undefined
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A group of nodes, with a computed map of their current ids, so an id change
|
|
14
|
+
* only re-reads the ids of the nodes in its chunk.
|
|
15
|
+
*/
|
|
16
|
+
class IdChunk implements SequenceChunk<object> {
|
|
17
|
+
items: object[]
|
|
18
|
+
/** Changed when nodes are added or removed. */
|
|
19
|
+
readonly atom = createAtom("refIdChunk")
|
|
20
|
+
readonly ids: IComputedValue<Map<string, object | object[]>>
|
|
21
|
+
/** Ids last copied to the index. */
|
|
22
|
+
syncedIds: Map<string, object | object[]> | undefined
|
|
23
|
+
|
|
24
|
+
constructor(nodes: object[], getId: GetId) {
|
|
25
|
+
this.items = nodes
|
|
26
|
+
this.ids = computed(() => {
|
|
27
|
+
this.atom.reportObserved()
|
|
28
|
+
const ids = new Map<string, object | object[]>()
|
|
29
|
+
for (const node of this.items) {
|
|
30
|
+
const id = getId(node)
|
|
31
|
+
if (id === undefined) {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
const existing = ids.get(id)
|
|
35
|
+
if (existing === undefined) {
|
|
36
|
+
ids.set(id, node)
|
|
37
|
+
} else if (Array.isArray(existing)) {
|
|
38
|
+
existing.push(node)
|
|
39
|
+
} else {
|
|
40
|
+
ids.set(id, [existing, node])
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return ids
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class RefIdIndex extends ChunkedSet<object, IdChunk> {
|
|
49
|
+
/** Changed when chunks are added or removed. */
|
|
50
|
+
readonly chunksAtom = createAtom("refIdChunks")
|
|
51
|
+
readonly byId = new NodesById<object>()
|
|
52
|
+
/** Copies the chunks' id changes into `byId`. Changes when `byId` does. */
|
|
53
|
+
readonly synced: IComputedValue<number>
|
|
54
|
+
/** Set when `byId` changed outside of a sync, so the version must change. */
|
|
55
|
+
private changedOutsideSync = false
|
|
56
|
+
keeper: Reaction | undefined
|
|
57
|
+
private readonly getId: GetId
|
|
58
|
+
|
|
59
|
+
constructor(getId: GetId) {
|
|
60
|
+
super()
|
|
61
|
+
this.getId = getId
|
|
62
|
+
let version = 0
|
|
63
|
+
this.synced = computed(
|
|
64
|
+
() => {
|
|
65
|
+
this.chunksAtom.reportObserved()
|
|
66
|
+
let changed = this.changedOutsideSync
|
|
67
|
+
this.changedOutsideSync = false
|
|
68
|
+
for (const chunk of this.chunks) {
|
|
69
|
+
if (this.syncChunk(chunk)) {
|
|
70
|
+
changed = true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (changed) {
|
|
74
|
+
version++
|
|
75
|
+
}
|
|
76
|
+
return version
|
|
77
|
+
},
|
|
78
|
+
{ name: "refIdIndex" }
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected createChunk(nodes: object[]): IdChunk {
|
|
83
|
+
return new IdChunk(nodes, this.getId)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected onChunkChanged(chunk: IdChunk): void {
|
|
87
|
+
chunk.atom.reportChanged()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected onChunksChanged(): void {
|
|
91
|
+
this.chunksAtom.reportChanged()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected onChunkDropped(chunk: IdChunk): void {
|
|
95
|
+
// its nodes (if any) moved to a chunk that will add their ids again
|
|
96
|
+
const synced = chunk.syncedIds
|
|
97
|
+
if (synced && synced.size > 0) {
|
|
98
|
+
for (const [id, value] of synced) {
|
|
99
|
+
forEachNode(value, (node) => this.byId.delete(id, node))
|
|
100
|
+
}
|
|
101
|
+
this.changedOutsideSync = true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private syncChunk(chunk: IdChunk): boolean {
|
|
106
|
+
const ids = chunk.ids.get()
|
|
107
|
+
const previous = chunk.syncedIds
|
|
108
|
+
if (ids === previous) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
chunk.syncedIds = ids
|
|
112
|
+
|
|
113
|
+
let changed = false
|
|
114
|
+
if (previous) {
|
|
115
|
+
for (const [id, value] of previous) {
|
|
116
|
+
if (!sameNodes(ids.get(id), value)) {
|
|
117
|
+
forEachNode(value, (node) => this.byId.delete(id, node))
|
|
118
|
+
changed = true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const [id, value] of ids) {
|
|
123
|
+
if (!sameNodes(previous?.get(id), value)) {
|
|
124
|
+
forEachNode(value, (node) => this.byId.add(id, node))
|
|
125
|
+
changed = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return changed
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function forEachNode(value: object | object[], fn: (node: object) => void) {
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
value.forEach(fn)
|
|
135
|
+
} else {
|
|
136
|
+
fn(value)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sameNodes(a: object | object[] | undefined, b: object | object[]) {
|
|
141
|
+
if (a === b) {
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
return a.every((node, i) => node === b[i])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createRefIdIndexExtension(getId: GetId) {
|
|
151
|
+
return registerDeepObjectChildrenExtension<RefIdIndex>({
|
|
152
|
+
initData() {
|
|
153
|
+
return new RefIdIndex(getId)
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
addNode(node, index) {
|
|
157
|
+
index.add(node)
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
removeNode(node, index) {
|
|
161
|
+
index.delete(node)
|
|
162
|
+
return true
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const refIdIndexes = new WeakMap<GetId, DeepObjectChildrenExtensionAccessor<RefIdIndex>>()
|
|
168
|
+
|
|
169
|
+
// Outside reactions and actions MobX does not cache computeds, so every
|
|
170
|
+
// untracked resolution would re-read every id. Keep the index observed until
|
|
171
|
+
// any of its ids change, then release it, so ids read from outside the tree
|
|
172
|
+
// cannot keep it alive.
|
|
173
|
+
function keepAliveUntilChanged(index: RefIdIndex) {
|
|
174
|
+
if (index.keeper) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
const keeper = new Reaction("refIdIndex keeper", () => {
|
|
178
|
+
keeper.dispose()
|
|
179
|
+
if (index.keeper === keeper) {
|
|
180
|
+
index.keeper = undefined
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
index.keeper = keeper
|
|
184
|
+
keeper.track(() => {
|
|
185
|
+
try {
|
|
186
|
+
index.synced.get()
|
|
187
|
+
} catch {
|
|
188
|
+
// the caller's own read rethrows the cached error
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolves a node below the root (excluding it) whose `getId` is the given
|
|
195
|
+
* id, reactively, in O(tree size / 128) plus the cost of re-reading the ids of
|
|
196
|
+
* any chunk of 128 nodes that changed, plus O(nodes × depth) when several
|
|
197
|
+
* nodes have that id.
|
|
198
|
+
*
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
export function resolveRefIdByIndex(root: object, id: string, getId: GetId): object | undefined {
|
|
202
|
+
const accessor = getOrCreate(refIdIndexes, getId, () => createRefIdIndexExtension(getId))
|
|
203
|
+
const index = accessor.get(root, false)
|
|
204
|
+
|
|
205
|
+
keepAliveUntilChanged(index)
|
|
206
|
+
index.synced.get()
|
|
207
|
+
|
|
208
|
+
return index.byId.resolve(root, id)
|
|
209
|
+
}
|
|
@@ -4,9 +4,10 @@ import { HookAction } from "../action/hookActions"
|
|
|
4
4
|
import { wrapInAction, wrapModelMethodInActionIfNeeded } from "../action/wrapInAction"
|
|
5
5
|
import { type AnyModel, BaseModel } from "../model/BaseModel"
|
|
6
6
|
import { WalkTreeMode, walkTree } from "../parent/walkTree"
|
|
7
|
+
import { forEachWithDelayedThrow } from "../utils/forEachWithDelayedThrow"
|
|
7
8
|
|
|
8
9
|
const onAttachedDisposers = new WeakMap<object, () => void>()
|
|
9
|
-
const attachedToRootStore = new
|
|
10
|
+
const attachedToRootStore = new WeakMap<object, object>()
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @internal
|
|
@@ -16,6 +17,7 @@ export const attachToRootStore = action(
|
|
|
16
17
|
(rootStore: object, child: object): void => {
|
|
17
18
|
// we use an array to ensure they will get called even if the actual hook modifies the tree
|
|
18
19
|
const childrenToCall: AnyModel[] = []
|
|
20
|
+
const attachment = {}
|
|
19
21
|
|
|
20
22
|
walkTree(
|
|
21
23
|
child,
|
|
@@ -25,7 +27,7 @@ export const attachToRootStore = action(
|
|
|
25
27
|
if (attachedToRootStore.has(ch)) {
|
|
26
28
|
return
|
|
27
29
|
}
|
|
28
|
-
attachedToRootStore.
|
|
30
|
+
attachedToRootStore.set(ch, attachment)
|
|
29
31
|
|
|
30
32
|
if (ch instanceof BaseModel && (ch as any).onAttachedToRootStore) {
|
|
31
33
|
wrapModelMethodInActionIfNeeded(
|
|
@@ -40,15 +42,23 @@ export const attachToRootStore = action(
|
|
|
40
42
|
WalkTreeMode.ParentFirst
|
|
41
43
|
)
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
forEachWithDelayedThrow(childrenToCall, (ch) => {
|
|
46
|
+
// An earlier hook may have detached or reattached this node already.
|
|
47
|
+
if (attachedToRootStore.get(ch) !== attachment) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
const disposer = (ch as any).onAttachedToRootStore(rootStore)
|
|
48
52
|
if (disposer) {
|
|
49
|
-
|
|
53
|
+
if (attachedToRootStore.get(ch) === attachment) {
|
|
54
|
+
onAttachedDisposers.set(ch, disposer)
|
|
55
|
+
} else {
|
|
56
|
+
// The hook detached or reattached this node before returning its cleanup.
|
|
57
|
+
// Dispose this attachment without overwriting a newer attachment's cleanup.
|
|
58
|
+
wrapDisposer(ch, disposer)()
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
|
-
}
|
|
61
|
+
})
|
|
52
62
|
}
|
|
53
63
|
)
|
|
54
64
|
|
|
@@ -69,11 +79,7 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
|
|
|
69
79
|
const disposer = onAttachedDisposers.get(ch)
|
|
70
80
|
if (disposer) {
|
|
71
81
|
// wrap disposer in action
|
|
72
|
-
const disposerAction =
|
|
73
|
-
nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
|
|
74
|
-
fn: disposer,
|
|
75
|
-
actionType: ActionContextActionType.Sync,
|
|
76
|
-
}).bind(ch)
|
|
82
|
+
const disposerAction = wrapDisposer(ch, disposer)
|
|
77
83
|
onAttachedDisposers.delete(ch)
|
|
78
84
|
|
|
79
85
|
disposersToCall.push(disposerAction)
|
|
@@ -82,8 +88,13 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
|
|
|
82
88
|
WalkTreeMode.ChildrenFirst
|
|
83
89
|
)
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
for (let i = 0; i < disposersToCallLen; i++) {
|
|
87
|
-
disposersToCall[i]()
|
|
88
|
-
}
|
|
91
|
+
forEachWithDelayedThrow(disposersToCall, (dispose) => dispose())
|
|
89
92
|
})
|
|
93
|
+
|
|
94
|
+
function wrapDisposer(node: object, disposer: () => void): () => void {
|
|
95
|
+
return wrapInAction({
|
|
96
|
+
nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
|
|
97
|
+
fn: disposer,
|
|
98
|
+
actionType: ActionContextActionType.Sync,
|
|
99
|
+
}).bind(node)
|
|
100
|
+
}
|
|
@@ -45,9 +45,11 @@ export const registerRootStore: <T extends object>(node: T) => T = action(
|
|
|
45
45
|
|
|
46
46
|
entry.is = true
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
try {
|
|
49
|
+
attachToRootStore(node, node)
|
|
50
|
+
} finally {
|
|
51
|
+
entry.atom?.reportChanged()
|
|
52
|
+
}
|
|
51
53
|
return node
|
|
52
54
|
}
|
|
53
55
|
)
|
|
@@ -65,9 +67,11 @@ export const unregisterRootStore: (node: object) => void = action("unregisterRoo
|
|
|
65
67
|
const entry = getOrCreateRootStoreEntry(node)
|
|
66
68
|
entry.is = false
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
try {
|
|
71
|
+
detachFromRootStore(node)
|
|
72
|
+
} finally {
|
|
73
|
+
entry.atom?.reportChanged()
|
|
74
|
+
}
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
/**
|