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.
Files changed (102) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +2561 -1214
  4. package/dist/mobx-keystone.esm.mjs +2561 -1214
  5. package/dist/mobx-keystone.umd.js +2566 -1220
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
  8. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  9. package/dist/types/modelShared/prop.d.ts +42 -29
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/treeOrder.d.ts +1 -0
  12. package/dist/types/patch/applyPatches.d.ts +2 -1
  13. package/dist/types/ref/Ref.d.ts +1 -4
  14. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  15. package/dist/types/ref/nodesById.d.ts +1 -0
  16. package/dist/types/ref/refIdIndex.d.ts +1 -0
  17. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  18. package/dist/types/snapshot/applySnapshot.d.ts +2 -1
  19. package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
  20. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  21. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  22. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  23. package/dist/types/types/index.d.ts +1 -0
  24. package/dist/types/types/schemas.d.ts +41 -37
  25. package/dist/types/types/tProp.d.ts +12 -6
  26. package/dist/types/types/types.d.ts +6 -6
  27. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  28. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  29. package/dist/types/utils/ModelPool.d.ts +7 -2
  30. package/dist/types/utils/chunks.d.ts +1 -0
  31. package/dist/types/utils/types.d.ts +6 -0
  32. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  33. package/package.json +5 -3
  34. package/src/action/modelFlow.ts +35 -66
  35. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  36. package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
  37. package/src/actionMiddlewares/undoMiddleware.ts +63 -35
  38. package/src/context/context.ts +15 -36
  39. package/src/dataModel/utils.ts +5 -28
  40. package/src/deepChange/onDeepChange.ts +44 -126
  41. package/src/model/BaseModel.ts +7 -5
  42. package/src/model/utils.ts +5 -10
  43. package/src/modelShared/modelDecorator.ts +16 -30
  44. package/src/modelShared/prop.ts +115 -111
  45. package/src/modelShared/sharedInternalModel.ts +2 -2
  46. package/src/parent/arrayPaths.ts +239 -0
  47. package/src/parent/coreObjectChildren.ts +300 -48
  48. package/src/parent/onChildAttachedTo.ts +130 -44
  49. package/src/parent/path.ts +10 -12
  50. package/src/parent/setParent.ts +27 -15
  51. package/src/parent/treeOrder.ts +53 -0
  52. package/src/parent/walkTree.ts +0 -73
  53. package/src/patch/applyPatches.ts +31 -26
  54. package/src/patch/patchRecorder.ts +13 -28
  55. package/src/ref/core.ts +69 -46
  56. package/src/ref/modelRefIdIndex.ts +131 -0
  57. package/src/ref/nodesById.ts +60 -0
  58. package/src/ref/refIdIndex.ts +209 -0
  59. package/src/rootStore/rootStore.ts +1 -3
  60. package/src/snapshot/SnapshotOf.ts +24 -48
  61. package/src/snapshot/applySnapshot.ts +27 -19
  62. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  63. package/src/snapshot/fromSnapshot.ts +12 -9
  64. package/src/snapshot/getSnapshot.ts +1 -1
  65. package/src/snapshot/internal.ts +67 -12
  66. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  67. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  68. package/src/treeUtils/draft.ts +6 -1
  69. package/src/treeUtils/sandbox.ts +10 -7
  70. package/src/tweaker/changeRollback.ts +218 -0
  71. package/src/tweaker/core.ts +33 -20
  72. package/src/tweaker/treeNodeMetadata.ts +12 -2
  73. package/src/tweaker/tweak.ts +181 -18
  74. package/src/tweaker/tweakArray.ts +127 -77
  75. package/src/tweaker/tweakPlainObject.ts +65 -46
  76. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  77. package/src/tweaker/typeChecking.ts +5 -104
  78. package/src/types/arrayBased/typesArray.ts +23 -30
  79. package/src/types/arrayBased/typesTuple.ts +16 -25
  80. package/src/types/createCachedTypeCheck.ts +509 -94
  81. package/src/types/index.ts +1 -0
  82. package/src/types/objectBased/typesDataModelData.ts +7 -8
  83. package/src/types/objectBased/typesObject.ts +11 -15
  84. package/src/types/objectBased/typesRecord.ts +12 -19
  85. package/src/types/schemas.ts +59 -71
  86. package/src/types/tProp.ts +29 -29
  87. package/src/types/utility/typeToStoredData.ts +22 -21
  88. package/src/types/utility/typesCodecCore.ts +5 -0
  89. package/src/types/utility/typesCodecSupport.ts +3 -3
  90. package/src/types/utility/typesMap.ts +43 -51
  91. package/src/types/utility/typesRefinement.ts +7 -8
  92. package/src/types/utility/typesSkipCheck.ts +7 -8
  93. package/src/types/utility/typesTag.ts +7 -8
  94. package/src/utils/ModelPool.ts +76 -37
  95. package/src/utils/chunks.ts +271 -0
  96. package/src/utils/decorators.ts +5 -5
  97. package/src/utils/index.ts +38 -20
  98. package/src/utils/types.ts +5 -0
  99. package/src/wrappers/ArraySet.ts +27 -14
  100. package/src/wrappers/ObjectMap.ts +1 -6
  101. package/src/wrappers/asMap.ts +1 -21
  102. 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: new Set(),
94
+ deep: unbuiltDeepChildren,
77
95
  deepDirty: true,
78
96
  deepAtom: undefined, // will be created when first observed
79
97
 
80
- extensionsData: initExtensionsData(),
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
- if (!obj.deepAtom) {
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 addNodeToDeepLists(node: any, data: DeepObjectChildren) {
119
- data.deep.add(node)
120
- data.extensionsData?.forEach((extensionData, dataSymbol) => {
121
- extensions.get(dataSymbol)!.addNode(node, extensionData)
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
- obj.deep = new Set()
151
- obj.extensionsData = initExtensionsData(obj.extensionsData)
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
- addNodeToDeepLists(childrenIteratorResult.value, obj)
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
- addNodeToDeepLists(childDeepChildrenIteratorResult.value, obj)
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
- const extensions = new Map<object, DeepObjectChildrenExtension<any>>()
338
-
339
- interface DeepObjectChildrenExtension<D> {
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
- addNode(node: any, data: D): void
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 function registerDeepObjectChildrenExtension<D>(extension: DeepObjectChildrenExtension<D>) {
348
- const dataSymbol = {}
349
- extensions.set(dataSymbol, extension)
350
-
351
- return (data: DeepObjectChildren): D => {
352
- let extensionsData = data.extensionsData
353
- let extensionData = extensionsData?.get(dataSymbol) as D | undefined
354
- if (!extensionsData?.has(dataSymbol)) {
355
- extensionsData = data.extensionsData ??= new Map()
356
- extensionData = extension.initData()
357
- extensionsData.set(dataSymbol, extensionData)
358
- data.deep.forEach((node) => {
359
- extension.addNode(node, extensionData!)
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
- return extensionData!
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 initExtensionsData(previousData?: ReadonlyMap<object, unknown>) {
367
- if (previousData === undefined || previousData.size === 0) {
368
- return undefined
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
- const extensionsData = new Map<object, unknown>()
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
- previousData?.forEach((_, dataSymbol) => {
374
- extensionsData.set(dataSymbol, extensions.get(dataSymbol)!.initData())
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
- return extensionsData
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 getCurrentChildren = () => {
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
- const currentChildren = fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
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 disposer = reaction(
84
- () => getCurrentChildren(),
85
- (newChildren) => {
86
- const disposersToRun: object[] = []
87
-
88
- // find dead
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
- currentChildrenCur = currentChildrenIter.next()
97
+ for (const n of born) {
98
+ if (disposed) {
99
+ break
101
100
  }
101
+ currentChildren.set(n, ++lastChildOrder)
102
102
 
103
- let firstError = runDetachDisposers(disposersToRun)
104
-
105
- // find new
106
- const newChildrenIter = newChildren.values()
107
- let newChildrenCur = newChildrenIter.next()
108
- while (!disposed && !newChildrenCur.done) {
109
- const n = newChildrenCur.value
110
- if (!currentChildren.has(n)) {
111
- currentChildren.add(n)
112
-
113
- try {
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
- newChildrenCur = newChildrenIter.next()
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
- if (firstError) {
124
- throw firstError.value
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
- const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren) : []
222
+ stopListening()
223
+ const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren.keys()) : []
138
224
  currentChildren.clear()
139
225
  const firstError = runDetachDisposers(pendingChildren)
140
226
  if (firstError) {