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
@@ -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
  /**
@@ -119,16 +127,6 @@ export function fastGetParent<T extends object = any>(
119
127
  return fastGetParentPath(value, useAtom)?.parent
120
128
  }
121
129
 
122
- /**
123
- * @internal
124
- */
125
- export function fastGetParentIncludingDataObjects<T extends object = any>(
126
- value: object,
127
- useAtom: boolean
128
- ): T | undefined {
129
- return fastGetParentPathIncludingDataObjects(value, useAtom)?.parent
130
- }
131
-
132
130
  /**
133
131
  * Returns if a given object is a model interim data object (`$`).
134
132
  *
@@ -17,9 +17,11 @@ import {
17
17
  reportParentPathChanged,
18
18
  setDataObjectParent,
19
19
  } from "./core"
20
- import { addObjectChild, removeObjectChild } from "./coreObjectChildren"
20
+ import { addObjectChild, hasObjectChildren, removeObjectChild } from "./coreObjectChildren"
21
21
  import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
22
22
 
23
+ const reindexChunkSize = 4096
24
+
23
25
  /**
24
26
  * Updates the unchanged tail of an array before a splice is applied. These
25
27
  * children keep their parent, so neither attachment nor child-index work is
@@ -29,18 +31,24 @@ import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
29
31
  export const reindexArrayChildren = action(
30
32
  "reindexArrayChildren",
31
33
  (array: readonly unknown[], oldStart: number, newStart: number): void => {
32
- for (let i = oldStart, j = newStart; i < array.length; i++, j++) {
33
- const value = array[i]
34
- if (isPrimitive(value)) {
35
- continue
36
- }
37
- const metadata = treeNodeMetadata.get(value as object)
38
- if (inDevMode && metadata?.parentPath?.parent !== array) {
39
- 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)
40
51
  }
41
- // Replace the path object: callers may have retained the previous path.
42
- metadata!.parentPath = { parent: array, path: j }
43
- reportParentPathChanged(value as object)
44
52
  }
45
53
  }
46
54
  )
@@ -123,11 +131,15 @@ export const setParent = action(
123
131
 
124
132
  const postUntweaker = parentPath ? undefined : tryUntweak(value)
125
133
 
126
- 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))
127
139
 
128
140
  let oldRoot: any
129
141
  let oldRootStore: any
130
- if (valueIsModel) {
142
+ if (checkRootStore) {
131
143
  oldRoot = fastGetRoot(value, false)
132
144
  oldRootStore = fastIsRootStoreNoAtom(oldRoot) ? oldRoot : undefined
133
145
  }
@@ -144,7 +156,7 @@ export const setParent = action(
144
156
  }
145
157
  reportParentPathChanged(value)
146
158
 
147
- if (valueIsModel) {
159
+ if (checkRootStore) {
148
160
  const newRoot = fastGetRoot(value, false)
149
161
  const newRootStore = fastIsRootStoreNoAtom(newRoot) ? newRoot : undefined
150
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
 
@@ -92,75 +91,3 @@ function walkTreeChildrenFirst<T = void>(
92
91
 
93
92
  return undefined
94
93
  }
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 children = getObjectChildren(target)
140
-
141
- // With one child and no local value, the child's map can be reused.
142
- if (rootVal === undefined && children.size === 1) {
143
- return recurse(children.values().next().value!)
144
- }
145
-
146
- for (const child of children) {
147
- const childMap = recurse(child)
148
- if (childMap) {
149
- map ??= new Map()
150
- for (const [key, value] of childMap) {
151
- map.set(key, value)
152
- }
153
- }
154
- }
155
-
156
- // add it at the end so parent resolutions have higher
157
- // priority than child ones
158
- if (rootVal !== undefined) {
159
- if (!map) {
160
- map = new Map()
161
- }
162
- map.set(rootVal, target)
163
- }
164
-
165
- return map
166
- }
@@ -6,8 +6,8 @@ import { modelToDataNode } from "../parent/core"
6
6
  import type { Patch } from "../patch/Patch"
7
7
  import { internalApplySnapshot } from "../snapshot/applySnapshot"
8
8
  import { reconcileSnapshot } from "../snapshot/reconcileSnapshot"
9
+ import { withChangeBatch } from "../tweaker/changeRollback"
9
10
  import { assertTweakedObject } from "../tweaker/core"
10
- import { withTypeCheckingBatch } from "../tweaker/typeChecking"
11
11
  import { failure, inDevMode, isArray, lazy } from "../utils"
12
12
  import { runWithErrorDiagnosticsContext, withErrorPathSegments } from "../utils/errorDiagnostics"
13
13
  import { ModelPool } from "../utils/ModelPool"
@@ -16,7 +16,8 @@ import { setIfDifferent } from "../utils/setIfDifferent"
16
16
  /**
17
17
  * Applies the given patches to the given target object.
18
18
  * Automatic type checking validates the completed batch, including nested lists.
19
- * A rejected batch is rolled back with compensating patches.
19
+ * A batch that throws or is rejected is rolled back with compensating patches.
20
+ * Errors thrown by listeners do not roll it back.
20
21
  *
21
22
  * @param node Target object.
22
23
  * @param patches List of patches to apply.
@@ -48,35 +49,39 @@ export function internalApplyPatches(
48
49
  const obj = this
49
50
  const modelPool = new ModelPool(obj)
50
51
 
51
- withTypeCheckingBatch(() => {
52
- if (reverse) {
53
- let i = patches.length
54
- while (i--) {
55
- const p = patches[i]
56
- if (isArray(p)) {
57
- let j = p.length
58
- while (j--) {
59
- applySinglePatchWithPath(obj, p[j], modelPool)
52
+ try {
53
+ withChangeBatch(() => {
54
+ if (reverse) {
55
+ let i = patches.length
56
+ while (i--) {
57
+ const p = patches[i]
58
+ if (isArray(p)) {
59
+ let j = p.length
60
+ while (j--) {
61
+ applySinglePatchWithPath(obj, p[j], modelPool)
62
+ }
63
+ } else {
64
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
60
65
  }
61
- } else {
62
- applySinglePatchWithPath(obj, p as Patch, modelPool)
63
66
  }
64
- }
65
- } else {
66
- const len = patches.length
67
- for (let i = 0; i < len; i++) {
68
- const p = patches[i]
69
- if (isArray(p)) {
70
- const len2 = p.length
71
- for (let j = 0; j < len2; j++) {
72
- applySinglePatchWithPath(obj, p[j], modelPool)
67
+ } else {
68
+ const len = patches.length
69
+ for (let i = 0; i < len; i++) {
70
+ const p = patches[i]
71
+ if (isArray(p)) {
72
+ const len2 = p.length
73
+ for (let j = 0; j < len2; j++) {
74
+ applySinglePatchWithPath(obj, p[j], modelPool)
75
+ }
76
+ } else {
77
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
73
78
  }
74
- } else {
75
- applySinglePatchWithPath(obj, p as Patch, modelPool)
76
79
  }
77
80
  }
78
- }
79
- })
81
+ })
82
+ } finally {
83
+ modelPool.release()
84
+ }
80
85
  })
81
86
  }
82
87
 
@@ -108,36 +108,21 @@ export function internalPatchRecorder(
108
108
  deep: false,
109
109
  })
110
110
 
111
- let onPatchesDisposer: OnPatchesDisposer
112
-
113
- if (subtreeRoot) {
114
- onPatchesDisposer = internalOnPatches(
115
- subtreeRoot,
116
- (p, invP) => {
117
- if (recording && filter(p, invP)) {
118
- events.push({
119
- target: subtreeRoot,
120
- patches: p,
121
- inversePatches: invP,
122
- })
123
- opts?.onPatches?.(p, invP)
124
- }
125
- },
126
- true
127
- )
128
- } else {
129
- onPatchesDisposer = internalOnGlobalPatches((target, p, invP) => {
130
- if (recording && filter(p, invP)) {
131
- events.push({
132
- target,
133
- patches: p,
134
- inversePatches: invP,
135
- })
136
- opts?.onPatches?.(p, invP)
137
- }
138
- }, true)
111
+ const record = (target: object, p: Patch[], invP: Patch[]) => {
112
+ if (recording && filter(p, invP)) {
113
+ events.push({
114
+ target,
115
+ patches: p,
116
+ inversePatches: invP,
117
+ })
118
+ opts?.onPatches?.(p, invP)
119
+ }
139
120
  }
140
121
 
122
+ const onPatchesDisposer: OnPatchesDisposer = subtreeRoot
123
+ ? internalOnPatches(subtreeRoot, (p, invP) => record(subtreeRoot, p, invP), true)
124
+ : internalOnGlobalPatches(record, true)
125
+
141
126
  return {
142
127
  get recording() {
143
128
  return recording
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>>
@@ -192,12 +190,6 @@ export function getModelRefId(target: object): string | undefined {
192
190
  return undefined
193
191
  }
194
192
 
195
- // one computed id tree per id function
196
- const computedIdTrees = new WeakMap<
197
- (node: object) => string | undefined,
198
- ComputedWalkTreeAggregate<string>
199
- >()
200
-
201
193
  /**
202
194
  * Resolves a node given its ID.
203
195
  *
@@ -212,38 +204,32 @@ export function resolveId<T extends object>(
212
204
  id: string,
213
205
  getId: RefIdResolver = getModelRefId
214
206
  ): T | undefined {
215
- // cache/reuse computedIdTrees for same getId function
216
- const computedIdTree = getOrCreate(computedIdTrees, getId, () =>
217
- computedWalkTreeAggregate<string>((node) => getId(node))
218
- )
207
+ // parent resolutions have priority over child ones
208
+ if (getId(root) === id) {
209
+ return root as T
210
+ }
211
+
212
+ if (getId === getModelRefId) {
213
+ const found = resolveModelRefId(root, id)
214
+ if (found !== modelRefIdNotIndexed) {
215
+ return found as T | undefined
216
+ }
217
+ }
219
218
 
220
- const idMap = computedIdTree.walk(root)
221
- return idMap ? (idMap.get(id) as T | undefined) : undefined
219
+ // custom ids (or models with a custom `getRefId`)
220
+ return resolveRefIdByIndex(root, id, getId) as T | undefined
222
221
  }
223
222
 
223
+ const createRefSet = () => observable.set<Ref<any>>(undefined, { deep: false })
224
+
225
+ const createBackRefs = (): BackRefs<any> => ({ all: createRefSet(), byType: new WeakMap() })
226
+
224
227
  function getBackRefs<T extends object>(
225
228
  target: T,
226
229
  refType?: RefConstructor<T>
227
230
  ): ObservableSet<Ref<T>> {
228
- let backRefs = objectBackRefs.get(target) as BackRefs<T> | undefined
229
- if (!backRefs) {
230
- backRefs = {
231
- all: observable.set(undefined, { deep: false }),
232
- byType: new WeakMap(),
233
- }
234
- objectBackRefs.set(target, backRefs)
235
- }
236
-
237
- if (refType) {
238
- let byType = backRefs.byType.get(refType)
239
- if (!byType) {
240
- byType = observable.set(undefined, { deep: false })
241
- backRefs.byType.set(refType, byType)
242
- }
243
- return byType
244
- } else {
245
- return backRefs.all
246
- }
231
+ const backRefs: BackRefs<T> = getOrCreate(objectBackRefs, target, createBackRefs)
232
+ return refType ? getOrCreate(backRefs.byType, refType, createRefSet) : backRefs.all
247
233
  }
248
234
 
249
235
  /**
@@ -285,14 +271,25 @@ export function getRefsResolvingTo<T extends object>(
285
271
  oldBackRefs.forEach(updateRef)
286
272
  }
287
273
 
288
- const refsChildrenOfRoot = getDeepChildrenRefs(getDeepObjectChildren(fastGetRoot(target, true)))
289
- let refs: Set<Ref<object>> | undefined
290
- if (refType) {
291
- refs = refsChildrenOfRoot.byType.get(refType.refClass)
274
+ const refsChildrenOfRoot = getDeepChildrenRefs.get(fastGetRoot(target, true))
275
+ if (shouldForceUpdateTrackedRefs) {
276
+ const refs = refType ? refsChildrenOfRoot.byType.get(refType.refClass) : refsChildrenOfRoot.all
277
+ refs?.forEach(updateRef)
292
278
  } else {
293
- refs = refsChildrenOfRoot.all
279
+ // tracked refs keep their back-references up to date by themselves
280
+ const untrackedRefs = refType
281
+ ? refsChildrenOfRoot.untrackedByType.get(refType.refClass)
282
+ : refsChildrenOfRoot.untracked
283
+ if (untrackedRefs && untrackedRefs.size > 0) {
284
+ // copy, since updating back-references might change the tree
285
+ for (const ref of Array.from(untrackedRefs)) {
286
+ updateRef(ref)
287
+ if (trackedRefs.has(ref)) {
288
+ deleteRef(refsChildrenOfRoot.untracked, refsChildrenOfRoot.untrackedByType, ref)
289
+ }
290
+ }
291
+ }
294
292
  }
295
- refs?.forEach(updateRef)
296
293
 
297
294
  return getBackRefs(target, refType)
298
295
  }
@@ -332,9 +329,24 @@ function isReactionDelayed() {
332
329
  return reactionDelayed
333
330
  }
334
331
 
332
+ type RefsByType = WeakMap<ModelClass<Ref<any>>, Set<Ref<any>>>
333
+
335
334
  interface DeepChildrenRefs {
336
335
  all: Set<Ref<any>>
337
- byType: WeakMap<ModelClass<Ref<any>>, Set<Ref<any>>>
336
+ byType: RefsByType
337
+ /** Refs whose back-references might not be tracked yet. */
338
+ untracked: Set<Ref<any>>
339
+ untrackedByType: RefsByType
340
+ }
341
+
342
+ function addRef(all: Set<Ref<any>>, byType: RefsByType, ref: Ref<any>) {
343
+ all.add(ref)
344
+ getOrCreate(byType, ref.constructor as ModelClass<Ref<any>>, () => new Set()).add(ref)
345
+ }
346
+
347
+ function deleteRef(all: Set<Ref<any>>, byType: RefsByType, ref: Ref<any>) {
348
+ all.delete(ref)
349
+ byType.get(ref.constructor as ModelClass<Ref<any>>)?.delete(ref)
338
350
  }
339
351
 
340
352
  const getDeepChildrenRefs = registerDeepObjectChildrenExtension<DeepChildrenRefs>({
@@ -342,14 +354,25 @@ const getDeepChildrenRefs = registerDeepObjectChildrenExtension<DeepChildrenRefs
342
354
  return {
343
355
  all: new Set(),
344
356
  byType: new WeakMap(),
357
+ untracked: new Set(),
358
+ untrackedByType: new WeakMap(),
345
359
  }
346
360
  },
347
361
 
348
362
  addNode(node, data) {
349
363
  if (node instanceof Ref) {
350
- data.all.add(node)
351
- const refsByThisType = getOrCreate(data.byType, node.constructor, () => new Set())
352
- refsByThisType.add(node)
364
+ addRef(data.all, data.byType, node)
365
+ if (!trackedRefs.has(node)) {
366
+ addRef(data.untracked, data.untrackedByType, node)
367
+ }
368
+ }
369
+ },
370
+
371
+ removeNode(node, data) {
372
+ if (node instanceof Ref) {
373
+ deleteRef(data.all, data.byType, node)
374
+ deleteRef(data.untracked, data.untrackedByType, node)
353
375
  }
376
+ return true
354
377
  },
355
378
  })