mobx-keystone 1.22.0 → 1.23.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 +7 -0
- package/dist/mobx-keystone.esm.js +6702 -5797
- package/dist/mobx-keystone.esm.mjs +6702 -5797
- package/dist/mobx-keystone.umd.js +6718 -5798
- package/dist/types/action/runUnprotected.d.ts +8 -4
- package/dist/types/model/Model.d.ts +5 -0
- package/dist/types/modelShared/prop.d.ts +8 -4
- package/dist/types/snapshot/modelInitialData.d.ts +1 -0
- package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
- package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
- package/package.json +11 -10
- package/src/action/middleware.ts +5 -18
- package/src/action/mutationBatch.ts +72 -0
- package/src/action/runUnprotected.ts +21 -8
- package/src/action/wrapInAction.ts +45 -15
- package/src/dataModel/BaseDataModel.ts +4 -8
- package/src/deepChange/onDeepChange.ts +246 -16
- package/src/model/BaseModel.ts +4 -12
- package/src/model/Model.ts +5 -0
- package/src/model/newModel.ts +68 -36
- package/src/modelShared/prop.ts +105 -63
- package/src/modelShared/sharedInternalModel.ts +91 -60
- package/src/parent/core.ts +16 -10
- package/src/parent/coreObjectChildren.ts +219 -21
- package/src/parent/path.ts +6 -5
- package/src/parent/setParent.ts +5 -5
- package/src/patch/emitPatch.ts +102 -20
- package/src/snapshot/fromArraySnapshot.ts +11 -2
- package/src/snapshot/fromModelSnapshot.ts +10 -3
- package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
- package/src/snapshot/fromSnapshot.ts +31 -9
- package/src/snapshot/getSnapshot.ts +8 -3
- package/src/snapshot/internal.ts +243 -82
- package/src/snapshot/modelInitialData.ts +41 -0
- package/src/snapshot/reconcileArraySnapshot.ts +9 -3
- package/src/snapshot/reconcileModelSnapshot.ts +97 -17
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
- package/src/tweaker/core.ts +30 -5
- package/src/tweaker/treeNodeMetadata.ts +38 -0
- package/src/tweaker/tweak.ts +15 -4
- package/src/tweaker/tweakArray.ts +159 -121
- package/src/tweaker/tweakFrozen.ts +2 -2
- package/src/tweaker/tweakModel.ts +2 -2
- package/src/tweaker/tweakPlainObject.ts +114 -122
- package/src/tweaker/typeChecking.ts +10 -1
- package/src/types/TypeChecker.ts +261 -15
- package/src/types/arrayBased/typesArray.ts +38 -40
- package/src/types/arrayBased/typesTuple.ts +51 -34
- package/src/types/createCachedTypeCheck.ts +181 -0
- package/src/types/objectBased/typesArraySet.ts +42 -32
- package/src/types/objectBased/typesDataModelData.ts +18 -11
- package/src/types/objectBased/typesModel.ts +1 -1
- package/src/types/objectBased/typesObject.ts +57 -28
- package/src/types/objectBased/typesObjectMap.ts +51 -40
- package/src/types/objectBased/typesRecord.ts +59 -39
- package/src/types/objectBased/typesRef.ts +1 -1
- package/src/types/primitiveBased/typesPrimitive.ts +9 -9
- package/src/types/tProp.ts +80 -44
- package/src/types/utility/typesOr.ts +35 -24
- package/src/types/utility/typesRefinement.ts +21 -8
- package/src/types/utility/typesSkipCheck.ts +21 -8
- package/src/types/utility/typesTag.ts +21 -9
- package/src/types/utility/typesUnchecked.ts +2 -3
- package/src/utils/ModelPool.ts +34 -19
- package/src/utils/errorDiagnostics.ts +60 -18
- package/src/utils/index.ts +9 -7
- package/src/types/createPerEntryCachedCheck.ts +0 -166
- /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { action, createAtom, type IAtom } from "mobx"
|
|
2
|
+
import { addMutationBatchFinisher, isMutationBatchActive } from "../action/mutationBatch"
|
|
3
|
+
import { getOrCreateTreeNodeMetadata } from "../tweaker/treeNodeMetadata"
|
|
2
4
|
import { fastGetParent } from "./path"
|
|
3
5
|
|
|
4
6
|
interface DeepObjectChildren {
|
|
5
7
|
deep: Set<object>
|
|
6
8
|
|
|
7
|
-
extensionsData: Map<object, unknown>
|
|
9
|
+
extensionsData: Map<object, unknown> | undefined
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/** @internal */
|
|
13
|
+
export interface ObjectChildrenData extends DeepObjectChildren {
|
|
11
14
|
shallow: Set<object>
|
|
12
15
|
shallowAtom: IAtom | undefined // will be created when first observed
|
|
13
16
|
|
|
@@ -15,10 +18,55 @@ interface ObjectChildrenData extends DeepObjectChildren {
|
|
|
15
18
|
deepAtom: IAtom | undefined // will be created when first observed
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
interface DeepChildrenInvalidationEntry {
|
|
22
|
+
readonly obj: ObjectChildrenData
|
|
23
|
+
rebuilt: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DirectChildrenMutationEntry {
|
|
27
|
+
added: Set<object> | undefined
|
|
28
|
+
removed: Set<object> | undefined
|
|
29
|
+
invalidatedNodes: Set<object> | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DeepChildrenMutationTransaction {
|
|
33
|
+
directMutations: Map<object, DirectChildrenMutationEntry> | undefined
|
|
34
|
+
invalidatedNodes: Map<object, DeepChildrenInvalidationEntry> | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let currentDeepChildrenMutationTransaction: DeepChildrenMutationTransaction | undefined
|
|
38
|
+
|
|
39
|
+
function getCurrentDeepChildrenMutationTransaction(): DeepChildrenMutationTransaction | undefined {
|
|
40
|
+
return currentDeepChildrenMutationTransaction
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getOrCreateDeepChildrenMutationTransaction(): DeepChildrenMutationTransaction | undefined {
|
|
44
|
+
if (!isMutationBatchActive()) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const currentTransaction = currentDeepChildrenMutationTransaction
|
|
49
|
+
if (currentTransaction) {
|
|
50
|
+
return currentTransaction
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const transaction: DeepChildrenMutationTransaction = {
|
|
54
|
+
directMutations: undefined,
|
|
55
|
+
invalidatedNodes: undefined,
|
|
56
|
+
}
|
|
57
|
+
currentDeepChildrenMutationTransaction = transaction
|
|
58
|
+
addMutationBatchFinisher(() => {
|
|
59
|
+
if (currentDeepChildrenMutationTransaction === transaction) {
|
|
60
|
+
currentDeepChildrenMutationTransaction = undefined
|
|
61
|
+
finishDeepChildrenMutationTransaction(transaction)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
return transaction
|
|
65
|
+
}
|
|
19
66
|
|
|
20
67
|
function getObjectChildrenObject(node: object) {
|
|
21
|
-
|
|
68
|
+
const metadata = getOrCreateTreeNodeMetadata(node)
|
|
69
|
+
let obj = metadata.objectChildren
|
|
22
70
|
|
|
23
71
|
if (!obj) {
|
|
24
72
|
obj = {
|
|
@@ -31,7 +79,7 @@ function getObjectChildrenObject(node: object) {
|
|
|
31
79
|
|
|
32
80
|
extensionsData: initExtensionsData(),
|
|
33
81
|
}
|
|
34
|
-
objectChildren
|
|
82
|
+
metadata.objectChildren = obj
|
|
35
83
|
}
|
|
36
84
|
|
|
37
85
|
return obj
|
|
@@ -69,17 +117,36 @@ export function getDeepObjectChildren(node: object): DeepObjectChildren {
|
|
|
69
117
|
|
|
70
118
|
function addNodeToDeepLists(node: any, data: DeepObjectChildren) {
|
|
71
119
|
data.deep.add(node)
|
|
72
|
-
data.extensionsData
|
|
120
|
+
data.extensionsData?.forEach((extensionData, dataSymbol) => {
|
|
73
121
|
extensions.get(dataSymbol)!.addNode(node, extensionData)
|
|
74
122
|
})
|
|
75
123
|
}
|
|
76
124
|
|
|
77
125
|
const updateDeepObjectChildren = action((node: object): DeepObjectChildren => {
|
|
126
|
+
return updateDeepObjectChildrenRecursive(node)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
function updateDeepObjectChildrenRecursive(node: object): DeepObjectChildren {
|
|
78
130
|
const obj = getObjectChildrenObject(node)
|
|
79
131
|
if (!obj.deepDirty) {
|
|
80
132
|
return obj
|
|
81
133
|
}
|
|
82
134
|
|
|
135
|
+
const transaction = getCurrentDeepChildrenMutationTransaction()
|
|
136
|
+
if (transaction) {
|
|
137
|
+
const invalidatedNodes = (transaction.invalidatedNodes ??= new Map())
|
|
138
|
+
const invalidationEntry = invalidatedNodes.get(node)
|
|
139
|
+
if (invalidationEntry) {
|
|
140
|
+
invalidationEntry.rebuilt = true
|
|
141
|
+
} else {
|
|
142
|
+
// a node without an entry was dirty when first invalidated in this batch
|
|
143
|
+
// (or was dirtied before it); record the rebuild so a later net-zero
|
|
144
|
+
// completion cannot restore this mid-batch state as if the node had been
|
|
145
|
+
// clean since the batch started
|
|
146
|
+
invalidatedNodes.set(node, { obj, rebuilt: true })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
83
150
|
obj.deep = new Set()
|
|
84
151
|
obj.extensionsData = initExtensionsData(obj.extensionsData)
|
|
85
152
|
|
|
@@ -88,7 +155,7 @@ const updateDeepObjectChildren = action((node: object): DeepObjectChildren => {
|
|
|
88
155
|
while (!childrenIteratorResult.done) {
|
|
89
156
|
addNodeToDeepLists(childrenIteratorResult.value, obj)
|
|
90
157
|
|
|
91
|
-
const childDeepChildren =
|
|
158
|
+
const childDeepChildren = updateDeepObjectChildrenRecursive(childrenIteratorResult.value).deep
|
|
92
159
|
const childDeepChildrenIterator = childDeepChildren.values()
|
|
93
160
|
let childDeepChildrenIteratorResult = childDeepChildrenIterator.next()
|
|
94
161
|
while (!childDeepChildrenIteratorResult.done) {
|
|
@@ -103,35 +170,115 @@ const updateDeepObjectChildren = action((node: object): DeepObjectChildren => {
|
|
|
103
170
|
obj.deepAtom?.reportChanged()
|
|
104
171
|
|
|
105
172
|
return obj
|
|
106
|
-
}
|
|
173
|
+
}
|
|
107
174
|
|
|
108
175
|
/**
|
|
176
|
+
* Must run inside the action-wrapped `setParent`, its only source caller.
|
|
177
|
+
*
|
|
109
178
|
* @internal
|
|
110
179
|
*/
|
|
111
|
-
export
|
|
180
|
+
export function addObjectChild(node: object, child: object): void {
|
|
112
181
|
const obj = getObjectChildrenObject(node)
|
|
113
|
-
obj.shallow
|
|
114
|
-
|
|
182
|
+
const shallow = obj.shallow
|
|
183
|
+
const previousShallowSize = shallow.size
|
|
184
|
+
shallow.add(child)
|
|
185
|
+
if (shallow.size === previousShallowSize) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
115
188
|
|
|
116
|
-
|
|
117
|
-
}
|
|
189
|
+
onShallowChildrenChanged(node, obj, child, true)
|
|
190
|
+
}
|
|
118
191
|
|
|
119
192
|
/**
|
|
193
|
+
* Must run inside the action-wrapped `setParent`, its only source caller.
|
|
194
|
+
*
|
|
120
195
|
* @internal
|
|
121
196
|
*/
|
|
122
|
-
export
|
|
197
|
+
export function removeObjectChild(node: object, child: object): void {
|
|
123
198
|
const obj = getObjectChildrenObject(node)
|
|
124
|
-
obj.shallow.delete(child)
|
|
199
|
+
if (!obj.shallow.delete(child)) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
onShallowChildrenChanged(node, obj, child, false)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function onShallowChildrenChanged(
|
|
207
|
+
node: object,
|
|
208
|
+
obj: ObjectChildrenData,
|
|
209
|
+
child: object,
|
|
210
|
+
added: boolean
|
|
211
|
+
) {
|
|
125
212
|
obj.shallowAtom?.reportChanged()
|
|
126
213
|
|
|
127
|
-
|
|
128
|
-
|
|
214
|
+
const transaction =
|
|
215
|
+
currentDeepChildrenMutationTransaction ??
|
|
216
|
+
(obj.deepDirty ? undefined : getOrCreateDeepChildrenMutationTransaction())
|
|
217
|
+
invalidateDeepChildren(
|
|
218
|
+
node,
|
|
219
|
+
obj,
|
|
220
|
+
transaction,
|
|
221
|
+
recordDirectChildrenMutation(transaction, node, child, added)
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function recordDirectChildrenMutation(
|
|
226
|
+
transaction: DeepChildrenMutationTransaction | undefined,
|
|
227
|
+
node: object,
|
|
228
|
+
child: object,
|
|
229
|
+
added: boolean
|
|
230
|
+
): DirectChildrenMutationEntry | undefined {
|
|
231
|
+
if (!transaction) {
|
|
232
|
+
return undefined
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const directMutations = (transaction.directMutations ??= new Map())
|
|
236
|
+
let mutation = directMutations.get(node)
|
|
237
|
+
if (!mutation) {
|
|
238
|
+
mutation = {
|
|
239
|
+
added: undefined,
|
|
240
|
+
removed: undefined,
|
|
241
|
+
invalidatedNodes: undefined,
|
|
242
|
+
}
|
|
243
|
+
directMutations.set(node, mutation)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (added) {
|
|
247
|
+
if (!mutation.removed?.delete(child)) {
|
|
248
|
+
;(mutation.added ??= new Set()).add(child)
|
|
249
|
+
}
|
|
250
|
+
} else if (!mutation.added?.delete(child)) {
|
|
251
|
+
;(mutation.removed ??= new Set()).add(child)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return mutation
|
|
255
|
+
}
|
|
129
256
|
|
|
130
|
-
function invalidateDeepChildren(
|
|
257
|
+
function invalidateDeepChildren(
|
|
258
|
+
node: object,
|
|
259
|
+
obj: ObjectChildrenData,
|
|
260
|
+
transaction: DeepChildrenMutationTransaction | undefined,
|
|
261
|
+
mutation: DirectChildrenMutationEntry | undefined
|
|
262
|
+
) {
|
|
131
263
|
let currentNode: object | undefined = node
|
|
132
264
|
let currentObj = obj
|
|
133
265
|
|
|
134
266
|
while (currentNode) {
|
|
267
|
+
if (transaction) {
|
|
268
|
+
let entry = transaction.invalidatedNodes?.get(currentNode)
|
|
269
|
+
if (!entry && !currentObj.deepDirty) {
|
|
270
|
+
const invalidatedNodes = (transaction.invalidatedNodes ??= new Map())
|
|
271
|
+
entry = {
|
|
272
|
+
obj: currentObj,
|
|
273
|
+
rebuilt: false,
|
|
274
|
+
}
|
|
275
|
+
invalidatedNodes.set(currentNode, entry)
|
|
276
|
+
}
|
|
277
|
+
if (mutation && entry) {
|
|
278
|
+
;(mutation.invalidatedNodes ??= new Set()).add(currentNode)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
135
282
|
currentObj.deepDirty = true
|
|
136
283
|
currentObj.deepAtom?.reportChanged()
|
|
137
284
|
|
|
@@ -142,6 +289,51 @@ function invalidateDeepChildren(node: object, obj: ObjectChildrenData) {
|
|
|
142
289
|
}
|
|
143
290
|
}
|
|
144
291
|
|
|
292
|
+
function finishDeepChildrenMutationTransaction(transaction: DeepChildrenMutationTransaction) {
|
|
293
|
+
const invalidatedNodes = transaction.invalidatedNodes
|
|
294
|
+
if (!invalidatedNodes) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// when every mutated parent has net membership changes every invalidated
|
|
299
|
+
// node must stay dirty, so there is nothing to restore
|
|
300
|
+
let hasNetZeroMutation = false
|
|
301
|
+
transaction.directMutations?.forEach((mutation) => {
|
|
302
|
+
if ((mutation.added?.size ?? 0) === 0 && (mutation.removed?.size ?? 0) === 0) {
|
|
303
|
+
hasNetZeroMutation = true
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
if (!hasNetZeroMutation) {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let genuinelyChangedNodes: Set<object> | undefined
|
|
311
|
+
transaction.directMutations?.forEach((mutation, node) => {
|
|
312
|
+
if ((mutation.added?.size ?? 0) > 0 || (mutation.removed?.size ?? 0) > 0) {
|
|
313
|
+
const changedNodes = (genuinelyChangedNodes ??= new Set())
|
|
314
|
+
mutation.invalidatedNodes?.forEach((invalidatedNode) => {
|
|
315
|
+
changedNodes.add(invalidatedNode)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
// a genuine membership change also dirties the node's ancestor chain as
|
|
319
|
+
// it stands now, which may differ from the chains recorded at mutation
|
|
320
|
+
// time (e.g. a subtree mutated while detached and then re-attached with
|
|
321
|
+
// a net-zero remove/add pair at its parent)
|
|
322
|
+
let currentNode: object | undefined = node
|
|
323
|
+
while (currentNode) {
|
|
324
|
+
changedNodes.add(currentNode)
|
|
325
|
+
currentNode = fastGetParent(currentNode, false)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
invalidatedNodes.forEach((entry, node) => {
|
|
331
|
+
if (!genuinelyChangedNodes?.has(node) && !entry.rebuilt) {
|
|
332
|
+
entry.obj.deepDirty = false
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
145
337
|
const extensions = new Map<object, DeepObjectChildrenExtension<any>>()
|
|
146
338
|
|
|
147
339
|
interface DeepObjectChildrenExtension<D> {
|
|
@@ -157,10 +349,12 @@ export function registerDeepObjectChildrenExtension<D>(extension: DeepObjectChil
|
|
|
157
349
|
extensions.set(dataSymbol, extension)
|
|
158
350
|
|
|
159
351
|
return (data: DeepObjectChildren): D => {
|
|
160
|
-
let
|
|
161
|
-
|
|
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()
|
|
162
356
|
extensionData = extension.initData()
|
|
163
|
-
|
|
357
|
+
extensionsData.set(dataSymbol, extensionData)
|
|
164
358
|
data.deep.forEach((node) => {
|
|
165
359
|
extension.addNode(node, extensionData!)
|
|
166
360
|
})
|
|
@@ -170,6 +364,10 @@ export function registerDeepObjectChildrenExtension<D>(extension: DeepObjectChil
|
|
|
170
364
|
}
|
|
171
365
|
|
|
172
366
|
function initExtensionsData(previousData?: ReadonlyMap<object, unknown>) {
|
|
367
|
+
if (previousData === undefined || previousData.size === 0) {
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
|
|
173
371
|
const extensionsData = new Map<object, unknown>()
|
|
174
372
|
|
|
175
373
|
previousData?.forEach((_, dataSymbol) => {
|
package/src/parent/path.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { modelIdKey } from "../model/metadata"
|
|
2
2
|
import { isModel } from "../model/utils"
|
|
3
3
|
import { assertTweakedObject } from "../tweaker/core"
|
|
4
|
+
import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
|
|
4
5
|
import { hasOwnProp, isArray, isObject } from "../utils"
|
|
5
6
|
import {
|
|
6
|
-
dataObjectParent,
|
|
7
7
|
dataToModelNode,
|
|
8
|
+
getDataObjectParent,
|
|
9
|
+
hasDataObjectParent,
|
|
8
10
|
modelToDataNode,
|
|
9
|
-
objectParents,
|
|
10
11
|
reportParentPathObserved,
|
|
11
12
|
} from "./core"
|
|
12
13
|
import type { Path, PathElement, WritablePath } from "./pathTypes"
|
|
@@ -73,7 +74,7 @@ export function fastGetParentPath<T extends object = any>(
|
|
|
73
74
|
if (useAtom) {
|
|
74
75
|
reportParentPathObserved(value)
|
|
75
76
|
}
|
|
76
|
-
return
|
|
77
|
+
return treeNodeMetadata.get(value)?.parentPath as ParentPath<T> | undefined
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
@@ -83,7 +84,7 @@ export function fastGetParentPathIncludingDataObjects<T extends object = any>(
|
|
|
83
84
|
value: object,
|
|
84
85
|
useAtom: boolean
|
|
85
86
|
): ParentPath<T> | undefined {
|
|
86
|
-
const parentModel =
|
|
87
|
+
const parentModel = getDataObjectParent(value)
|
|
87
88
|
if (parentModel) {
|
|
88
89
|
return { parent: parentModel as T, path: "$" }
|
|
89
90
|
}
|
|
@@ -144,7 +145,7 @@ export function isModelDataObject(value: object): boolean {
|
|
|
144
145
|
* @internal
|
|
145
146
|
*/
|
|
146
147
|
export function fastIsModelDataObject(value: object): boolean {
|
|
147
|
-
return
|
|
148
|
+
return hasDataObjectParent(value)
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/**
|
package/src/parent/setParent.ts
CHANGED
|
@@ -7,14 +7,14 @@ import { attachToRootStore, detachFromRootStore } from "../rootStore/attachDetac
|
|
|
7
7
|
import { fastIsRootStoreNoAtom } from "../rootStore/rootStore"
|
|
8
8
|
import { clone } from "../snapshot/clone"
|
|
9
9
|
import { isTweakedObject } from "../tweaker/core"
|
|
10
|
+
import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
|
|
10
11
|
import { tryUntweak } from "../tweaker/tweak"
|
|
11
12
|
import { failure, inDevMode, isPrimitive } from "../utils"
|
|
12
13
|
import {
|
|
13
|
-
dataObjectParent,
|
|
14
14
|
dataToModelNode,
|
|
15
|
-
objectParents,
|
|
16
15
|
parentPathEquals,
|
|
17
16
|
reportParentPathChanged,
|
|
17
|
+
setDataObjectParent,
|
|
18
18
|
} from "./core"
|
|
19
19
|
import { addObjectChild, removeObjectChild } from "./coreObjectChildren"
|
|
20
20
|
import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
|
|
@@ -62,7 +62,7 @@ export const setParent = action(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (isDataObject) {
|
|
65
|
-
|
|
65
|
+
setDataObjectParent(value, parentPath!.parent)
|
|
66
66
|
// data object will proxy to use the actual parent model for child/parent stuff
|
|
67
67
|
return value
|
|
68
68
|
}
|
|
@@ -94,7 +94,7 @@ export const setParent = action(
|
|
|
94
94
|
if (oldParentPath && parentPath) {
|
|
95
95
|
if (oldParentPath.parent === parentPath.parent && indexChangeAllowed) {
|
|
96
96
|
// just changing the index
|
|
97
|
-
|
|
97
|
+
treeNodeMetadata.get(value)!.parentPath = parentPath
|
|
98
98
|
reportParentPathChanged(value)
|
|
99
99
|
return value
|
|
100
100
|
} else {
|
|
@@ -119,7 +119,7 @@ export const setParent = action(
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// attach to new
|
|
122
|
-
|
|
122
|
+
treeNodeMetadata.get(value)!.parentPath = parentPath
|
|
123
123
|
if (parentPath?.parent) {
|
|
124
124
|
addObjectChild(parentPath.parent, value)
|
|
125
125
|
}
|
package/src/patch/emitPatch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { action, isAction } from "mobx"
|
|
2
|
-
import {
|
|
2
|
+
import { dataToModelNode } from "../parent/core"
|
|
3
|
+
import { fastGetParentPath, type ParentPath } from "../parent/path"
|
|
3
4
|
import type { PathElement } from "../parent/pathTypes"
|
|
4
5
|
import { freezeInternalSnapshot, getInternalSnapshot } from "../snapshot/internal"
|
|
5
6
|
import { assertTweakedObject } from "../tweaker/core"
|
|
@@ -14,19 +15,27 @@ const emptyPatchArray: Patch[] = []
|
|
|
14
15
|
export class InternalPatchRecorder {
|
|
15
16
|
patches: Patch[] = emptyPatchArray
|
|
16
17
|
invPatches: Patch[] = emptyPatchArray
|
|
18
|
+
hasChanges = false
|
|
17
19
|
|
|
18
20
|
reset() {
|
|
19
21
|
this.patches = emptyPatchArray
|
|
20
22
|
this.invPatches = emptyPatchArray
|
|
23
|
+
this.hasChanges = false
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
record(patches: Patch[], invPatches: Patch[]) {
|
|
24
|
-
this.patches = patches
|
|
25
|
-
this.invPatches = invPatches
|
|
26
|
+
record(patches: Patch[] | undefined, invPatches: Patch[] | undefined) {
|
|
27
|
+
this.patches = patches ?? emptyPatchArray
|
|
28
|
+
this.invPatches = invPatches ?? emptyPatchArray
|
|
29
|
+
// An effective change requires at least one forward or inverse patch. A no-op
|
|
30
|
+
// mutation (e.g. an equal-count splice where every value is identical) records
|
|
31
|
+
// empty arrays and must not trigger ancestor type checking.
|
|
32
|
+
this.hasChanges = this.patches.length > 0 || this.invPatches.length > 0
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
emit(obj: object) {
|
|
29
|
-
|
|
36
|
+
if (this.patches.length > 0) {
|
|
37
|
+
emitPatches(obj, this.patches, this.invPatches)
|
|
38
|
+
}
|
|
30
39
|
this.reset()
|
|
31
40
|
}
|
|
32
41
|
}
|
|
@@ -35,9 +44,11 @@ export class InternalPatchRecorder {
|
|
|
35
44
|
* @internal
|
|
36
45
|
*/
|
|
37
46
|
export function emitPatches(obj: object, patches: Patch[], invPatches: Patch[]): void {
|
|
38
|
-
if (patches.length > 0
|
|
47
|
+
if (patches.length > 0) {
|
|
39
48
|
emitGlobalPatch(obj, patches, invPatches)
|
|
40
|
-
|
|
49
|
+
if (patchListenerCount > 0) {
|
|
50
|
+
emitPatch(obj, patches, invPatches)
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|
|
@@ -62,6 +73,37 @@ export type OnPatchesDisposer = () => void
|
|
|
62
73
|
|
|
63
74
|
const patchListeners = new WeakMap<object, OnPatchesListener[]>()
|
|
64
75
|
const globalPatchListeners: OnGlobalPatchesListener[] = []
|
|
76
|
+
let patchListenerCount = 0
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export function hasGlobalPatchListeners(): boolean {
|
|
82
|
+
return globalPatchListeners.length > 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
export function hasPatchListenersFor(target: object): boolean {
|
|
89
|
+
if (hasGlobalPatchListeners()) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
if (patchListenerCount === 0) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let current: object | undefined = dataToModelNode(target)
|
|
97
|
+
while (current) {
|
|
98
|
+
if ((patchListeners.get(current)?.length ?? 0) > 0) {
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parentPath: ParentPath<object> | undefined = fastGetParentPath(current, false)
|
|
103
|
+
current = parentPath?.parent
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
65
107
|
|
|
66
108
|
/**
|
|
67
109
|
* Adds a listener that will be called every time a patch is generated for the tree of the given target object.
|
|
@@ -85,8 +127,11 @@ export function onPatches(subtreeRoot: object, listener: OnPatchesListener): OnP
|
|
|
85
127
|
}
|
|
86
128
|
|
|
87
129
|
listenersForObject.push(listener)
|
|
130
|
+
patchListenerCount++
|
|
88
131
|
return () => {
|
|
89
|
-
deleteFromArray(listenersForObject, listener)
|
|
132
|
+
if (deleteFromArray(listenersForObject, listener)) {
|
|
133
|
+
patchListenerCount--
|
|
134
|
+
}
|
|
90
135
|
}
|
|
91
136
|
}
|
|
92
137
|
|
|
@@ -117,11 +162,16 @@ function emitGlobalPatch(obj: object, patches: Patch[], inversePatches: Patch[])
|
|
|
117
162
|
}
|
|
118
163
|
}
|
|
119
164
|
|
|
165
|
+
// `reversedPrefix` holds the path segments from the changed node up to (but not
|
|
166
|
+
// including) the current target, in child-to-root order. The prefix to prepend to
|
|
167
|
+
// a patch is that slice reversed (root-to-child); `prefixCount` is how many of the
|
|
168
|
+
// leading entries are relevant for the current target.
|
|
120
169
|
function emitPatchForTarget(
|
|
121
170
|
obj: object,
|
|
122
171
|
patches: Patch[],
|
|
123
172
|
inversePatches: Patch[],
|
|
124
|
-
|
|
173
|
+
reversedPrefix: readonly PathElement[],
|
|
174
|
+
prefixCount: number
|
|
125
175
|
): void {
|
|
126
176
|
const listenersForObject = patchListeners.get(obj)
|
|
127
177
|
|
|
@@ -129,11 +179,10 @@ function emitPatchForTarget(
|
|
|
129
179
|
return
|
|
130
180
|
}
|
|
131
181
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const invPatchesWithPathPrefix = fixPath(inversePatches)
|
|
182
|
+
const patchesWithPathPrefix =
|
|
183
|
+
prefixCount > 0 ? prefixPatches(patches, reversedPrefix, prefixCount) : patches
|
|
184
|
+
const invPatchesWithPathPrefix =
|
|
185
|
+
prefixCount > 0 ? prefixPatches(inversePatches, reversedPrefix, prefixCount) : inversePatches
|
|
137
186
|
|
|
138
187
|
for (let i = 0; i < listenersForObject.length; i++) {
|
|
139
188
|
const listener = listenersForObject[i]
|
|
@@ -142,24 +191,57 @@ function emitPatchForTarget(
|
|
|
142
191
|
}
|
|
143
192
|
|
|
144
193
|
function emitPatch(obj: object, patches: Patch[], inversePatches: Patch[]): void {
|
|
145
|
-
|
|
194
|
+
// Segments are pushed in child-to-root order (O(1) each) rather than unshifted
|
|
195
|
+
// at the front (O(depth) each), keeping the whole walk O(depth) instead of
|
|
196
|
+
// O(depth^2) on deep trees.
|
|
197
|
+
const reversedPrefix: PathElement[] = []
|
|
146
198
|
|
|
147
|
-
emitPatchForTarget(obj, patches, inversePatches,
|
|
199
|
+
emitPatchForTarget(obj, patches, inversePatches, reversedPrefix, 0)
|
|
148
200
|
|
|
149
201
|
// and also emit subtree listeners all the way to the root
|
|
150
202
|
let parentPath = fastGetParentPath(obj, false)
|
|
151
203
|
while (parentPath) {
|
|
152
|
-
|
|
153
|
-
emitPatchForTarget(
|
|
204
|
+
reversedPrefix.push(parentPath.path)
|
|
205
|
+
emitPatchForTarget(
|
|
206
|
+
parentPath.parent,
|
|
207
|
+
patches,
|
|
208
|
+
inversePatches,
|
|
209
|
+
reversedPrefix,
|
|
210
|
+
reversedPrefix.length
|
|
211
|
+
)
|
|
154
212
|
|
|
155
213
|
parentPath = fastGetParentPath(parentPath.parent, false)
|
|
156
214
|
}
|
|
157
215
|
}
|
|
158
216
|
|
|
159
|
-
function
|
|
217
|
+
function prefixPatches(
|
|
218
|
+
patchesArray: Patch[],
|
|
219
|
+
reversedPrefix: readonly PathElement[],
|
|
220
|
+
prefixCount: number
|
|
221
|
+
): Patch[] {
|
|
222
|
+
const result = new Array<Patch>(patchesArray.length)
|
|
223
|
+
for (let i = 0; i < patchesArray.length; i++) {
|
|
224
|
+
result[i] = addPathToPatch(patchesArray[i], reversedPrefix, prefixCount)
|
|
225
|
+
}
|
|
226
|
+
return result
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function addPathToPatch(
|
|
230
|
+
patch: Patch,
|
|
231
|
+
reversedPrefix: readonly PathElement[],
|
|
232
|
+
prefixCount: number
|
|
233
|
+
): Patch {
|
|
234
|
+
const patchPath = patch.path
|
|
235
|
+
const newPath = new Array<PathElement>(prefixCount + patchPath.length)
|
|
236
|
+
for (let i = 0; i < prefixCount; i++) {
|
|
237
|
+
newPath[i] = reversedPrefix[prefixCount - 1 - i]
|
|
238
|
+
}
|
|
239
|
+
for (let j = 0; j < patchPath.length; j++) {
|
|
240
|
+
newPath[prefixCount + j] = patchPath[j]
|
|
241
|
+
}
|
|
160
242
|
return {
|
|
161
243
|
...patch,
|
|
162
|
-
path:
|
|
244
|
+
path: newPath,
|
|
163
245
|
}
|
|
164
246
|
}
|
|
165
247
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tweakArray } from "../tweaker/tweakArray"
|
|
2
2
|
import { isArray } from "../utils"
|
|
3
|
-
import {
|
|
3
|
+
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
4
4
|
import { type FromSnapshotContext, internalFromSnapshot, registerSnapshotter } from "./fromSnapshot"
|
|
5
5
|
import type { SnapshotInOfObject } from "./SnapshotOf"
|
|
6
6
|
import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
|
|
@@ -8,8 +8,17 @@ import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPrio
|
|
|
8
8
|
function fromArraySnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotContext): any[] {
|
|
9
9
|
const ln = sn.length
|
|
10
10
|
const arr: any[] = new Array(ln)
|
|
11
|
+
// Hydration visits every slot. Keep this direct stack use rather than
|
|
12
|
+
// withErrorPathSegment: it avoids one callback closure per item and has a
|
|
13
|
+
// measured production hydration win.
|
|
14
|
+
const errorDiagnosticsContext = getCurrentErrorDiagnosticsContext()
|
|
11
15
|
for (let i = 0; i < ln; i++) {
|
|
12
|
-
|
|
16
|
+
errorDiagnosticsContext?.pushPath(i)
|
|
17
|
+
try {
|
|
18
|
+
arr[i] = internalFromSnapshot(sn[i], ctx)
|
|
19
|
+
} finally {
|
|
20
|
+
errorDiagnosticsContext?.popPath()
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
return tweakArray(arr, undefined, true)
|
|
15
24
|
}
|
|
@@ -5,7 +5,7 @@ import { modelTypeKey } from "../model/metadata"
|
|
|
5
5
|
import { getModelOrSnapshotTypeAndId, getSnapshotModelType } from "../model/utils"
|
|
6
6
|
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
7
7
|
import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
|
|
8
|
-
import {
|
|
8
|
+
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
9
9
|
import { type FromSnapshotContext, registerSnapshotter } from "./fromSnapshot"
|
|
10
10
|
import type { SnapshotInOfModel } from "./SnapshotOf"
|
|
11
11
|
import { SnapshotProcessingError } from "./SnapshotProcessingError"
|
|
@@ -40,7 +40,12 @@ function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotCon
|
|
|
40
40
|
const modelTypeAndId = getModelOrSnapshotTypeAndId(sn)
|
|
41
41
|
const modelId = modelTypeAndId?.modelId
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// Hydration constructs every model. Keep this direct stack use rather than
|
|
44
|
+
// withErrorModelTrailEntry: it avoids one callback closure per model and
|
|
45
|
+
// has a measured production hydration win.
|
|
46
|
+
const errorDiagnosticsContext = getCurrentErrorDiagnosticsContext()
|
|
47
|
+
errorDiagnosticsContext?.pushModelTrail(type, modelId)
|
|
48
|
+
try {
|
|
44
49
|
return new (modelInfo.class as any)(undefined, {
|
|
45
50
|
snapshotInitialData: {
|
|
46
51
|
unprocessedSnapshot: sn,
|
|
@@ -54,7 +59,9 @@ function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotCon
|
|
|
54
59
|
},
|
|
55
60
|
generateNewIds: ctx.options.generateNewIds,
|
|
56
61
|
} satisfies ModelConstructorOptions)
|
|
57
|
-
}
|
|
62
|
+
} finally {
|
|
63
|
+
errorDiagnosticsContext?.popModelTrail()
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|