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.
Files changed (141) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/dist/mobx-keystone.esm.js +3627 -1540
  3. package/dist/mobx-keystone.esm.mjs +3627 -1540
  4. package/dist/mobx-keystone.umd.js +3638 -1543
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
  8. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  9. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/detach.d.ts +1 -1
  12. package/dist/types/parent/findChildren.d.ts +1 -1
  13. package/dist/types/parent/treeOrder.d.ts +1 -0
  14. package/dist/types/patch/applyPatches.d.ts +3 -0
  15. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  16. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  17. package/dist/types/ref/nodesById.d.ts +1 -0
  18. package/dist/types/ref/refIdIndex.d.ts +1 -0
  19. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  20. package/dist/types/snapshot/applySnapshot.d.ts +1 -0
  21. package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
  22. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  23. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  24. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  25. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  26. package/dist/types/types/TypeCheckError.d.ts +1 -0
  27. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  28. package/dist/types/utils/ModelPool.d.ts +7 -1
  29. package/dist/types/utils/chunks.d.ts +1 -0
  30. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  31. package/dist/types/utils/index.d.ts +13 -0
  32. package/dist/types/utils/mapUtils.d.ts +2 -2
  33. package/dist/types/utils/setLike.d.ts +1 -0
  34. package/package.json +2 -2
  35. package/src/action/applyAction.ts +1 -1
  36. package/src/action/middleware.ts +17 -36
  37. package/src/action/modelFlow.ts +4 -5
  38. package/src/action/mutationBatch.ts +10 -10
  39. package/src/action/pendingActions.ts +3 -4
  40. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  41. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  42. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  43. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  44. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  45. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  46. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  47. package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
  48. package/src/actionMiddlewares/undoMiddleware.ts +112 -59
  49. package/src/actionMiddlewares/utils.ts +2 -1
  50. package/src/computedTree/computedTree.ts +14 -3
  51. package/src/context/context.ts +17 -45
  52. package/src/dataModel/BaseDataModel.ts +20 -16
  53. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  54. package/src/deepChange/onDeepChange.ts +179 -24
  55. package/src/frozen/Frozen.ts +50 -33
  56. package/src/model/BaseModel.ts +10 -13
  57. package/src/model/newModel.ts +21 -24
  58. package/src/model/utils.ts +2 -2
  59. package/src/modelShared/modelClassInitializer.ts +3 -2
  60. package/src/modelShared/modelDecorator.ts +39 -39
  61. package/src/modelShared/modelInfo.ts +1 -1
  62. package/src/modelShared/prop.ts +6 -9
  63. package/src/modelShared/sharedInternalModel.ts +16 -3
  64. package/src/parent/arrayPaths.ts +242 -0
  65. package/src/parent/coreObjectChildren.ts +300 -48
  66. package/src/parent/detach.ts +1 -1
  67. package/src/parent/findChildren.ts +4 -7
  68. package/src/parent/onChildAttachedTo.ts +157 -55
  69. package/src/parent/path.ts +15 -5
  70. package/src/parent/setParent.ts +34 -15
  71. package/src/parent/treeOrder.ts +53 -0
  72. package/src/parent/walkTree.ts +12 -101
  73. package/src/patch/applyPatches.ts +74 -34
  74. package/src/patch/emitPatch.ts +142 -26
  75. package/src/patch/jsonPatch.ts +5 -0
  76. package/src/patch/patchRecorder.ts +18 -14
  77. package/src/redux/connectReduxDevTools.ts +24 -18
  78. package/src/redux/redux.ts +5 -21
  79. package/src/ref/core.ts +81 -29
  80. package/src/ref/modelRefIdIndex.ts +131 -0
  81. package/src/ref/nodesById.ts +60 -0
  82. package/src/ref/refIdIndex.ts +209 -0
  83. package/src/rootStore/attachDetach.ts +27 -16
  84. package/src/rootStore/rootStore.ts +10 -6
  85. package/src/snapshot/SnapshotOf.ts +2 -2
  86. package/src/snapshot/applySnapshot.ts +27 -19
  87. package/src/snapshot/clone.ts +1 -2
  88. package/src/snapshot/cloneTreeValue.ts +1 -2
  89. package/src/snapshot/fromModelSnapshot.ts +6 -2
  90. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  91. package/src/snapshot/fromSnapshot.ts +2 -6
  92. package/src/snapshot/internal.ts +89 -19
  93. package/src/snapshot/onSnapshot.ts +12 -1
  94. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  95. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  96. package/src/standardActions/arrayActions.ts +12 -2
  97. package/src/standardActions/objectActions.ts +3 -2
  98. package/src/transforms/ImmutableDate.ts +4 -0
  99. package/src/transforms/asMap.ts +3 -1
  100. package/src/treeUtils/deepEquals.ts +15 -13
  101. package/src/treeUtils/draft.ts +8 -0
  102. package/src/treeUtils/sandbox.ts +31 -27
  103. package/src/tweaker/changeRollback.ts +218 -0
  104. package/src/tweaker/core.ts +33 -22
  105. package/src/tweaker/treeNodeMetadata.ts +12 -2
  106. package/src/tweaker/tweak.ts +183 -19
  107. package/src/tweaker/tweakArray.ts +176 -33
  108. package/src/tweaker/tweakPlainObject.ts +117 -39
  109. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  110. package/src/tweaker/typeChecking.ts +25 -15
  111. package/src/types/TypeCheckError.ts +17 -19
  112. package/src/types/arrayBased/typesArray.ts +10 -11
  113. package/src/types/createCachedTypeCheck.ts +541 -88
  114. package/src/types/objectBased/typesDataModelData.ts +4 -2
  115. package/src/types/objectBased/typesModel.ts +4 -2
  116. package/src/types/objectBased/typesObject.ts +20 -16
  117. package/src/types/objectBased/typesRecord.ts +8 -18
  118. package/src/types/objectBased/typesRef.ts +2 -2
  119. package/src/types/primitiveBased/typesEnum.ts +4 -2
  120. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  121. package/src/types/typeCheckErrorUtils.ts +1 -0
  122. package/src/types/utility/typesCodecSupport.ts +97 -56
  123. package/src/types/utility/typesMap.ts +24 -41
  124. package/src/types/utility/typesRefinement.ts +3 -2
  125. package/src/types/utility/typesSet.ts +24 -0
  126. package/src/utils/ModelPool.ts +78 -22
  127. package/src/utils/chainFns.ts +4 -0
  128. package/src/utils/chunks.ts +271 -0
  129. package/src/utils/decorators.ts +1 -1
  130. package/src/utils/errorDiagnostics.ts +6 -2
  131. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  132. package/src/utils/index.ts +71 -2
  133. package/src/utils/mapUtils.ts +3 -7
  134. package/src/utils/setIfDifferent.ts +3 -2
  135. package/src/utils/setLike.ts +134 -0
  136. package/src/wrappers/ArraySet.ts +5 -9
  137. package/src/wrappers/ObjectMap.ts +15 -11
  138. package/src/wrappers/asMap.ts +30 -18
  139. package/src/wrappers/asSet.ts +36 -12
  140. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  141. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -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.
@@ -32,11 +38,10 @@ export function onChildAttachedTo(
32
38
  assertIsFunction(target, "target")
33
39
  assertIsFunction(fn, "fn")
34
40
 
35
- const opts = {
36
- deep: false,
37
- fireForCurrentChildren: true,
38
- ...options,
39
- }
41
+ const { deep = false, fireForCurrentChildren = true } = options ?? {}
42
+
43
+ let disposed = false
44
+ let runCleanupAfterDispose = false
40
45
 
41
46
  const detachDisposers = new WeakMap<object, () => void>()
42
47
 
@@ -48,81 +53,178 @@ export function onChildAttachedTo(
48
53
  }
49
54
  }
50
55
 
56
+ const runDetachDisposers = (nodes: readonly object[]) => {
57
+ let firstError: { value: unknown } | undefined
58
+ for (let i = nodes.length - 1; i >= 0; i--) {
59
+ try {
60
+ runDetachDisposer(nodes[i])
61
+ } catch (value) {
62
+ firstError ??= { value }
63
+ }
64
+ }
65
+ return firstError
66
+ }
67
+
51
68
  const addDetachDisposer = (n: object, disposer: (() => void) | void) => {
52
69
  if (disposer) {
53
- detachDisposers.set(n, action(disposer))
70
+ const detach = action(disposer)
71
+ if (disposed) {
72
+ if (runCleanupAfterDispose) detach()
73
+ } else {
74
+ detachDisposers.set(n, detach)
75
+ }
54
76
  }
55
77
  }
56
78
 
57
- const getChildrenObjectOpts = { deep: opts.deep }
58
- const getCurrentChildren = () => {
79
+ const getChildrenObjectOpts = { deep }
80
+ const getTarget = () => {
59
81
  const t = target()
60
82
  assertTweakedObject(t, "target()")
61
-
62
- return new Set(getChildrenObjects(t, getChildrenObjectOpts))
83
+ return t
63
84
  }
64
85
 
65
- const currentChildren = opts.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
66
89
 
67
- const disposer = reaction(
68
- () => getCurrentChildren(),
69
- (newChildren) => {
70
- const disposersToRun: object[] = []
71
-
72
- // find dead
73
- const currentChildrenIter = currentChildren.values()
74
- let currentChildrenCur = currentChildrenIter.next()
75
- while (!currentChildrenCur.done) {
76
- const n = currentChildrenCur.value
77
- if (!newChildren.has(n)) {
78
- currentChildren.delete(n)
79
-
80
- // we should run it in inverse order
81
- disposersToRun.push(n)
82
- }
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)
83
96
 
84
- currentChildrenCur = currentChildrenIter.next()
97
+ for (const n of born) {
98
+ if (disposed) {
99
+ break
85
100
  }
101
+ currentChildren.set(n, ++lastChildOrder)
86
102
 
87
- if (disposersToRun.length > 0) {
88
- for (let i = disposersToRun.length - 1; i >= 0; i--) {
89
- runDetachDisposer(disposersToRun[i])
90
- }
103
+ try {
104
+ const detachAction = runInAction(() => fn(n))
105
+ addDetachDisposer(n, detachAction)
106
+ } catch (value) {
107
+ firstError ??= { value }
91
108
  }
109
+ }
110
+ if (firstError) {
111
+ throw firstError.value
112
+ }
113
+ }
92
114
 
93
- // find new
94
- const newChildrenIter = newChildren.values()
95
- let newChildrenCur = newChildrenIter.next()
96
- while (!newChildrenCur.done) {
97
- const n = newChildrenCur.value
98
- if (!currentChildren.has(n)) {
99
- currentChildren.add(n)
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)
126
+ }
127
+ }
128
+ updateChildren(dead, born)
129
+ }
100
130
 
101
- const detachAction = runInAction(() => fn(n))
102
- addDetachDisposer(n, detachAction)
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)
103
141
  }
142
+ } else if (isChild) {
143
+ born.push({ node, path })
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
+ }
104
155
 
105
- newChildrenCur = newChildrenIter.next()
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)
106
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)
107
211
  },
108
212
  {
109
213
  fireImmediately: true,
110
214
  }
111
215
  )
112
216
 
113
- return (runDetachDisposers: boolean) => {
217
+ return (runPendingDetachDisposers: boolean) => {
218
+ if (disposed) return
219
+ disposed = true
220
+ runCleanupAfterDispose = runPendingDetachDisposers
114
221
  disposer()
115
-
116
- if (runDetachDisposers) {
117
- const currentChildrenIter = currentChildren.values()
118
- let currentChildrenCur = currentChildrenIter.next()
119
- while (!currentChildrenCur.done) {
120
- const n = currentChildrenCur.value
121
- runDetachDisposer(n)
122
-
123
- currentChildrenCur = currentChildrenIter.next()
124
- }
125
- }
222
+ stopListening()
223
+ const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren.keys()) : []
126
224
  currentChildren.clear()
225
+ const firstError = runDetachDisposers(pendingChildren)
226
+ if (firstError) {
227
+ throw firstError.value
228
+ }
127
229
  }
128
230
  }
@@ -3,6 +3,7 @@ import { isModel } from "../model/utils"
3
3
  import { assertTweakedObject } from "../tweaker/core"
4
4
  import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
5
5
  import { hasOwnProp, isArray, isObject } from "../utils"
6
+ import { LazyArrayParentPath } from "./arrayPaths"
6
7
  import {
7
8
  dataToModelNode,
8
9
  getDataObjectParent,
@@ -61,7 +62,10 @@ export interface RootPath<T extends object> {
61
62
  export function getParentPath<T extends object = any>(value: object): ParentPath<T> | undefined {
62
63
  assertTweakedObject(value, "value")
63
64
 
64
- return fastGetParentPath(value, true)
65
+ const parentPath = fastGetParentPath<T>(value, true)
66
+ return parentPath instanceof LazyArrayParentPath
67
+ ? (parentPath.toParentPath() as ParentPath<T>)
68
+ : parentPath
65
69
  }
66
70
 
67
71
  /**
@@ -71,10 +75,14 @@ export function fastGetParentPath<T extends object = any>(
71
75
  value: object,
72
76
  useAtom: boolean
73
77
  ): ParentPath<T> | undefined {
78
+ const parentPath = treeNodeMetadata.get(value)?.parentPath
74
79
  if (useAtom) {
75
80
  reportParentPathObserved(value)
81
+ if (parentPath instanceof LazyArrayParentPath) {
82
+ parentPath.reportObserved()
83
+ }
76
84
  }
77
- return treeNodeMetadata.get(value)?.parentPath as ParentPath<T> | undefined
85
+ return parentPath as ParentPath<T> | undefined
78
86
  }
79
87
 
80
88
  /**
@@ -267,11 +275,13 @@ export function resolvePath<T = any>(
267
275
  const dataNode = modelToDataNode(current)
268
276
  if (p in dataNode) {
269
277
  current = dataNode
270
- } else if (!(p in current)) {
271
- return unresolved
272
278
  }
273
279
  }
274
280
 
281
+ if (!(p in current)) {
282
+ return unresolved
283
+ }
284
+
275
285
  current = current[p]
276
286
  }
277
287
 
@@ -323,7 +333,7 @@ export function resolvePathCheckingIds<T = any>(
323
333
 
324
334
  // check just to avoid mobx warnings about trying to access out of bounds index
325
335
  if (isArray(current)) {
326
- if (+p >= current.length) {
336
+ if (+p >= current.length || !(p in current)) {
327
337
  return { resolved: false }
328
338
  }
329
339
  } else if (!hasOwnProp(current, p)) {
@@ -6,6 +6,7 @@ import { isModel } from "../model/utils"
6
6
  import { attachToRootStore, detachFromRootStore } from "../rootStore/attachDetach"
7
7
  import { fastIsRootStoreNoAtom } from "../rootStore/rootStore"
8
8
  import { clone } from "../snapshot/clone"
9
+ import { flushInternalSnapshot } from "../snapshot/internal"
9
10
  import { isTweakedObject } from "../tweaker/core"
10
11
  import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
11
12
  import { tryUntweak } from "../tweaker/tweak"
@@ -16,9 +17,11 @@ import {
16
17
  reportParentPathChanged,
17
18
  setDataObjectParent,
18
19
  } from "./core"
19
- import { addObjectChild, removeObjectChild } from "./coreObjectChildren"
20
+ import { addObjectChild, hasObjectChildren, removeObjectChild } from "./coreObjectChildren"
20
21
  import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
21
22
 
23
+ const reindexChunkSize = 4096
24
+
22
25
  /**
23
26
  * Updates the unchanged tail of an array before a splice is applied. These
24
27
  * children keep their parent, so neither attachment nor child-index work is
@@ -28,18 +31,24 @@ import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
28
31
  export const reindexArrayChildren = action(
29
32
  "reindexArrayChildren",
30
33
  (array: readonly unknown[], oldStart: number, newStart: number): void => {
31
- for (let i = oldStart, j = newStart; i < array.length; i++, j++) {
32
- const value = array[i]
33
- if (isPrimitive(value)) {
34
- continue
35
- }
36
- const metadata = treeNodeMetadata.get(value as object)
37
- if (inDevMode && metadata?.parentPath?.parent !== array) {
38
- throw failure("assertion failed: reindexed child must already belong to the array")
34
+ // Copy the tail in chunks: indexed reads through an observable array are
35
+ // much slower than a slice, and chunking bounds the temporary copy size.
36
+ const length = array.length
37
+ for (let chunkStart = oldStart; chunkStart < length; chunkStart += reindexChunkSize) {
38
+ const chunk = array.slice(chunkStart, chunkStart + reindexChunkSize)
39
+ for (let i = 0, j = newStart + chunkStart - oldStart; i < chunk.length; i++, j++) {
40
+ const value = chunk[i]
41
+ if (isPrimitive(value)) {
42
+ continue
43
+ }
44
+ const metadata = treeNodeMetadata.get(value as object)
45
+ if (inDevMode && metadata?.parentPath?.parent !== array) {
46
+ throw failure("assertion failed: reindexed child must already belong to the array")
47
+ }
48
+ // Replace the path object: callers may have retained the previous path.
49
+ metadata!.parentPath = { parent: array, path: j }
50
+ reportParentPathChanged(value as object)
39
51
  }
40
- // Replace the path object: callers may have retained the previous path.
41
- metadata!.parentPath = { parent: array, path: j }
42
- reportParentPathChanged(value as object)
43
52
  }
44
53
  }
45
54
  )
@@ -86,6 +95,12 @@ export const setParent = action(
86
95
  return value
87
96
  }
88
97
 
98
+ // The new parent captures this node's snapshot during attachment. Flush
99
+ // descendant edits made while detached before that snapshot is copied.
100
+ if (parentPath && !oldParentPath) {
101
+ flushInternalSnapshot(value, false)
102
+ }
103
+
89
104
  // make sure the new parent actually points to models when we give model data objs
90
105
  if (parentPath) {
91
106
  const actualParent = dataToModelNode(parentPath.parent)
@@ -116,11 +131,15 @@ export const setParent = action(
116
131
 
117
132
  const postUntweaker = parentPath ? undefined : tryUntweak(value)
118
133
 
119
- const valueIsModel = value instanceof BaseModel
134
+ // A model's root store hooks run when its own root changes. A plain node
135
+ // has none of its own, but one attached with children may bring models
136
+ // into a root store. (When detached, its children get detached one by one.)
137
+ const checkRootStore =
138
+ value instanceof BaseModel || (!oldParentPath && !!parentPath && hasObjectChildren(value))
120
139
 
121
140
  let oldRoot: any
122
141
  let oldRootStore: any
123
- if (valueIsModel) {
142
+ if (checkRootStore) {
124
143
  oldRoot = fastGetRoot(value, false)
125
144
  oldRootStore = fastIsRootStoreNoAtom(oldRoot) ? oldRoot : undefined
126
145
  }
@@ -137,7 +156,7 @@ export const setParent = action(
137
156
  }
138
157
  reportParentPathChanged(value)
139
158
 
140
- if (valueIsModel) {
159
+ if (checkRootStore) {
141
160
  const newRoot = fastGetRoot(value, false)
142
161
  const newRootStore = fastIsRootStoreNoAtom(newRoot) ? newRoot : undefined
143
162
 
@@ -0,0 +1,53 @@
1
+ import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
2
+ import { reportParentPathObserved } from "./core"
3
+
4
+ /**
5
+ * Nodes from right below the ancestor down to the node, or `undefined` when the
6
+ * node is not below it. Optionally observes their parents.
7
+ *
8
+ * @internal
9
+ */
10
+ export function getPathFromAncestor(
11
+ ancestor: object,
12
+ node: object,
13
+ observeParents: boolean
14
+ ): object[] | undefined {
15
+ const path: object[] = []
16
+ let current: object | undefined = node
17
+ while (current !== ancestor) {
18
+ if (!current) {
19
+ return undefined
20
+ }
21
+ path.push(current)
22
+ if (observeParents) {
23
+ reportParentPathObserved(current)
24
+ }
25
+ current = treeNodeMetadata.get(current)?.parentPath?.parent
26
+ }
27
+ return path.reverse()
28
+ }
29
+
30
+ /**
31
+ * Compares two paths from the same ancestor (see `getPathFromAncestor`) by the
32
+ * order a walk of the tree visits their nodes. The walk visits the children of
33
+ * a node in the order they were attached, and visits parents before their
34
+ * children when `parentsFirst`, or after them otherwise.
35
+ *
36
+ * @internal
37
+ */
38
+ export function compareTreePaths(
39
+ a: readonly object[],
40
+ b: readonly object[],
41
+ parentsFirst: boolean
42
+ ): number {
43
+ const length = Math.min(a.length, b.length)
44
+ for (let i = 0; i < length; i++) {
45
+ if (a[i] !== b[i]) {
46
+ // siblings
47
+ return treeNodeMetadata.get(a[i])!.attachOrder - treeNodeMetadata.get(b[i])!.attachOrder
48
+ }
49
+ }
50
+ // the shorter path ends at an ancestor of the other node
51
+ const depthDifference = a.length - b.length
52
+ return parentsFirst ? depthDifference : -depthDifference
53
+ }
@@ -1,4 +1,3 @@
1
- import { computed, type IComputedValue } from "mobx"
2
1
  import { assertTweakedObject } from "../tweaker/core"
3
2
  import { getObjectChildren } from "./coreObjectChildren"
4
3
 
@@ -75,108 +74,20 @@ function walkTreeChildrenFirst<T = void>(
75
74
  root: object,
76
75
  visit: (node: object) => T | undefined
77
76
  ): T | undefined {
78
- const childrenIter = getObjectChildren(root).values()
79
- let ch = childrenIter.next()
80
- while (!ch.done) {
81
- const ret = walkTreeChildrenFirst(ch.value, visit)
82
- if (ret !== undefined) {
83
- return ret
84
- }
85
- ch = childrenIter.next()
86
- }
87
-
88
- const ret = visit(root)
89
- if (ret !== undefined) {
90
- return ret
91
- }
92
-
93
- return undefined
94
- }
95
-
96
- /**
97
- * @internal
98
- */
99
- export interface ComputedWalkTreeAggregate<R> {
100
- walk(target: object): Map<R, object> | undefined
101
- }
102
-
103
- function getComputedTreeResult<R>(
104
- computedFns: WeakMap<object, IComputedValue<Map<R, object> | undefined>>,
105
- visit: (node: object) => R | undefined,
106
- tree: object
107
- ): Map<R, object> | undefined {
108
- let cmpted = computedFns.get(tree)
109
- if (!cmpted) {
110
- cmpted = computed(() => {
111
- return walkTreeAggregate(tree, visit, (ch) => getComputedTreeResult(computedFns, visit, ch))
112
- })
113
- computedFns.set(tree, cmpted)
114
- }
115
- return cmpted.get()
116
- }
117
-
118
- /**
119
- * @internal
120
- */
121
- export function computedWalkTreeAggregate<R>(
122
- visit: (node: object) => R | undefined
123
- ): ComputedWalkTreeAggregate<R> {
124
- const computedFns = new WeakMap<object, IComputedValue<Map<R, object> | undefined>>()
125
-
126
- return {
127
- walk: (n) => getComputedTreeResult(computedFns, visit, n),
128
- }
129
- }
130
-
131
- function walkTreeAggregate<R>(
132
- target: object,
133
- visit: (node: object) => R | undefined,
134
- recurse: (node: object) => Map<R, object> | undefined
135
- ): Map<R, object> | undefined {
136
- let map: Map<R, object> | undefined
137
- const rootVal = visit(target)
138
-
139
- const childrenMap = getObjectChildren(target)
140
- const childrenIter = childrenMap.values()
141
- let ch = childrenIter.next()
142
-
143
- // small optimization, if there is only one child and this
144
- // object provides no value we can just reuse the child ones
145
- if (rootVal === undefined && childrenMap.size === 1) {
146
- return recurse(ch.value!)
147
- }
148
-
149
- while (!ch.done) {
150
- const childMap = recurse(ch.value)
151
-
152
- if (childMap) {
153
- if (!map) {
154
- map = new Map()
155
- }
156
-
157
- // add child map keys/values to own map
158
- const mapIter = childMap.keys()
159
-
160
- let mapCur = mapIter.next()
161
- while (!mapCur.done) {
162
- const key = mapCur.value
163
- const val = childMap.get(key)!
164
- map.set(key, val)
165
- mapCur = mapIter.next()
77
+ const stack = [{ node: root, children: getObjectChildren(root).values() }]
78
+ while (stack.length > 0) {
79
+ const frame = stack[stack.length - 1]
80
+ const child = frame.children.next()
81
+ if (!child.done) {
82
+ stack.push({ node: child.value, children: getObjectChildren(child.value).values() })
83
+ } else {
84
+ stack.pop()
85
+ const ret = visit(frame.node)
86
+ if (ret !== undefined) {
87
+ return ret
166
88
  }
167
89
  }
168
-
169
- ch = childrenIter.next()
170
90
  }
171
91
 
172
- // add it at the end so parent resolutions have higher
173
- // priority than child ones
174
- if (rootVal !== undefined) {
175
- if (!map) {
176
- map = new Map()
177
- }
178
- map.set(rootVal, target)
179
- }
180
-
181
- return map
92
+ return undefined
182
93
  }