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
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { action, createAtom, type IAtom } from "mobx"
|
|
1
|
+
import { action, createAtom, type IAtom, untracked } from "mobx"
|
|
2
2
|
import { addMutationBatchFinisher, isMutationBatchActive } from "../action/mutationBatch"
|
|
3
|
-
import { getOrCreateTreeNodeMetadata } from "../tweaker/treeNodeMetadata"
|
|
3
|
+
import { getOrCreateTreeNodeMetadata, treeNodeMetadata } from "../tweaker/treeNodeMetadata"
|
|
4
4
|
import { fastGetParent } from "./path"
|
|
5
5
|
|
|
6
6
|
interface DeepObjectChildren {
|
|
7
7
|
deep: Set<object>
|
|
8
|
-
|
|
9
|
-
extensionsData: Map<object, unknown> | undefined
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
/** @internal */
|
|
@@ -16,6 +14,22 @@ export interface ObjectChildrenData extends DeepObjectChildren {
|
|
|
16
14
|
|
|
17
15
|
deepDirty: boolean
|
|
18
16
|
deepAtom: IAtom | undefined // will be created when first observed
|
|
17
|
+
|
|
18
|
+
extensions: Map<DeepObjectChildrenExtension<any>, ExtensionState<any>> | undefined
|
|
19
|
+
|
|
20
|
+
listeners: Set<ObjectChildrenListener> | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Told about the nodes that may have been attached to or detached from the
|
|
25
|
+
* children of a node: its direct children, or all of its descendants when
|
|
26
|
+
* `deep`. Called inside the action making the change.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export interface ObjectChildrenListener {
|
|
31
|
+
readonly deep: boolean
|
|
32
|
+
onChildrenChanged(nodes: readonly object[]): void
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
interface DeepChildrenInvalidationEntry {
|
|
@@ -64,6 +78,10 @@ function getOrCreateDeepChildrenMutationTransaction(): DeepChildrenMutationTrans
|
|
|
64
78
|
return transaction
|
|
65
79
|
}
|
|
66
80
|
|
|
81
|
+
// shared by all nodes whose deep children were never built (it is never mutated,
|
|
82
|
+
// but replaced when they are built), so each node does not allocate an empty set
|
|
83
|
+
const unbuiltDeepChildren = new Set<object>()
|
|
84
|
+
|
|
67
85
|
function getObjectChildrenObject(node: object) {
|
|
68
86
|
const metadata = getOrCreateTreeNodeMetadata(node)
|
|
69
87
|
let obj = metadata.objectChildren
|
|
@@ -73,11 +91,13 @@ function getObjectChildrenObject(node: object) {
|
|
|
73
91
|
shallow: new Set(),
|
|
74
92
|
shallowAtom: undefined, // will be created when first observed
|
|
75
93
|
|
|
76
|
-
deep:
|
|
94
|
+
deep: unbuiltDeepChildren,
|
|
77
95
|
deepDirty: true,
|
|
78
96
|
deepAtom: undefined, // will be created when first observed
|
|
79
97
|
|
|
80
|
-
|
|
98
|
+
extensions: undefined,
|
|
99
|
+
|
|
100
|
+
listeners: undefined,
|
|
81
101
|
}
|
|
82
102
|
metadata.objectChildren = obj
|
|
83
103
|
}
|
|
@@ -97,6 +117,14 @@ export function getObjectChildren(node: object): ObjectChildrenData["shallow"] {
|
|
|
97
117
|
return obj.shallow
|
|
98
118
|
}
|
|
99
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Like `getObjectChildren(node).size > 0`, but without observing or allocating anything.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
export function hasObjectChildren(node: object): boolean {
|
|
125
|
+
return (treeNodeMetadata.get(node)?.objectChildren?.shallow.size ?? 0) > 0
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
/**
|
|
101
129
|
* @internal
|
|
102
130
|
*/
|
|
@@ -107,19 +135,16 @@ export function getDeepObjectChildren(node: object): DeepObjectChildren {
|
|
|
107
135
|
updateDeepObjectChildren(node)
|
|
108
136
|
}
|
|
109
137
|
|
|
110
|
-
|
|
111
|
-
obj.deepAtom = createAtom("deepChildrenAtom")
|
|
112
|
-
}
|
|
113
|
-
obj.deepAtom.reportObserved()
|
|
138
|
+
reportDeepObserved(obj)
|
|
114
139
|
|
|
115
140
|
return obj
|
|
116
141
|
}
|
|
117
142
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
function reportDeepObserved(obj: ObjectChildrenData) {
|
|
144
|
+
if (!obj.deepAtom) {
|
|
145
|
+
obj.deepAtom = createAtom("deepChildrenAtom")
|
|
146
|
+
}
|
|
147
|
+
obj.deepAtom.reportObserved()
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
const updateDeepObjectChildren = action((node: object): DeepObjectChildren => {
|
|
@@ -147,19 +172,19 @@ function updateDeepObjectChildrenRecursive(node: object): DeepObjectChildren {
|
|
|
147
172
|
}
|
|
148
173
|
}
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
obj.
|
|
175
|
+
const deep = new Set<object>()
|
|
176
|
+
obj.deep = deep
|
|
152
177
|
|
|
153
178
|
const childrenIterator = obj.shallow.values()
|
|
154
179
|
let childrenIteratorResult = childrenIterator.next()
|
|
155
180
|
while (!childrenIteratorResult.done) {
|
|
156
|
-
|
|
181
|
+
deep.add(childrenIteratorResult.value)
|
|
157
182
|
|
|
158
183
|
const childDeepChildren = updateDeepObjectChildrenRecursive(childrenIteratorResult.value).deep
|
|
159
184
|
const childDeepChildrenIterator = childDeepChildren.values()
|
|
160
185
|
let childDeepChildrenIteratorResult = childDeepChildrenIterator.next()
|
|
161
186
|
while (!childDeepChildrenIteratorResult.done) {
|
|
162
|
-
|
|
187
|
+
deep.add(childDeepChildrenIteratorResult.value)
|
|
163
188
|
childDeepChildrenIteratorResult = childDeepChildrenIterator.next()
|
|
164
189
|
}
|
|
165
190
|
|
|
@@ -172,6 +197,8 @@ function updateDeepObjectChildrenRecursive(node: object): DeepObjectChildren {
|
|
|
172
197
|
return obj
|
|
173
198
|
}
|
|
174
199
|
|
|
200
|
+
let lastAttachOrder = 0
|
|
201
|
+
|
|
175
202
|
/**
|
|
176
203
|
* Must run inside the action-wrapped `setParent`, its only source caller.
|
|
177
204
|
*
|
|
@@ -185,6 +212,7 @@ export function addObjectChild(node: object, child: object): void {
|
|
|
185
212
|
if (shallow.size === previousShallowSize) {
|
|
186
213
|
return
|
|
187
214
|
}
|
|
215
|
+
getOrCreateTreeNodeMetadata(child).attachOrder = ++lastAttachOrder
|
|
188
216
|
|
|
189
217
|
onShallowChildrenChanged(node, obj, child, true)
|
|
190
218
|
}
|
|
@@ -218,7 +246,9 @@ function onShallowChildrenChanged(
|
|
|
218
246
|
node,
|
|
219
247
|
obj,
|
|
220
248
|
transaction,
|
|
221
|
-
recordDirectChildrenMutation(transaction, node, child, added)
|
|
249
|
+
recordDirectChildrenMutation(transaction, node, child, added),
|
|
250
|
+
child,
|
|
251
|
+
added
|
|
222
252
|
)
|
|
223
253
|
}
|
|
224
254
|
|
|
@@ -258,12 +288,35 @@ function invalidateDeepChildren(
|
|
|
258
288
|
node: object,
|
|
259
289
|
obj: ObjectChildrenData,
|
|
260
290
|
transaction: DeepChildrenMutationTransaction | undefined,
|
|
261
|
-
mutation: DirectChildrenMutationEntry | undefined
|
|
291
|
+
mutation: DirectChildrenMutationEntry | undefined,
|
|
292
|
+
child: object,
|
|
293
|
+
added: boolean
|
|
262
294
|
) {
|
|
263
295
|
let currentNode: object | undefined = node
|
|
264
296
|
let currentObj = obj
|
|
297
|
+
let extensionChange: ExtensionChange | undefined
|
|
298
|
+
let subtree: readonly object[] | undefined
|
|
265
299
|
|
|
266
300
|
while (currentNode) {
|
|
301
|
+
if (currentObj.extensions) {
|
|
302
|
+
extensionChange ??= new ExtensionChange(
|
|
303
|
+
() => (subtree ??= collectSubtree(child)),
|
|
304
|
+
added,
|
|
305
|
+
false
|
|
306
|
+
)
|
|
307
|
+
extensionChange.applyTo(currentObj.extensions)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (currentObj.listeners) {
|
|
311
|
+
for (const listener of currentObj.listeners) {
|
|
312
|
+
if (listener.deep) {
|
|
313
|
+
listener.onChildrenChanged((subtree ??= collectSubtree(child)))
|
|
314
|
+
} else if (currentNode === node) {
|
|
315
|
+
listener.onChildrenChanged([child])
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
267
320
|
if (transaction) {
|
|
268
321
|
let entry = transaction.invalidatedNodes?.get(currentNode)
|
|
269
322
|
if (!entry && !currentObj.deepDirty) {
|
|
@@ -334,45 +387,244 @@ function finishDeepChildrenMutationTransaction(transaction: DeepChildrenMutation
|
|
|
334
387
|
})
|
|
335
388
|
}
|
|
336
389
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Derived data kept per node about its deep children (e.g. an index of them).
|
|
392
|
+
* It is built from the deep children the first time it is needed and then kept
|
|
393
|
+
* up to date as descendants are attached and detached.
|
|
394
|
+
*
|
|
395
|
+
* @internal
|
|
396
|
+
*/
|
|
397
|
+
export interface DeepObjectChildrenExtension<D> {
|
|
340
398
|
initData(): D
|
|
341
|
-
|
|
399
|
+
/** Adds a node while building the data from scratch, in deep children order. */
|
|
400
|
+
addNode(node: object, data: D): void
|
|
401
|
+
/**
|
|
402
|
+
* Adds a node attached after the data was built. Returns false when the data
|
|
403
|
+
* must be rebuilt instead. Defaults to `addNode`.
|
|
404
|
+
*/
|
|
405
|
+
addNodeIncrementally?(node: object, data: D): boolean
|
|
406
|
+
/** Removes a detached node. Returns false when the data must be rebuilt instead. */
|
|
407
|
+
removeNode(node: object, data: D): boolean
|
|
408
|
+
/** Whether the data must be updated when a descendant model's id changes. */
|
|
409
|
+
dependsOnModelIds?: boolean
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
interface PendingExtensionChange {
|
|
413
|
+
readonly nodes: readonly object[]
|
|
414
|
+
readonly added: boolean
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
interface ExtensionState<D> {
|
|
418
|
+
readonly data: D
|
|
419
|
+
/** Data that could not be updated and must be rebuilt. */
|
|
420
|
+
dirty: boolean
|
|
421
|
+
/** Readers that need the data frozen; changes are queued meanwhile. */
|
|
422
|
+
pins: number
|
|
423
|
+
pending: PendingExtensionChange[] | undefined
|
|
342
424
|
}
|
|
343
425
|
|
|
344
426
|
/**
|
|
427
|
+
* Data of an extension frozen until released; changes made meanwhile are
|
|
428
|
+
* applied on release.
|
|
429
|
+
*
|
|
345
430
|
* @internal
|
|
346
431
|
*/
|
|
347
|
-
export
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
432
|
+
export interface PinnedDeepObjectChildrenExtensionData<D> {
|
|
433
|
+
readonly data: D
|
|
434
|
+
release(): void
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @internal
|
|
439
|
+
*/
|
|
440
|
+
export interface DeepObjectChildrenExtensionAccessor<D> {
|
|
441
|
+
/**
|
|
442
|
+
* Gets the data. By default any change of the deep children is observed as a
|
|
443
|
+
* change of the data; pass `observeDeepChildren` false when the extension
|
|
444
|
+
* reports its own finer-grained changes.
|
|
445
|
+
*/
|
|
446
|
+
get(node: object, observeDeepChildren?: boolean): D
|
|
447
|
+
pin(node: object): PinnedDeepObjectChildrenExtensionData<D>
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @internal
|
|
452
|
+
*/
|
|
453
|
+
export function registerDeepObjectChildrenExtension<D>(
|
|
454
|
+
extension: DeepObjectChildrenExtension<D>
|
|
455
|
+
): DeepObjectChildrenExtensionAccessor<D> {
|
|
456
|
+
const getState = (node: object, observeDeepChildren: boolean): ExtensionState<D> => {
|
|
457
|
+
const obj = getObjectChildrenObject(node)
|
|
458
|
+
let state = obj.extensions?.get(extension) as ExtensionState<D> | undefined
|
|
459
|
+
// changes queued while pinned make the data stale for anybody else
|
|
460
|
+
if (!state || state.dirty || state.pending) {
|
|
461
|
+
// building reads the nodes, which must not become dependencies
|
|
462
|
+
state = untracked(() => buildExtensionState(extension, node))
|
|
361
463
|
}
|
|
362
|
-
|
|
464
|
+
if (observeDeepChildren) {
|
|
465
|
+
reportDeepObserved(obj)
|
|
466
|
+
}
|
|
467
|
+
return state
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
get(node, observeDeepChildren = true) {
|
|
472
|
+
return getState(node, observeDeepChildren).data
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
pin(node) {
|
|
476
|
+
const state = getState(node, false)
|
|
477
|
+
state.pins++
|
|
478
|
+
let released = false
|
|
479
|
+
return {
|
|
480
|
+
data: state.data,
|
|
481
|
+
release() {
|
|
482
|
+
if (released) return
|
|
483
|
+
released = true
|
|
484
|
+
state.pins--
|
|
485
|
+
if (state.pins === 0 && state.pending) {
|
|
486
|
+
const pending = state.pending
|
|
487
|
+
state.pending = undefined
|
|
488
|
+
for (const change of pending) {
|
|
489
|
+
applyExtensionChange(extension, state, change.nodes, change.added)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
},
|
|
363
495
|
}
|
|
364
496
|
}
|
|
365
497
|
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
498
|
+
function buildExtensionState<D>(
|
|
499
|
+
extension: DeepObjectChildrenExtension<D>,
|
|
500
|
+
node: object
|
|
501
|
+
): ExtensionState<D> {
|
|
502
|
+
const data = extension.initData()
|
|
503
|
+
getDeepObjectChildren(node).deep.forEach((n) => {
|
|
504
|
+
extension.addNode(n, data)
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const state: ExtensionState<D> = {
|
|
508
|
+
data,
|
|
509
|
+
dirty: false,
|
|
510
|
+
pins: 0,
|
|
511
|
+
pending: undefined,
|
|
369
512
|
}
|
|
513
|
+
const obj = getObjectChildrenObject(node)
|
|
514
|
+
;(obj.extensions ??= new Map()).set(extension, state)
|
|
515
|
+
return state
|
|
516
|
+
}
|
|
370
517
|
|
|
371
|
-
|
|
518
|
+
function applyExtensionChange(
|
|
519
|
+
extension: DeepObjectChildrenExtension<any>,
|
|
520
|
+
state: ExtensionState<any>,
|
|
521
|
+
nodes: readonly object[],
|
|
522
|
+
added: boolean
|
|
523
|
+
) {
|
|
524
|
+
if (state.dirty) {
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
for (const n of nodes) {
|
|
528
|
+
let ok: boolean
|
|
529
|
+
if (!added) {
|
|
530
|
+
ok = extension.removeNode(n, state.data)
|
|
531
|
+
} else if (extension.addNodeIncrementally) {
|
|
532
|
+
ok = extension.addNodeIncrementally(n, state.data)
|
|
533
|
+
} else {
|
|
534
|
+
extension.addNode(n, state.data)
|
|
535
|
+
ok = true
|
|
536
|
+
}
|
|
537
|
+
if (!ok) {
|
|
538
|
+
state.dirty = true
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
372
543
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
544
|
+
/**
|
|
545
|
+
* A membership change of some deep children, applied to the extension data of
|
|
546
|
+
* each ancestor of the changed parent.
|
|
547
|
+
*/
|
|
548
|
+
class ExtensionChange {
|
|
549
|
+
private nodes: readonly object[] | undefined
|
|
550
|
+
private readonly getNodes: () => readonly object[]
|
|
551
|
+
private readonly added: boolean
|
|
552
|
+
private readonly onlyModelIdDependent: boolean
|
|
553
|
+
|
|
554
|
+
constructor(getNodes: () => readonly object[], added: boolean, onlyModelIdDependent: boolean) {
|
|
555
|
+
this.getNodes = getNodes
|
|
556
|
+
this.added = added
|
|
557
|
+
this.onlyModelIdDependent = onlyModelIdDependent
|
|
558
|
+
}
|
|
376
559
|
|
|
377
|
-
|
|
560
|
+
applyTo(extensions: NonNullable<ObjectChildrenData["extensions"]>) {
|
|
561
|
+
extensions.forEach((state, extension) => {
|
|
562
|
+
if (state.dirty || (this.onlyModelIdDependent && !extension.dependsOnModelIds)) {
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
this.nodes ??= this.getNodes()
|
|
566
|
+
if (state.pins > 0) {
|
|
567
|
+
;(state.pending ??= []).push({ nodes: this.nodes, added: this.added })
|
|
568
|
+
} else {
|
|
569
|
+
applyExtensionChange(extension, state, this.nodes, this.added)
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** The node and its descendants, parents first. */
|
|
576
|
+
function collectSubtree(root: object): object[] {
|
|
577
|
+
const nodes: object[] = []
|
|
578
|
+
const stack = [root]
|
|
579
|
+
while (stack.length > 0) {
|
|
580
|
+
const node = stack.pop()!
|
|
581
|
+
nodes.push(node)
|
|
582
|
+
const shallow = treeNodeMetadata.get(node)?.objectChildren?.shallow
|
|
583
|
+
shallow?.forEach((child) => {
|
|
584
|
+
stack.push(child)
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
return nodes
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* @internal
|
|
592
|
+
*/
|
|
593
|
+
export function addObjectChildrenListener(node: object, listener: ObjectChildrenListener): void {
|
|
594
|
+
;(getObjectChildrenObject(node).listeners ??= new Set()).add(listener)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* @internal
|
|
599
|
+
*/
|
|
600
|
+
export function removeObjectChildrenListener(node: object, listener: ObjectChildrenListener): void {
|
|
601
|
+
const obj = treeNodeMetadata.get(node)?.objectChildren
|
|
602
|
+
if (obj?.listeners?.delete(listener) && obj.listeners.size === 0) {
|
|
603
|
+
obj.listeners = undefined
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Must be called after the id of a model changes.
|
|
609
|
+
*
|
|
610
|
+
* @internal
|
|
611
|
+
*/
|
|
612
|
+
export function onModelIdChanged(model: object): void {
|
|
613
|
+
const parent = fastGetParent(model, false)
|
|
614
|
+
if (!parent) {
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
// re-index the model under its new id
|
|
618
|
+
const nodes = [model]
|
|
619
|
+
const removal = new ExtensionChange(() => nodes, false, true)
|
|
620
|
+
const addition = new ExtensionChange(() => nodes, true, true)
|
|
621
|
+
let currentNode: object | undefined = parent
|
|
622
|
+
while (currentNode) {
|
|
623
|
+
const extensions = treeNodeMetadata.get(currentNode)?.objectChildren?.extensions
|
|
624
|
+
if (extensions) {
|
|
625
|
+
removal.applyTo(extensions)
|
|
626
|
+
addition.applyTo(extensions)
|
|
627
|
+
}
|
|
628
|
+
currentNode = fastGetParent(currentNode, false)
|
|
629
|
+
}
|
|
378
630
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { action, reaction, runInAction } from "mobx"
|
|
1
|
+
import { action, createAtom, reaction, runInAction } from "mobx"
|
|
2
2
|
import { assertTweakedObject } from "../tweaker/core"
|
|
3
3
|
import { assertIsFunction } from "../utils"
|
|
4
|
+
import {
|
|
5
|
+
addObjectChildrenListener,
|
|
6
|
+
type ObjectChildrenListener,
|
|
7
|
+
removeObjectChildrenListener,
|
|
8
|
+
} from "./coreObjectChildren"
|
|
4
9
|
import { getChildrenObjects } from "./getChildrenObjects"
|
|
10
|
+
import { compareTreePaths, getPathFromAncestor } from "./treeOrder"
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* Runs a callback everytime a new object is attached to a given node.
|
|
@@ -47,7 +53,7 @@ export function onChildAttachedTo(
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
const runDetachDisposers = (nodes: object[]) => {
|
|
56
|
+
const runDetachDisposers = (nodes: readonly object[]) => {
|
|
51
57
|
let firstError: { value: unknown } | undefined
|
|
52
58
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
53
59
|
try {
|
|
@@ -71,58 +77,137 @@ export function onChildAttachedTo(
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
const getChildrenObjectOpts = { deep }
|
|
74
|
-
const
|
|
80
|
+
const getTarget = () => {
|
|
75
81
|
const t = target()
|
|
76
82
|
assertTweakedObject(t, "target()")
|
|
77
|
-
|
|
78
|
-
return new Set(getChildrenObjects(t, getChildrenObjectOpts))
|
|
83
|
+
return t
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
/** Children the callback ran for, with increasing numbers in the order it did. */
|
|
87
|
+
const currentChildren = new Map<object, number>()
|
|
88
|
+
let lastChildOrder = 0
|
|
82
89
|
|
|
83
|
-
const
|
|
84
|
-
(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const currentChildrenIter = currentChildren.values()
|
|
90
|
-
let currentChildrenCur = currentChildrenIter.next()
|
|
91
|
-
while (!currentChildrenCur.done) {
|
|
92
|
-
const n = currentChildrenCur.value
|
|
93
|
-
if (!newChildren.has(n)) {
|
|
94
|
-
currentChildren.delete(n)
|
|
95
|
-
|
|
96
|
-
// we should run it in inverse order
|
|
97
|
-
disposersToRun.push(n)
|
|
98
|
-
}
|
|
90
|
+
const updateChildren = (dead: readonly object[], born: Iterable<object>) => {
|
|
91
|
+
for (const n of dead) {
|
|
92
|
+
currentChildren.delete(n)
|
|
93
|
+
}
|
|
94
|
+
// we should run them in inverse order
|
|
95
|
+
let firstError = runDetachDisposers(dead)
|
|
99
96
|
|
|
100
|
-
|
|
97
|
+
for (const n of born) {
|
|
98
|
+
if (disposed) {
|
|
99
|
+
break
|
|
101
100
|
}
|
|
101
|
+
currentChildren.set(n, ++lastChildOrder)
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const detachAction = runInAction(() => fn(n))
|
|
115
|
-
addDetachDisposer(n, detachAction)
|
|
116
|
-
} catch (value) {
|
|
117
|
-
firstError ??= { value }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
103
|
+
try {
|
|
104
|
+
const detachAction = runInAction(() => fn(n))
|
|
105
|
+
addDetachDisposer(n, detachAction)
|
|
106
|
+
} catch (value) {
|
|
107
|
+
firstError ??= { value }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (firstError) {
|
|
111
|
+
throw firstError.value
|
|
112
|
+
}
|
|
113
|
+
}
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
const updateChildrenByDiff = (newChildren: ReadonlySet<object>) => {
|
|
116
|
+
const dead: object[] = []
|
|
117
|
+
for (const n of currentChildren.keys()) {
|
|
118
|
+
if (!newChildren.has(n)) {
|
|
119
|
+
dead.push(n)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const born: object[] = []
|
|
123
|
+
for (const n of newChildren) {
|
|
124
|
+
if (!currentChildren.has(n)) {
|
|
125
|
+
born.push(n)
|
|
122
126
|
}
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
}
|
|
128
|
+
updateChildren(dead, born)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Updates the children given the only nodes that may have been attached or detached. */
|
|
132
|
+
const updateChildrenIncrementally = (t: object, nodes: ReadonlySet<object>) => {
|
|
133
|
+
const dead: object[] = []
|
|
134
|
+
const born: { node: object; path: object[] }[] = []
|
|
135
|
+
for (const node of nodes) {
|
|
136
|
+
const path = getPathFromAncestor(t, node, false)
|
|
137
|
+
const isChild = path !== undefined && (deep || path.length === 1)
|
|
138
|
+
if (currentChildren.has(node)) {
|
|
139
|
+
if (!isChild) {
|
|
140
|
+
dead.push(node)
|
|
141
|
+
}
|
|
142
|
+
} else if (isChild) {
|
|
143
|
+
born.push({ node, path })
|
|
125
144
|
}
|
|
145
|
+
}
|
|
146
|
+
// same orders as a diff: the old children in the order they were added,
|
|
147
|
+
// and the new ones in the order of the children set
|
|
148
|
+
dead.sort((a, b) => currentChildren.get(a)! - currentChildren.get(b)!)
|
|
149
|
+
born.sort((a, b) => compareTreePaths(a.path, b.path, true))
|
|
150
|
+
updateChildren(
|
|
151
|
+
dead,
|
|
152
|
+
born.map((b) => b.node)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// nodes that may have been attached to or detached from the target since
|
|
157
|
+
// the last update
|
|
158
|
+
let touched = new Set<object>()
|
|
159
|
+
const touchedAtom = createAtom("onChildAttachedTo")
|
|
160
|
+
const listener: ObjectChildrenListener = {
|
|
161
|
+
deep,
|
|
162
|
+
onChildrenChanged(nodes) {
|
|
163
|
+
for (const n of nodes) {
|
|
164
|
+
touched.add(n)
|
|
165
|
+
}
|
|
166
|
+
touchedAtom.reportChanged()
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
let listenedTarget: object | undefined
|
|
170
|
+
|
|
171
|
+
const stopListening = () => {
|
|
172
|
+
if (listenedTarget) {
|
|
173
|
+
removeObjectChildrenListener(listenedTarget, listener)
|
|
174
|
+
listenedTarget = undefined
|
|
175
|
+
}
|
|
176
|
+
touched = new Set()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const update = (t: object) => {
|
|
180
|
+
if (t !== listenedTarget) {
|
|
181
|
+
stopListening()
|
|
182
|
+
addObjectChildrenListener(t, listener)
|
|
183
|
+
listenedTarget = t
|
|
184
|
+
updateChildrenByDiff(getChildrenObjects(t, getChildrenObjectOpts))
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const nodes = touched
|
|
189
|
+
touched = new Set()
|
|
190
|
+
// (even when most children changed, this is about as fast as a diff)
|
|
191
|
+
if (nodes.size > 0) {
|
|
192
|
+
updateChildrenIncrementally(t, nodes)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!fireForCurrentChildren) {
|
|
197
|
+
for (const n of getChildrenObjects(getTarget(), getChildrenObjectOpts)) {
|
|
198
|
+
currentChildren.set(n, ++lastChildOrder)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const disposer = reaction(
|
|
203
|
+
() => {
|
|
204
|
+
const t = getTarget()
|
|
205
|
+
touchedAtom.reportObserved()
|
|
206
|
+
// a new object, so every change runs the effect
|
|
207
|
+
return { t }
|
|
208
|
+
},
|
|
209
|
+
({ t }) => {
|
|
210
|
+
update(t)
|
|
126
211
|
},
|
|
127
212
|
{
|
|
128
213
|
fireImmediately: true,
|
|
@@ -134,7 +219,8 @@ export function onChildAttachedTo(
|
|
|
134
219
|
disposed = true
|
|
135
220
|
runCleanupAfterDispose = runPendingDetachDisposers
|
|
136
221
|
disposer()
|
|
137
|
-
|
|
222
|
+
stopListening()
|
|
223
|
+
const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren.keys()) : []
|
|
138
224
|
currentChildren.clear()
|
|
139
225
|
const firstError = runDetachDisposers(pendingChildren)
|
|
140
226
|
if (firstError) {
|