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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/dist/mobx-keystone.esm.js +2561 -1214
- package/dist/mobx-keystone.esm.mjs +2561 -1214
- package/dist/mobx-keystone.umd.js +2566 -1220
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
- package/dist/types/modelShared/modelDecorator.d.ts +5 -2
- package/dist/types/modelShared/prop.d.ts +42 -29
- 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/Ref.d.ts +1 -4
- 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/SnapshotOf.d.ts +4 -4
- package/dist/types/snapshot/applySnapshot.d.ts +2 -1
- package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
- package/dist/types/snapshot/getSnapshot.d.ts +1 -1
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/schemas.d.ts +41 -37
- package/dist/types/types/tProp.d.ts +12 -6
- package/dist/types/types/types.d.ts +6 -6
- package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
- package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
- package/dist/types/utils/ModelPool.d.ts +7 -2
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/types.d.ts +6 -0
- package/dist/types/wrappers/ArraySet.d.ts +1 -0
- package/package.json +5 -3
- package/src/action/modelFlow.ts +35 -66
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
- package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
- package/src/actionMiddlewares/undoMiddleware.ts +63 -35
- package/src/context/context.ts +15 -36
- package/src/dataModel/utils.ts +5 -28
- package/src/deepChange/onDeepChange.ts +44 -126
- package/src/model/BaseModel.ts +7 -5
- package/src/model/utils.ts +5 -10
- package/src/modelShared/modelDecorator.ts +16 -30
- package/src/modelShared/prop.ts +115 -111
- package/src/modelShared/sharedInternalModel.ts +2 -2
- package/src/parent/arrayPaths.ts +239 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/onChildAttachedTo.ts +130 -44
- package/src/parent/path.ts +10 -12
- 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/patch/patchRecorder.ts +13 -28
- package/src/ref/core.ts +69 -46
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/rootStore.ts +1 -3
- package/src/snapshot/SnapshotOf.ts +24 -48
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +12 -9
- package/src/snapshot/getSnapshot.ts +1 -1
- package/src/snapshot/internal.ts +67 -12
- package/src/snapshot/reconcileModelSnapshot.ts +5 -3
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
- package/src/treeUtils/draft.ts +6 -1
- package/src/treeUtils/sandbox.ts +10 -7
- 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 +127 -77
- package/src/tweaker/tweakPlainObject.ts +65 -46
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +5 -104
- package/src/types/arrayBased/typesArray.ts +23 -30
- package/src/types/arrayBased/typesTuple.ts +16 -25
- package/src/types/createCachedTypeCheck.ts +509 -94
- package/src/types/index.ts +1 -0
- package/src/types/objectBased/typesDataModelData.ts +7 -8
- package/src/types/objectBased/typesObject.ts +11 -15
- package/src/types/objectBased/typesRecord.ts +12 -19
- package/src/types/schemas.ts +59 -71
- package/src/types/tProp.ts +29 -29
- package/src/types/utility/typeToStoredData.ts +22 -21
- package/src/types/utility/typesCodecCore.ts +5 -0
- package/src/types/utility/typesCodecSupport.ts +3 -3
- package/src/types/utility/typesMap.ts +43 -51
- package/src/types/utility/typesRefinement.ts +7 -8
- package/src/types/utility/typesSkipCheck.ts +7 -8
- package/src/types/utility/typesTag.ts +7 -8
- package/src/utils/ModelPool.ts +76 -37
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +5 -5
- package/src/utils/index.ts +38 -20
- package/src/utils/types.ts +5 -0
- package/src/wrappers/ArraySet.ts +27 -14
- package/src/wrappers/ObjectMap.ts +1 -6
- package/src/wrappers/asMap.ts +1 -21
- package/src/wrappers/asSet.ts +0 -8
|
@@ -1,34 +1,102 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
createAtom,
|
|
4
|
+
type IArrayDidChange,
|
|
5
|
+
type IComputedValue,
|
|
6
|
+
type IObjectDidChange,
|
|
7
|
+
isObservableObject,
|
|
8
|
+
keys,
|
|
9
|
+
transaction,
|
|
10
|
+
untracked,
|
|
11
|
+
} from "mobx"
|
|
2
12
|
import { dataToModelNode, modelToDataNode } from "../parent/core"
|
|
3
|
-
import { fastGetParent } from "../parent/path"
|
|
4
|
-
import type { Path } from "../parent/pathTypes"
|
|
13
|
+
import { fastGetParent, fastGetParentPath } from "../parent/path"
|
|
14
|
+
import type { Path, PathElement } from "../parent/pathTypes"
|
|
5
15
|
import { isTweakedObject } from "../tweaker/core"
|
|
16
|
+
import {
|
|
17
|
+
addTweakedChangeListener,
|
|
18
|
+
type TweakedChangeListener,
|
|
19
|
+
} from "../tweaker/tweakedChangeListeners"
|
|
20
|
+
import { getMobxVersion } from "../utils"
|
|
21
|
+
import { ChunkedSequence, ChunkedSet, chunkSize, type SequenceChunk } from "../utils/chunks"
|
|
22
|
+
import { getOrCreate } from "../utils/mapUtils"
|
|
6
23
|
import { TypeCheckError } from "./TypeCheckError"
|
|
7
24
|
|
|
8
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Containers whose cached checks may be stale, each with its child on the way
|
|
27
|
+
* to the changed object (undefined for the changed object itself, and null
|
|
28
|
+
* when there are several).
|
|
29
|
+
*/
|
|
30
|
+
let uncachedTypeCheckObjects: Map<object, object | undefined | null> | undefined
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Context-free results of the uncached checks of those containers, by checker.
|
|
34
|
+
* Every typed ancestor of the changed object checks the path to it again, so
|
|
35
|
+
* without them a change would be checked in O(depth²).
|
|
36
|
+
*/
|
|
37
|
+
let uncachedResults: Map<object, Map<object, TypeCheckError | null>> | undefined
|
|
9
38
|
|
|
10
39
|
/** @internal */
|
|
11
40
|
export function withoutCachedTypeChecking(obj: object, fn: () => void): void {
|
|
12
41
|
const previous = uncachedTypeCheckObjects
|
|
13
|
-
const affected = new
|
|
42
|
+
const affected = new Map(previous)
|
|
43
|
+
const add = (node: object, child: object | undefined) => {
|
|
44
|
+
affected.set(node, affected.has(node) && affected.get(node) !== child ? null : child)
|
|
45
|
+
}
|
|
14
46
|
// Only these containers can retain a cached result for the old shape.
|
|
15
47
|
// Model schema checks run on $ data, while refinements can check the model.
|
|
48
|
+
let child: object | undefined
|
|
16
49
|
let current: object | undefined = dataToModelNode(obj)
|
|
17
50
|
while (current) {
|
|
18
|
-
|
|
19
|
-
|
|
51
|
+
add(current, child)
|
|
52
|
+
const data: object = modelToDataNode(current)
|
|
53
|
+
if (data !== current) {
|
|
54
|
+
add(data, child)
|
|
55
|
+
}
|
|
56
|
+
child = current
|
|
20
57
|
current = fastGetParent(current, false)
|
|
21
58
|
}
|
|
59
|
+
const previousResults = uncachedResults
|
|
22
60
|
uncachedTypeCheckObjects = affected
|
|
61
|
+
uncachedResults = new Map()
|
|
23
62
|
try {
|
|
24
63
|
fn()
|
|
25
64
|
} finally {
|
|
26
65
|
uncachedTypeCheckObjects = previous
|
|
66
|
+
uncachedResults = previousResults
|
|
67
|
+
// the tree changed again, so the outer results may be stale
|
|
68
|
+
previousResults?.clear()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Runs the uncached, context-free check of a container once per
|
|
74
|
+
* `withoutCachedTypeChecking` call, and contextualizes its result.
|
|
75
|
+
*/
|
|
76
|
+
function checkUncached(
|
|
77
|
+
checker: object,
|
|
78
|
+
value: object,
|
|
79
|
+
path: Path,
|
|
80
|
+
typeCheckedValue: any,
|
|
81
|
+
check: () => TypeCheckError | null
|
|
82
|
+
): TypeCheckError | null {
|
|
83
|
+
const results = getOrCreate(uncachedResults!, checker, () => new Map())
|
|
84
|
+
let error = results.get(value)
|
|
85
|
+
if (error === undefined) {
|
|
86
|
+
error = check()
|
|
87
|
+
results.set(value, error)
|
|
27
88
|
}
|
|
89
|
+
return contextualizeCachedError(error, path, typeCheckedValue)
|
|
28
90
|
}
|
|
29
91
|
|
|
30
92
|
const emptyPath: Path = []
|
|
31
93
|
|
|
94
|
+
/** Key or index of a child in its parent, or undefined when it is not its child. */
|
|
95
|
+
function getChildKey(parent: object, child: object): PathElement | undefined {
|
|
96
|
+
const parentPath = fastGetParentPath(child, false)
|
|
97
|
+
return parentPath?.parent === parent ? parentPath.path : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
32
100
|
function contextualizeCachedError(
|
|
33
101
|
error: TypeCheckError | null,
|
|
34
102
|
path: Path,
|
|
@@ -49,11 +117,6 @@ function contextualizeCachedError(
|
|
|
49
117
|
|
|
50
118
|
type CachedEntry = IComputedValue<TypeCheckError | null>
|
|
51
119
|
|
|
52
|
-
interface EntryCache<K> {
|
|
53
|
-
get(key: K): CachedEntry | undefined
|
|
54
|
-
set(key: K, entry: CachedEntry): void
|
|
55
|
-
}
|
|
56
|
-
|
|
57
120
|
type IterateEntries<K> = (
|
|
58
121
|
value: any,
|
|
59
122
|
checkEntry: (entryKey: K) => TypeCheckError | null
|
|
@@ -66,118 +129,435 @@ type CheckEntry<K> = (
|
|
|
66
129
|
typeCheckedValue: any
|
|
67
130
|
) => TypeCheckError | null
|
|
68
131
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Creates one reactive computed for a complete container check.
|
|
134
|
+
*
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
export function createWholeContainerCachedCheck(
|
|
138
|
+
check: (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null
|
|
73
139
|
): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const checkAllEntries = (value: any, path: Path, typeCheckedValue: any) =>
|
|
77
|
-
iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
|
|
140
|
+
const cache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
|
|
78
141
|
|
|
79
142
|
return (value, path, typeCheckedValue) => {
|
|
80
|
-
if (
|
|
81
|
-
return
|
|
143
|
+
if (!isTweakedObject(value, true)) {
|
|
144
|
+
return check(value, path, typeCheckedValue)
|
|
145
|
+
}
|
|
146
|
+
if (uncachedTypeCheckObjects?.has(value)) {
|
|
147
|
+
return checkUncached(cache, value, path, typeCheckedValue, () =>
|
|
148
|
+
check(value, emptyPath, undefined)
|
|
149
|
+
)
|
|
82
150
|
}
|
|
83
151
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
152
|
+
const cached = getOrCreate(cache, value, () =>
|
|
153
|
+
computed(() => check(value, emptyPath, undefined), { keepAlive: true })
|
|
154
|
+
)
|
|
155
|
+
const error = cached.get()
|
|
156
|
+
return contextualizeCachedError(error, path, typeCheckedValue)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// MobX 6+ batches while tracking a derivation. MobX < 6 does not, and computes
|
|
161
|
+
// unobserved computeds read outside a batch without caching them, which would
|
|
162
|
+
// check new chunks twice.
|
|
163
|
+
const needsBatchToCacheChunks = getMobxVersion() < 6
|
|
164
|
+
|
|
165
|
+
function checkChunks<T>(fn: () => T): T {
|
|
166
|
+
return needsBatchToCacheChunks ? transaction(fn) : fn()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Checks the items of an array, starting at `firstIndex`, and returns the first
|
|
171
|
+
* error, or null.
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export type CheckArrayItems = (
|
|
176
|
+
items: ArrayLike<unknown>,
|
|
177
|
+
firstIndex: number,
|
|
178
|
+
path: Path,
|
|
179
|
+
typeCheckedValue: any
|
|
180
|
+
) => TypeCheckError | null
|
|
181
|
+
|
|
182
|
+
class ArrayChunk {
|
|
183
|
+
items: unknown[]
|
|
184
|
+
/** Changed when the items change. */
|
|
185
|
+
readonly atom = createAtom("arrayTypeCheckChunk")
|
|
186
|
+
readonly check: IComputedValue<TypeCheckError | null>
|
|
187
|
+
|
|
188
|
+
constructor(items: unknown[], checkItems: CheckArrayItems) {
|
|
189
|
+
this.items = items
|
|
190
|
+
// error indexes are relative to the chunk, so moving it keeps its result
|
|
191
|
+
this.check = computed(() => {
|
|
192
|
+
this.atom.reportObserved()
|
|
193
|
+
return checkItems(this.items, 0, emptyPath, undefined)
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The items of an array split in chunks, kept up to date through the array
|
|
200
|
+
* changes.
|
|
201
|
+
*/
|
|
202
|
+
class ArrayChunks
|
|
203
|
+
extends ChunkedSequence<unknown, ArrayChunk>
|
|
204
|
+
implements TweakedChangeListener<IArrayDidChange>
|
|
205
|
+
{
|
|
206
|
+
/** Changed when chunks are resized, since error indexes depend on their sizes. */
|
|
207
|
+
readonly atom = createAtom("arrayTypeCheckChunks")
|
|
208
|
+
private readonly array: readonly unknown[]
|
|
209
|
+
private readonly checkItems: CheckArrayItems
|
|
210
|
+
|
|
211
|
+
constructor(array: readonly unknown[], checkItems: CheckArrayItems) {
|
|
212
|
+
super()
|
|
213
|
+
this.array = array
|
|
214
|
+
this.checkItems = checkItems
|
|
215
|
+
this.appendChunks(untracked(() => array.slice()))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
resync() {
|
|
219
|
+
this.reset(untracked(() => this.array.slice()))
|
|
220
|
+
this.atom.reportChanged()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns the first error. When given, the chunk with the item at
|
|
225
|
+
* `uncachedIndex` is checked without using its cached result.
|
|
226
|
+
*/
|
|
227
|
+
check(uncachedIndex?: number): TypeCheckError | null {
|
|
228
|
+
let uncached = -1
|
|
229
|
+
if (uncachedIndex === undefined) {
|
|
230
|
+
this.atom.reportObserved()
|
|
231
|
+
} else {
|
|
232
|
+
uncached = this.locate(uncachedIndex)[0]
|
|
233
|
+
}
|
|
234
|
+
let offset = 0
|
|
235
|
+
for (let c = 0; c < this.chunks.length; c++) {
|
|
236
|
+
const chunk = this.chunks[c]
|
|
237
|
+
const error =
|
|
238
|
+
c === uncached ? this.checkItems(chunk.items, 0, emptyPath, undefined) : chunk.check.get()
|
|
239
|
+
if (error) {
|
|
240
|
+
return offset === 0 ? error : offsetErrorIndex(error, offset)
|
|
241
|
+
}
|
|
242
|
+
offset += chunk.items.length
|
|
243
|
+
}
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
applyChange(change: IArrayDidChange) {
|
|
248
|
+
if (change.type === "update") {
|
|
249
|
+
const [c, offset] = this.locate(change.index)
|
|
250
|
+
const chunk = this.chunks[c]
|
|
251
|
+
chunk.items[offset] = change.newValue
|
|
252
|
+
chunk.atom.reportChanged()
|
|
253
|
+
} else {
|
|
254
|
+
this.splice(change.index, change.removedCount, change.added)
|
|
255
|
+
this.atom.reportChanged()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected createChunk(items: unknown[]): ArrayChunk {
|
|
260
|
+
return new ArrayChunk(items, this.checkItems)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
protected onSpliced(chunk: ArrayChunk): void {
|
|
264
|
+
chunk.atom.reportChanged()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
protected onItemsMoved(chunk: ArrayChunk): void {
|
|
268
|
+
chunk.atom.reportChanged()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function offsetErrorIndex(error: TypeCheckError, offset: number): TypeCheckError {
|
|
273
|
+
return new TypeCheckError({
|
|
274
|
+
path: [(error.path[0] as number) + offset, ...error.path.slice(1)],
|
|
275
|
+
modelTrail: error.modelTrail,
|
|
276
|
+
expectedTypeName: error.expectedTypeName,
|
|
277
|
+
actualValue: error.actualValue,
|
|
278
|
+
typeCheckedValue: error.typeCheckedValue,
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class RecordChunk implements SequenceChunk<string> {
|
|
283
|
+
items: string[]
|
|
284
|
+
/** Changed when its keys change. */
|
|
285
|
+
readonly atom = createAtom("recordTypeCheckChunk")
|
|
286
|
+
readonly check: IComputedValue<TypeCheckError | null>
|
|
287
|
+
|
|
288
|
+
constructor(
|
|
289
|
+
keys: string[],
|
|
290
|
+
checkKey: (key: string) => TypeCheckError | null,
|
|
291
|
+
getEntry: (key: string) => CachedEntry
|
|
292
|
+
) {
|
|
293
|
+
this.items = keys
|
|
294
|
+
let isFirstEvaluation = true
|
|
295
|
+
const checkKeys = (): TypeCheckError | null => {
|
|
296
|
+
this.atom.reportObserved()
|
|
297
|
+
const keys = this.items
|
|
298
|
+
// the first check does not allocate per-entry state, and later ones only
|
|
299
|
+
// re-check the entries that changed
|
|
300
|
+
const check = isFirstEvaluation ? checkKey : (key: string) => getEntry(key).get()
|
|
301
|
+
isFirstEvaluation = false
|
|
302
|
+
for (let i = 0; i < keys.length; i++) {
|
|
303
|
+
const error = check(keys[i])
|
|
304
|
+
if (error) {
|
|
305
|
+
return error
|
|
91
306
|
}
|
|
307
|
+
}
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
// (the entries would not be cached otherwise when MobX < 6 checks whether
|
|
311
|
+
// this chunk changed outside a batch)
|
|
312
|
+
this.check = computed(needsBatchToCacheChunks ? () => transaction(checkKeys) : checkKeys)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
92
315
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
316
|
+
/**
|
|
317
|
+
* The keys of a record split in chunks, kept up to date through the record
|
|
318
|
+
* changes.
|
|
319
|
+
*/
|
|
320
|
+
class RecordChunks
|
|
321
|
+
extends ChunkedSet<string, RecordChunk>
|
|
322
|
+
implements TweakedChangeListener<IObjectDidChange>
|
|
323
|
+
{
|
|
324
|
+
/** Check of each entry, created once its chunk is checked again. */
|
|
325
|
+
private readonly entries = new Map<string, CachedEntry>()
|
|
326
|
+
/** Changed when chunks are added or removed. */
|
|
327
|
+
readonly atom = createAtom("recordTypeCheckChunks")
|
|
328
|
+
private readonly record: object
|
|
329
|
+
private readonly checkKey: (key: string) => TypeCheckError | null
|
|
330
|
+
private readonly getEntry = (key: string): CachedEntry =>
|
|
331
|
+
getOrCreate(this.entries, key, () => computed(() => this.checkKey(key)))
|
|
332
|
+
|
|
333
|
+
constructor(record: object, checkKey: (key: string) => TypeCheckError | null) {
|
|
334
|
+
super()
|
|
335
|
+
this.record = record
|
|
336
|
+
this.checkKey = checkKey
|
|
337
|
+
this.appendChunks(untracked(() => keys(record) as string[]))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
resync() {
|
|
341
|
+
this.entries.clear()
|
|
342
|
+
this.reset(untracked(() => keys(this.record) as string[]))
|
|
343
|
+
this.atom.reportChanged()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Whether any entry has an error. When given, the entry of `uncachedKey` is
|
|
348
|
+
* checked without using its cached result.
|
|
349
|
+
*/
|
|
350
|
+
hasError(uncachedKey?: string): boolean {
|
|
351
|
+
let uncached: RecordChunk | undefined
|
|
352
|
+
if (uncachedKey === undefined) {
|
|
353
|
+
this.atom.reportObserved()
|
|
354
|
+
} else {
|
|
355
|
+
uncached = this.getChunkOf(uncachedKey)
|
|
356
|
+
}
|
|
357
|
+
for (const chunk of this.chunks) {
|
|
358
|
+
if (chunk !== uncached) {
|
|
359
|
+
if (chunk.check.get()) {
|
|
360
|
+
return true
|
|
97
361
|
}
|
|
98
|
-
|
|
362
|
+
continue
|
|
99
363
|
}
|
|
364
|
+
for (const k of chunk.items) {
|
|
365
|
+
if (k === uncachedKey ? this.checkKey(k) : this.getEntry(k).get()) {
|
|
366
|
+
return true
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
100
372
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return checkAllEntries(value, emptyPath, undefined)
|
|
106
|
-
}
|
|
107
|
-
return iterateEntries(value, cachedCheckEntry)
|
|
108
|
-
},
|
|
109
|
-
{ keepAlive: true }
|
|
110
|
-
)
|
|
111
|
-
aggregationCache.set(value, aggregation)
|
|
373
|
+
applyChange(change: IObjectDidChange) {
|
|
374
|
+
const key = change.name
|
|
375
|
+
if (typeof key !== "string") {
|
|
376
|
+
return
|
|
112
377
|
}
|
|
378
|
+
// value updates invalidate the chunk reading them
|
|
379
|
+
if (change.type === "add") {
|
|
380
|
+
this.add(key)
|
|
381
|
+
} else if (change.type === "remove") {
|
|
382
|
+
this.entries.delete(key)
|
|
383
|
+
this.delete(key)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
113
386
|
|
|
114
|
-
|
|
387
|
+
protected createChunk(keys: string[]) {
|
|
388
|
+
return new RecordChunk(keys, this.checkKey, this.getEntry)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
protected onChunkChanged(chunk: RecordChunk): void {
|
|
392
|
+
chunk.atom.reportChanged()
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected onChunksChanged(): void {
|
|
396
|
+
this.atom.reportChanged()
|
|
115
397
|
}
|
|
116
398
|
}
|
|
117
399
|
|
|
118
400
|
/**
|
|
119
|
-
*
|
|
401
|
+
* Cached check for the values of a record.
|
|
120
402
|
*
|
|
121
|
-
*
|
|
403
|
+
* A single computed over every entry would make MobX revalidate every entry on
|
|
404
|
+
* each change, so the keys are split in chunks, each with its own computed, kept
|
|
405
|
+
* up to date from the record changes. A change then only revalidates one chunk
|
|
406
|
+
* and the chunk results. Once a chunk is invalidated each of its entries gets
|
|
407
|
+
* its own computed, so a value change only re-checks that value.
|
|
122
408
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* 2. After the first invalidation, per-entry computeds each read one property, so
|
|
126
|
-
* changing property A only invalidates property A's computed.
|
|
127
|
-
* 3. The aggregation computed iterates per-entry computeds. Thanks to MobX's propagation,
|
|
128
|
-
* if a per-entry computed re-evaluates but returns the same result (e.g. null → null),
|
|
129
|
-
* the aggregation body is skipped entirely.
|
|
130
|
-
* 4. A pruning observer is installed when the promoted entry cache is first used
|
|
131
|
-
* and removes stale entries immediately when record keys are deleted.
|
|
409
|
+
* Chunks are not in the order the keys are iterated in, so when a chunk has an
|
|
410
|
+
* error the first error is found by checking every entry again in order.
|
|
132
411
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
412
|
+
* The chunks learn about added and removed keys before the checks after that
|
|
413
|
+
* change run, and read values rather than keys, so unlike other cached checks
|
|
414
|
+
* this one stays valid while MobX has yet to invalidate the readers of the
|
|
415
|
+
* record keys (see `withoutCachedTypeChecking`). When the keys of an object
|
|
416
|
+
* below an entry change instead, only the check of that entry is stale, so
|
|
417
|
+
* only it is checked again without its cache.
|
|
135
418
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
419
|
+
* Cached checks produce context-free errors, which are contextualized with the
|
|
420
|
+
* current path and checked root without running the checks again. Values that
|
|
421
|
+
* are not tweaked (plain snapshots) are checked directly.
|
|
138
422
|
*
|
|
139
|
-
* @
|
|
140
|
-
*
|
|
141
|
-
* @param
|
|
142
|
-
*
|
|
423
|
+
* @internal
|
|
424
|
+
*
|
|
425
|
+
* @param iterateEntries Iterates the keys in order and returns the first error, or null.
|
|
426
|
+
* @param checkEntry Checks a single entry.
|
|
143
427
|
*/
|
|
144
|
-
export function
|
|
428
|
+
export function createChunkedRecordCachedCheck(
|
|
145
429
|
iterateEntries: IterateEntries<PropertyKey>,
|
|
146
430
|
checkEntry: CheckEntry<PropertyKey>
|
|
147
431
|
): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
432
|
+
const cache = new WeakMap<
|
|
433
|
+
object,
|
|
434
|
+
{ readonly chunks: RecordChunks; readonly check: IComputedValue<TypeCheckError | null> }
|
|
435
|
+
>()
|
|
436
|
+
|
|
437
|
+
const checkAllEntries = (value: any, path: Path, typeCheckedValue: any) =>
|
|
438
|
+
iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
|
|
439
|
+
|
|
440
|
+
return (value, path, typeCheckedValue) => {
|
|
441
|
+
// only tweaked plain objects notify the chunks of their key changes
|
|
442
|
+
if (!isTweakedObject(value, true) || !isObservableObject(value)) {
|
|
443
|
+
return checkAllEntries(value, path, typeCheckedValue)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let cached = cache.get(value)
|
|
447
|
+
const uncachedChild = uncachedTypeCheckObjects?.get(value)
|
|
448
|
+
if (uncachedChild !== undefined) {
|
|
449
|
+
// an entry (or something below it) has a stale cached check
|
|
450
|
+
const key = uncachedChild && getChildKey(value, uncachedChild)
|
|
451
|
+
const chunks = cached?.chunks
|
|
452
|
+
return checkUncached(cache, value, path, typeCheckedValue, () => {
|
|
453
|
+
if (chunks && typeof key === "string") {
|
|
454
|
+
if (!checkChunks(() => chunks.hasError(key))) {
|
|
455
|
+
return null
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return checkAllEntries(value, emptyPath, undefined)
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!cached) {
|
|
463
|
+
// keys already include a change being notified
|
|
464
|
+
const chunks = new RecordChunks(value, (key) => checkEntry(value, key, emptyPath, undefined))
|
|
465
|
+
addTweakedChangeListener<object>(value, chunks)
|
|
466
|
+
const check = computed(
|
|
467
|
+
() => {
|
|
468
|
+
// any added or removed key changes a chunk, so this runs again then
|
|
469
|
+
return checkChunks(() => chunks.hasError())
|
|
470
|
+
? checkAllEntries(value, emptyPath, undefined)
|
|
471
|
+
: null
|
|
472
|
+
},
|
|
473
|
+
{ keepAlive: true }
|
|
474
|
+
)
|
|
475
|
+
cached = { chunks, check }
|
|
476
|
+
cache.set(value, cached)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return contextualizeCachedError(cached.check.get(), path, typeCheckedValue)
|
|
480
|
+
}
|
|
157
481
|
}
|
|
158
482
|
|
|
159
483
|
/**
|
|
160
|
-
*
|
|
484
|
+
* Cached check for the items of an array.
|
|
485
|
+
*
|
|
486
|
+
* MobX invalidates array reads at collection granularity, so one computed per
|
|
487
|
+
* item does not help. A single computed over all the items is O(n) on every
|
|
488
|
+
* change though: a push re-reads every item check, and a change inside one item
|
|
489
|
+
* makes MobX revalidate every item check the computed depends on.
|
|
490
|
+
*
|
|
491
|
+
* Instead, once an array grows large its items are split into chunks, each with
|
|
492
|
+
* its own computed over a plain copy of its items, which are kept up to date
|
|
493
|
+
* from the array changes rather than by reading the array. An item change
|
|
494
|
+
* revalidates one chunk, and a splice re-checks only the chunks it touched.
|
|
495
|
+
* Chunk errors use indexes relative to their chunk, so inserting items does not
|
|
496
|
+
* invalidate the chunks after it.
|
|
497
|
+
*
|
|
498
|
+
* When the keys of an object below an item change, only the chunk of that item
|
|
499
|
+
* is checked again without its cache (see `withoutCachedTypeChecking`).
|
|
161
500
|
*
|
|
162
501
|
* @internal
|
|
163
502
|
*/
|
|
164
|
-
export function
|
|
165
|
-
|
|
503
|
+
export function createChunkedArrayCachedCheck(
|
|
504
|
+
checkItems: CheckArrayItems
|
|
166
505
|
): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
|
|
167
|
-
const cache = new WeakMap<
|
|
506
|
+
const cache = new WeakMap<
|
|
507
|
+
object,
|
|
508
|
+
{ chunks: ArrayChunks | undefined; readonly check: IComputedValue<TypeCheckError | null> }
|
|
509
|
+
>()
|
|
168
510
|
|
|
169
511
|
return (value, path, typeCheckedValue) => {
|
|
170
|
-
|
|
171
|
-
|
|
512
|
+
const array: readonly unknown[] = value
|
|
513
|
+
if (!isTweakedObject(array, true)) {
|
|
514
|
+
return checkItems(array, 0, path, typeCheckedValue)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let cached = cache.get(array)
|
|
518
|
+
const uncachedChild = uncachedTypeCheckObjects?.get(array)
|
|
519
|
+
if (uncachedChild !== undefined) {
|
|
520
|
+
// an item (or something below it) has a stale cached check
|
|
521
|
+
const index = uncachedChild && getChildKey(array, uncachedChild)
|
|
522
|
+
const chunks = cached?.chunks
|
|
523
|
+
return checkUncached(cache, array, path, typeCheckedValue, () => {
|
|
524
|
+
if (!chunks || typeof index !== "number") {
|
|
525
|
+
return checkItems(array, 0, emptyPath, undefined)
|
|
526
|
+
}
|
|
527
|
+
return checkChunks(() => chunks.check(index))
|
|
528
|
+
})
|
|
172
529
|
}
|
|
173
530
|
|
|
174
|
-
let cached = cache.get(value)
|
|
175
531
|
if (!cached) {
|
|
176
|
-
|
|
177
|
-
|
|
532
|
+
const state: {
|
|
533
|
+
chunks: ArrayChunks | undefined
|
|
534
|
+
readonly check: IComputedValue<TypeCheckError | null>
|
|
535
|
+
} = {
|
|
536
|
+
chunks: undefined,
|
|
537
|
+
check: computed(
|
|
538
|
+
() => {
|
|
539
|
+
if (!state.chunks) {
|
|
540
|
+
if (array.length <= chunkSize) {
|
|
541
|
+
return checkItems(array, 0, emptyPath, undefined)
|
|
542
|
+
}
|
|
543
|
+
// from now on the chunks follow the array changes, so the array
|
|
544
|
+
// is no longer read
|
|
545
|
+
const newChunks = new ArrayChunks(array, checkItems)
|
|
546
|
+
addTweakedChangeListener(array, newChunks)
|
|
547
|
+
state.chunks = newChunks
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const chunks = state.chunks
|
|
551
|
+
return checkChunks(() => chunks.check())
|
|
552
|
+
},
|
|
553
|
+
{ keepAlive: true }
|
|
554
|
+
),
|
|
555
|
+
}
|
|
556
|
+
cached = state
|
|
557
|
+
cache.set(array, cached)
|
|
178
558
|
}
|
|
179
|
-
|
|
180
|
-
return contextualizeCachedError(
|
|
559
|
+
|
|
560
|
+
return contextualizeCachedError(cached.check.get(), path, typeCheckedValue)
|
|
181
561
|
}
|
|
182
562
|
}
|
|
183
563
|
|
|
@@ -194,13 +574,48 @@ export function createIndexedPerEntryCachedCheck(
|
|
|
194
574
|
iterateEntries: IterateEntries<number>,
|
|
195
575
|
checkEntry: CheckEntry<number>
|
|
196
576
|
): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
577
|
+
const aggregationCache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
|
|
578
|
+
|
|
579
|
+
const checkAllEntries = (value: any, path: Path, typeCheckedValue: any) =>
|
|
580
|
+
iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
|
|
581
|
+
|
|
582
|
+
return (value, path, typeCheckedValue) => {
|
|
583
|
+
if (!isTweakedObject(value, true)) {
|
|
584
|
+
return checkAllEntries(value, path, typeCheckedValue)
|
|
204
585
|
}
|
|
205
|
-
|
|
586
|
+
if (uncachedTypeCheckObjects?.has(value)) {
|
|
587
|
+
return checkUncached(aggregationCache, value, path, typeCheckedValue, () =>
|
|
588
|
+
checkAllEntries(value, emptyPath, undefined)
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let aggregation = aggregationCache.get(value)
|
|
593
|
+
if (!aggregation) {
|
|
594
|
+
let entries: Array<CachedEntry | undefined> | undefined
|
|
595
|
+
let isFirstEvaluation = true
|
|
596
|
+
const cachedCheckEntry = (index: number): TypeCheckError | null => {
|
|
597
|
+
entries ??= []
|
|
598
|
+
let entry = entries[index]
|
|
599
|
+
if (!entry) {
|
|
600
|
+
entry = computed(() => checkEntry(value, index, emptyPath, undefined))
|
|
601
|
+
entries[index] = entry
|
|
602
|
+
}
|
|
603
|
+
return entry.get()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
aggregation = computed(
|
|
607
|
+
() => {
|
|
608
|
+
if (isFirstEvaluation) {
|
|
609
|
+
isFirstEvaluation = false
|
|
610
|
+
return checkAllEntries(value, emptyPath, undefined)
|
|
611
|
+
}
|
|
612
|
+
return iterateEntries(value, cachedCheckEntry)
|
|
613
|
+
},
|
|
614
|
+
{ keepAlive: true }
|
|
615
|
+
)
|
|
616
|
+
aggregationCache.set(value, aggregation)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return contextualizeCachedError(aggregation.get(), path, typeCheckedValue)
|
|
620
|
+
}
|
|
206
621
|
}
|