mobx-keystone 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/mobx-keystone.esm.js +2341 -780
- package/dist/mobx-keystone.esm.mjs +2341 -780
- package/dist/mobx-keystone.umd.js +2346 -782
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- 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/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/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- package/package.json +2 -2
- package/src/actionMiddlewares/transactionMiddleware.ts +40 -41
- package/src/actionMiddlewares/undoMiddleware.ts +47 -13
- package/src/model/BaseModel.ts +6 -4
- package/src/modelShared/modelDecorator.ts +5 -25
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/onChildAttachedTo.ts +130 -44
- package/src/parent/path.ts +10 -2
- 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/ref/core.ts +63 -27
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/snapshot/applySnapshot.ts +26 -18
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +67 -10
- 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 +107 -27
- package/src/tweaker/tweakPlainObject.ts +56 -31
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +5 -104
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +522 -94
- package/src/types/objectBased/typesRecord.ts +2 -5
- package/src/utils/ModelPool.ts +75 -19
- package/src/utils/chunks.ts +271 -0
- package/src/utils/index.ts +37 -0
|
@@ -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) {
|
package/src/parent/path.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
85
|
+
return parentPath as ParentPath<T> | undefined
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
package/src/parent/setParent.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
+
}
|
package/src/parent/walkTree.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|