mobx-keystone 1.22.0 → 1.24.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 (77) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +1 -1
  4. package/dist/mobx-keystone.esm.js +7198 -6242
  5. package/dist/mobx-keystone.esm.mjs +7198 -6242
  6. package/dist/mobx-keystone.umd.js +7217 -6243
  7. package/dist/types/action/runUnprotected.d.ts +8 -4
  8. package/dist/types/model/Model.d.ts +5 -0
  9. package/dist/types/modelShared/modelDecorator.d.ts +1 -1
  10. package/dist/types/modelShared/prop.d.ts +8 -4
  11. package/dist/types/snapshot/modelInitialData.d.ts +1 -0
  12. package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
  13. package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
  14. package/dist/types/utils/index.d.ts +24 -0
  15. package/package.json +15 -11
  16. package/src/action/middleware.ts +5 -18
  17. package/src/action/mutationBatch.ts +72 -0
  18. package/src/action/runUnprotected.ts +21 -8
  19. package/src/action/wrapInAction.ts +45 -15
  20. package/src/actionMiddlewares/undoMiddleware.ts +19 -13
  21. package/src/dataModel/BaseDataModel.ts +4 -8
  22. package/src/deepChange/onDeepChange.ts +246 -16
  23. package/src/model/BaseModel.ts +4 -12
  24. package/src/model/Model.ts +5 -0
  25. package/src/model/newModel.ts +68 -36
  26. package/src/modelShared/modelDecorator.ts +64 -53
  27. package/src/modelShared/prop.ts +105 -63
  28. package/src/modelShared/sharedInternalModel.ts +91 -60
  29. package/src/parent/core.ts +16 -10
  30. package/src/parent/coreObjectChildren.ts +219 -21
  31. package/src/parent/path.ts +6 -5
  32. package/src/parent/setParent.ts +5 -5
  33. package/src/patch/emitPatch.ts +102 -20
  34. package/src/ref/Ref.ts +4 -5
  35. package/src/ref/core.ts +2 -2
  36. package/src/snapshot/fromArraySnapshot.ts +11 -2
  37. package/src/snapshot/fromModelSnapshot.ts +10 -3
  38. package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
  39. package/src/snapshot/fromSnapshot.ts +31 -9
  40. package/src/snapshot/getSnapshot.ts +8 -3
  41. package/src/snapshot/internal.ts +243 -82
  42. package/src/snapshot/modelInitialData.ts +41 -0
  43. package/src/snapshot/reconcileArraySnapshot.ts +9 -3
  44. package/src/snapshot/reconcileModelSnapshot.ts +97 -17
  45. package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
  46. package/src/treeUtils/draft.ts +7 -8
  47. package/src/tweaker/core.ts +30 -5
  48. package/src/tweaker/treeNodeMetadata.ts +38 -0
  49. package/src/tweaker/tweak.ts +15 -4
  50. package/src/tweaker/tweakArray.ts +159 -121
  51. package/src/tweaker/tweakFrozen.ts +2 -2
  52. package/src/tweaker/tweakModel.ts +2 -2
  53. package/src/tweaker/tweakPlainObject.ts +117 -124
  54. package/src/tweaker/typeChecking.ts +10 -1
  55. package/src/types/TypeChecker.ts +261 -15
  56. package/src/types/arrayBased/typesArray.ts +38 -40
  57. package/src/types/arrayBased/typesTuple.ts +51 -34
  58. package/src/types/createCachedTypeCheck.ts +181 -0
  59. package/src/types/objectBased/typesArraySet.ts +42 -32
  60. package/src/types/objectBased/typesDataModelData.ts +18 -11
  61. package/src/types/objectBased/typesModel.ts +1 -1
  62. package/src/types/objectBased/typesObject.ts +57 -28
  63. package/src/types/objectBased/typesObjectMap.ts +51 -40
  64. package/src/types/objectBased/typesRecord.ts +59 -39
  65. package/src/types/objectBased/typesRef.ts +1 -1
  66. package/src/types/primitiveBased/typesPrimitive.ts +9 -9
  67. package/src/types/tProp.ts +80 -44
  68. package/src/types/utility/typesOr.ts +35 -24
  69. package/src/types/utility/typesRefinement.ts +21 -8
  70. package/src/types/utility/typesSkipCheck.ts +21 -8
  71. package/src/types/utility/typesTag.ts +21 -9
  72. package/src/types/utility/typesUnchecked.ts +2 -3
  73. package/src/utils/ModelPool.ts +34 -19
  74. package/src/utils/errorDiagnostics.ts +60 -18
  75. package/src/utils/index.ts +142 -9
  76. package/src/types/createPerEntryCachedCheck.ts +0 -166
  77. /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
@@ -6,6 +6,195 @@ import type { TypeCheckError } from "./TypeCheckError"
6
6
 
7
7
  type CheckFunction = (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null
8
8
 
9
+ /** @internal */
10
+ export type SnapshotProcessor = (snapshot: any) => unknown
11
+
12
+ /**
13
+ * A lazily discovered snapshot-processor node. Dependencies are deliberately
14
+ * TypeCheckers rather than processors so recursive graphs can be discovered
15
+ * before any processor body is compiled.
16
+ *
17
+ * @internal
18
+ */
19
+ export interface SnapshotProcessorPlan {
20
+ readonly intrinsic?: boolean
21
+ readonly getDependencies: () => ReadonlyArray<TypeChecker>
22
+ readonly compile: (
23
+ dependencyProcessors: ReadonlyArray<SnapshotProcessor | undefined>
24
+ ) => SnapshotProcessor | undefined
25
+ }
26
+
27
+ /** @internal */
28
+ export function snapshotProcessorPlan(
29
+ getDependencies: () => ReadonlyArray<TypeChecker>,
30
+ compile: SnapshotProcessorPlan["compile"],
31
+ intrinsic = false
32
+ ): SnapshotProcessorPlan {
33
+ return { intrinsic, getDependencies, compile }
34
+ }
35
+
36
+ type SnapshotProcessorSource = SnapshotProcessor | SnapshotProcessorPlan | undefined
37
+ type SnapshotProcessorDirection = "from" | "to"
38
+
39
+ /** @internal */
40
+ export class SnapshotProcessorPlanNode {
41
+ private readonly owner: TypeChecker | undefined
42
+ private readonly direction: SnapshotProcessorDirection
43
+ private readonly source: SnapshotProcessorSource
44
+ private presence: boolean | undefined
45
+ private dependencies: SnapshotProcessorPlanNode[] | undefined
46
+ private discovering = false
47
+ private compiling = false
48
+ private compiled = false
49
+ private compiledProcessor: SnapshotProcessor | undefined
50
+ private forwardingProcessor: SnapshotProcessor | undefined
51
+
52
+ constructor(
53
+ owner: TypeChecker | undefined,
54
+ direction: SnapshotProcessorDirection,
55
+ source: SnapshotProcessorSource
56
+ ) {
57
+ this.owner = owner
58
+ this.direction = direction
59
+ this.source = source
60
+ if (typeof source === "function" || source === undefined) {
61
+ this.presence = !!source
62
+ this.compiled = true
63
+ this.compiledProcessor = source
64
+ }
65
+ }
66
+
67
+ resolve(): SnapshotProcessor | undefined {
68
+ if (this.compiled) {
69
+ return this.compiledProcessor
70
+ }
71
+
72
+ const discoveredNodes: SnapshotProcessorPlanNode[] = []
73
+ const dependents = new Map<SnapshotProcessorPlanNode, SnapshotProcessorPlanNode[]>()
74
+ this.discover(discoveredNodes, dependents, new Set())
75
+
76
+ // Presence is the least fixed point of
77
+ // intrinsic || any(dependency is present)
78
+ // over the newly discovered graph. An identity-only recursive component
79
+ // therefore remains absent, while an intrinsic processor propagates
80
+ // through every parent that contains it.
81
+ const presentQueue: SnapshotProcessorPlanNode[] = []
82
+ for (let i = 0; i < discoveredNodes.length; i++) {
83
+ const node = discoveredNodes[i]
84
+ const plan = node.source as SnapshotProcessorPlan
85
+ if (plan.intrinsic || node.dependencies!.some((dependency) => dependency.presence === true)) {
86
+ node.presence = true
87
+ presentQueue.push(node)
88
+ }
89
+ }
90
+
91
+ for (let queueIndex = 0; queueIndex < presentQueue.length; queueIndex++) {
92
+ const nodeDependents = dependents.get(presentQueue[queueIndex])
93
+ if (!nodeDependents) {
94
+ continue
95
+ }
96
+ for (let i = 0; i < nodeDependents.length; i++) {
97
+ const dependent = nodeDependents[i]
98
+ if (dependent.presence === undefined) {
99
+ dependent.presence = true
100
+ presentQueue.push(dependent)
101
+ }
102
+ }
103
+ }
104
+
105
+ for (let i = 0; i < discoveredNodes.length; i++) {
106
+ const node = discoveredNodes[i]
107
+ if (node.presence === undefined) {
108
+ node.presence = false
109
+ node.compiled = true
110
+ node.compiledProcessor = undefined
111
+ node.dependencies = undefined
112
+ }
113
+ }
114
+
115
+ return this.compile()
116
+ }
117
+
118
+ private discover(
119
+ discoveredNodes: SnapshotProcessorPlanNode[],
120
+ dependents: Map<SnapshotProcessorPlanNode, SnapshotProcessorPlanNode[]>,
121
+ visited: Set<SnapshotProcessorPlanNode>
122
+ ): void {
123
+ if (this.presence !== undefined || this.discovering || visited.has(this)) {
124
+ return
125
+ }
126
+
127
+ visited.add(this)
128
+ this.discovering = true
129
+ discoveredNodes.push(this)
130
+ try {
131
+ const plan = this.source as SnapshotProcessorPlan
132
+ const dependencyCheckers = plan.getDependencies()
133
+ this.dependencies = new Array(dependencyCheckers.length)
134
+ for (let i = 0; i < dependencyCheckers.length; i++) {
135
+ const dependency = dependencyCheckers[i].getSnapshotProcessorPlanNode(this.direction)
136
+ this.dependencies[i] = dependency
137
+ if (dependency.presence === undefined) {
138
+ let dependencyDependents = dependents.get(dependency)
139
+ if (!dependencyDependents) {
140
+ dependencyDependents = []
141
+ dependents.set(dependency, dependencyDependents)
142
+ }
143
+ dependencyDependents.push(this)
144
+ }
145
+ dependency.discover(discoveredNodes, dependents, visited)
146
+ }
147
+ } catch (error) {
148
+ // A lazy schema may temporarily fail while a recursive declaration is
149
+ // still initializing. Leave this node discoverable for a later retry.
150
+ this.dependencies = undefined
151
+ throw error
152
+ } finally {
153
+ this.discovering = false
154
+ }
155
+ }
156
+
157
+ private compile(): SnapshotProcessor | undefined {
158
+ if (this.compiled) {
159
+ return this.compiledProcessor
160
+ }
161
+ if (!this.presence) {
162
+ this.compiled = true
163
+ return undefined
164
+ }
165
+ if (this.compiling) {
166
+ if (!this.forwardingProcessor) {
167
+ this.forwardingProcessor = (snapshot) => this.compiledProcessor!(snapshot)
168
+ }
169
+ return this.forwardingProcessor
170
+ }
171
+
172
+ this.compiling = true
173
+ try {
174
+ const dependencies = this.dependencies!
175
+ const dependencyProcessors = new Array<SnapshotProcessor | undefined>(dependencies.length)
176
+ for (let i = 0; i < dependencies.length; i++) {
177
+ dependencyProcessors[i] = dependencies[i].compileForDependency()
178
+ }
179
+
180
+ const processor = (this.source as SnapshotProcessorPlan).compile(dependencyProcessors)
181
+ this.compiledProcessor = processor
182
+ this.compiled = true
183
+ this.dependencies = undefined
184
+ return processor
185
+ } finally {
186
+ this.compiling = false
187
+ }
188
+ }
189
+
190
+ private compileForDependency(): SnapshotProcessor | undefined {
191
+ const processor = this.compile()
192
+ return processor && this.direction === "to" ? this.owner!.toSnapshotProcessor : processor
193
+ }
194
+ }
195
+
196
+ const absentSnapshotProcessorPlanNode = new SnapshotProcessorPlanNode(undefined, "from", undefined)
197
+
9
198
  /**
10
199
  * @internal
11
200
  */
@@ -37,6 +226,7 @@ const typeCheckersWithCachedSnapshotProcessorResultsOfObject = new WeakMap<
37
226
  object,
38
227
  Set<TypeChecker>
39
228
  >()
229
+ const cachedUndefinedSnapshotProcessorResult = Symbol("cachedUndefinedSnapshotProcessorResult")
40
230
 
41
231
  /**
42
232
  * @internal
@@ -63,8 +253,10 @@ export class TypeChecker {
63
253
  readonly getTypeName: (...recursiveTypeCheckers: TypeChecker[]) => string
64
254
  readonly typeInfoGen: TypeInfoGen
65
255
  readonly snapshotType: (sn: unknown) => TypeChecker | null
66
- private readonly _fromSnapshotProcessor: (sn: any) => unknown
67
- private readonly _toSnapshotProcessor: (sn: any) => unknown
256
+ private readonly fromSnapshotProcessorSource: SnapshotProcessorSource
257
+ private readonly toSnapshotProcessorSource: SnapshotProcessorSource
258
+ private fromSnapshotProcessorPlanNode: SnapshotProcessorPlanNode | undefined
259
+ private toSnapshotProcessorPlanNode: SnapshotProcessorPlanNode | undefined
68
260
 
69
261
  check(value: any, path: Path, typeCheckedValue: any): TypeCheckError | null {
70
262
  if (this.unchecked || this.skipCheck) {
@@ -86,24 +278,63 @@ export class TypeChecker {
86
278
  getTypeName: (...recursiveTypeCheckers: TypeChecker[]) => string,
87
279
  typeInfoGen: TypeInfoGen,
88
280
  snapshotType: (sn: unknown) => TypeChecker | null,
89
- _fromSnapshotProcessor: (sn: any) => unknown,
90
- _toSnapshotProcessor: (sn: any) => unknown
281
+ fromSnapshotProcessor: SnapshotProcessorSource,
282
+ toSnapshotProcessor: SnapshotProcessorSource
91
283
  ) {
92
284
  this.baseType = baseType
93
285
  this._check = _check
94
286
  this.getTypeName = getTypeName
95
287
  this.typeInfoGen = typeInfoGen
96
288
  this.snapshotType = snapshotType
97
- this._fromSnapshotProcessor = _fromSnapshotProcessor
98
- this._toSnapshotProcessor = _toSnapshotProcessor
289
+ this.fromSnapshotProcessorSource = fromSnapshotProcessor
290
+ this.toSnapshotProcessorSource = toSnapshotProcessor
99
291
  this.unchecked = !_check
100
292
  this._cachedTypeInfoGen = lazy(typeInfoGen)
101
293
  }
102
294
 
295
+ getSnapshotProcessorPlanNode(direction: SnapshotProcessorDirection): SnapshotProcessorPlanNode {
296
+ const source =
297
+ direction === "from" ? this.fromSnapshotProcessorSource : this.toSnapshotProcessorSource
298
+ if (source === undefined) {
299
+ return absentSnapshotProcessorPlanNode
300
+ }
301
+
302
+ let node =
303
+ direction === "from" ? this.fromSnapshotProcessorPlanNode : this.toSnapshotProcessorPlanNode
304
+ if (!node) {
305
+ node = new SnapshotProcessorPlanNode(this, direction, source)
306
+ if (direction === "from") {
307
+ this.fromSnapshotProcessorPlanNode = node
308
+ } else {
309
+ this.toSnapshotProcessorPlanNode = node
310
+ }
311
+ }
312
+ return node
313
+ }
314
+
315
+ private resolveSnapshotProcessor(
316
+ direction: SnapshotProcessorDirection
317
+ ): SnapshotProcessor | undefined {
318
+ const source =
319
+ direction === "from" ? this.fromSnapshotProcessorSource : this.toSnapshotProcessorSource
320
+ return typeof source === "function"
321
+ ? source
322
+ : source === undefined
323
+ ? undefined
324
+ : this.getSnapshotProcessorPlanNode(direction).resolve()
325
+ }
326
+
327
+ getFromSnapshotProcessor(): SnapshotProcessor | undefined {
328
+ return this.resolveSnapshotProcessor("from")
329
+ }
330
+
331
+ getToSnapshotProcessor(): SnapshotProcessor | undefined {
332
+ return this.resolveSnapshotProcessor("to") ? this.toSnapshotProcessor : undefined
333
+ }
334
+
103
335
  fromSnapshotProcessor = (sn: any): unknown => {
104
- // we cannot cache fromSnapshotProcessor since nobody ensures us
105
- // the original snapshot won't be tweaked after use
106
- return this._fromSnapshotProcessor(sn)
336
+ const processor = this.getFromSnapshotProcessor()
337
+ return processor ? processor(sn) : sn
107
338
  }
108
339
 
109
340
  private readonly _toSnapshotProcessorCache = new WeakMap<object, unknown>()
@@ -113,17 +344,26 @@ export class TypeChecker {
113
344
  }
114
345
 
115
346
  toSnapshotProcessor = (sn: any): unknown => {
347
+ const processor = this.resolveSnapshotProcessor("to")
348
+ if (!processor) {
349
+ return sn
350
+ }
351
+
116
352
  if (typeof sn !== "object" || sn === null) {
117
353
  // not cacheable
118
- return this._toSnapshotProcessor(sn)
354
+ return processor(sn)
119
355
  }
120
356
 
121
- if (this._toSnapshotProcessorCache.has(sn)) {
122
- return this._toSnapshotProcessorCache.get(sn)
357
+ const cachedResult = this._toSnapshotProcessorCache.get(sn)
358
+ if (cachedResult !== undefined) {
359
+ return cachedResult === cachedUndefinedSnapshotProcessorResult ? undefined : cachedResult
123
360
  }
124
361
 
125
- const val = this._toSnapshotProcessor(sn)
126
- this._toSnapshotProcessorCache.set(sn, val)
362
+ const val = processor(sn)
363
+ this._toSnapshotProcessorCache.set(
364
+ sn,
365
+ val === undefined ? cachedUndefinedSnapshotProcessorResult : val
366
+ )
127
367
 
128
368
  // register this type checker as listener of that sn changes
129
369
  const typeCheckerSet = getOrCreate(
@@ -146,6 +386,8 @@ const lateTypeCheckerSymbol = Symbol("lateTypeCheker")
146
386
  export interface LateTypeChecker {
147
387
  [lateTypeCheckerSymbol]: true
148
388
  (): TypeChecker
389
+ getFromSnapshotProcessor(): SnapshotProcessor | undefined
390
+ getToSnapshotProcessor(): SnapshotProcessor | undefined
149
391
  typeInfo: TypeInfo
150
392
  }
151
393
 
@@ -162,7 +404,11 @@ export function lateTypeChecker(fn: () => TypeChecker, typeInfoGen: TypeInfoGen)
162
404
  cached = fn()
163
405
  return cached
164
406
  }
165
- ;(ltc as LateTypeChecker)[lateTypeCheckerSymbol] = true
407
+ Object.assign(ltc, {
408
+ [lateTypeCheckerSymbol]: true as const,
409
+ getFromSnapshotProcessor: () => ltc().getFromSnapshotProcessor(),
410
+ getToSnapshotProcessor: () => ltc().getToSnapshotProcessor(),
411
+ })
166
412
 
167
413
  const cachedTypeInfoGen = lazy(typeInfoGen)
168
414
 
@@ -1,13 +1,14 @@
1
1
  import type { Path } from "../../parent/pathTypes"
2
2
  import { isArray } from "../../utils"
3
3
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
4
- import { arrayCachePruning, createPerEntryCachedCheck } from "../createPerEntryCachedCheck"
4
+ import { createWholeContainerCachedCheck } from "../createCachedTypeCheck"
5
5
  import { getTypeInfo } from "../getTypeInfo"
6
6
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
7
  import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
8
8
  import { TypeCheckError } from "../TypeCheckError"
9
9
  import {
10
10
  lateTypeChecker,
11
+ snapshotProcessorPlan,
11
12
  TypeChecker,
12
13
  TypeCheckerBaseType,
13
14
  TypeInfo,
@@ -28,31 +29,27 @@ import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
28
29
  * @returns
29
30
  */
30
31
  export function typesArray<T extends AnyType>(itemType: T): ArrayType<T[]> {
31
- const typeInfoGen: TypeInfoGen = (t) => new ArrayTypeInfo(t, resolveStandardType(itemType))
32
-
33
- return lateTypeChecker(() => {
34
- const itemChecker = resolveTypeChecker(itemType)
32
+ const resolvedItemType = resolveStandardType(itemType)
33
+ const typeInfoGen: TypeInfoGen = (t) => new ArrayTypeInfo(t, resolvedItemType)
34
+ const typeChecker = lateTypeChecker(() => {
35
+ const itemChecker = resolveTypeChecker(resolvedItemType)
35
36
  const emptyChildPath: Path = []
36
37
 
37
38
  const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) =>
38
39
  `Array<${itemChecker.getTypeName(...recursiveTypeCheckers, itemChecker)}>`
39
40
 
40
- const checkArrayItems = createPerEntryCachedCheck<number>(
41
- (array, checkEntry) => {
42
- for (let i = 0; i < array.length; i++) {
43
- const error = checkEntry(i)
44
- if (error) return error
45
- }
46
- return null
47
- },
48
- (array, index, path, typeCheckedValue) => {
41
+ // MobX observable arrays invalidate reads at collection granularity. Per-index
42
+ // computeds would therefore all be invalidated by the same mutation while adding
43
+ // a computed and cache entry per item.
44
+ const checkArrayItems = createWholeContainerCachedCheck((array, path, typeCheckedValue) => {
45
+ for (let index = 0; index < array.length; index++) {
49
46
  const error = itemChecker.check(array[index], emptyChildPath, typeCheckedValue)
50
- return error
51
- ? prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
52
- : null
53
- },
54
- arrayCachePruning
55
- )
47
+ if (error) {
48
+ return prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
49
+ }
50
+ }
51
+ return null
52
+ })
56
53
 
57
54
  const thisTc: TypeChecker = new TypeChecker(
58
55
  TypeCheckerBaseType.Array,
@@ -93,29 +90,30 @@ export function typesArray<T extends AnyType>(itemType: T): ArrayType<T[]> {
93
90
  return thisTc
94
91
  },
95
92
 
96
- (sn: unknown[]) => {
97
- if (itemChecker.unchecked) {
98
- return sn
99
- }
100
-
101
- return sn.map((item, i) =>
102
- withErrorPathSegment(i, () => itemChecker.fromSnapshotProcessor(item))
103
- )
104
- },
105
-
106
- (sn: unknown[]) => {
107
- if (itemChecker.unchecked) {
108
- return sn
109
- }
110
-
111
- return sn.map((item, i) =>
112
- withErrorPathSegment(i, () => itemChecker.toSnapshotProcessor(item))
113
- )
114
- }
93
+ snapshotProcessorPlan(
94
+ () => [itemChecker],
95
+ ([itemProcessor]) =>
96
+ itemProcessor
97
+ ? (sn: unknown[]) => {
98
+ return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
99
+ }
100
+ : undefined
101
+ ),
102
+
103
+ snapshotProcessorPlan(
104
+ () => [itemChecker],
105
+ ([itemProcessor]) =>
106
+ itemProcessor
107
+ ? (sn: unknown[]) => {
108
+ return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
109
+ }
110
+ : undefined
111
+ )
115
112
  )
116
113
 
117
114
  return thisTc
118
- }, typeInfoGen) as any
115
+ }, typeInfoGen)
116
+ return typeChecker as any
119
117
  }
120
118
 
121
119
  /**
@@ -1,13 +1,14 @@
1
1
  import type { Path } from "../../parent/pathTypes"
2
2
  import { isArray, lazy } from "../../utils"
3
3
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
4
- import { createPerEntryCachedCheck } from "../createPerEntryCachedCheck"
4
+ import { createWholeContainerCachedCheck } from "../createCachedTypeCheck"
5
5
  import { getTypeInfo } from "../getTypeInfo"
6
6
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
7
  import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
8
8
  import { TypeCheckError } from "../TypeCheckError"
9
9
  import {
10
10
  lateTypeChecker,
11
+ snapshotProcessorPlan,
11
12
  TypeChecker,
12
13
  TypeCheckerBaseType,
13
14
  TypeInfo,
@@ -28,10 +29,10 @@ import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
28
29
  * @returns
29
30
  */
30
31
  export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
31
- const typeInfoGen: TypeInfoGen = (t) => new TupleTypeInfo(t, itemTypes.map(resolveStandardType))
32
-
33
- return lateTypeChecker(() => {
34
- const checkers = itemTypes.map(resolveTypeChecker)
32
+ const resolvedItemTypes = itemTypes.map(resolveStandardType)
33
+ const typeInfoGen: TypeInfoGen = (t) => new TupleTypeInfo(t, resolvedItemTypes)
34
+ const typeChecker = lateTypeChecker(() => {
35
+ const checkers = resolvedItemTypes.map(resolveTypeChecker)
35
36
  const emptyChildPath: Path = []
36
37
 
37
38
  const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) => {
@@ -45,26 +46,27 @@ export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
45
46
  return "[" + typeNames.join(", ") + "]"
46
47
  }
47
48
 
48
- // No setupCachePruning needed: tuple length is fixed, so entries never become stale.
49
- const checkTupleItems = createPerEntryCachedCheck<number>(
50
- (_array, checkEntry) => {
51
- for (let i = 0; i < checkers.length; i++) {
52
- const error = checkEntry(i)
53
- if (error) return error
54
- }
55
- return null
56
- },
57
- (array, index, path, typeCheckedValue) => {
58
- const checker = checkers[index]
49
+ const checkTupleItemsImperatively = (
50
+ array: unknown[],
51
+ path: Path,
52
+ typeCheckedValue: any
53
+ ): TypeCheckError | null => {
54
+ for (let i = 0; i < checkers.length; i++) {
55
+ const checker = checkers[i]
59
56
  if (checker.unchecked || checker.skipCheck) {
60
- return null
57
+ continue
58
+ }
59
+ const error = checker.check(array[i], emptyChildPath, typeCheckedValue)
60
+ if (error) {
61
+ return prependPathElementToTypeCheckError(error, path, i, typeCheckedValue)
61
62
  }
62
- const error = checker.check(array[index], emptyChildPath, typeCheckedValue)
63
- return error
64
- ? prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
65
- : null
66
63
  }
67
- )
64
+ return null
65
+ }
66
+
67
+ // MobX observable arrays invalidate reads at collection granularity, so indexed
68
+ // computeds cannot isolate tuple-item changes and only add allocation overhead.
69
+ const checkTupleItems = createWholeContainerCachedCheck(checkTupleItemsImperatively)
68
70
 
69
71
  const thisTc: TypeChecker = new TypeChecker(
70
72
  TypeCheckerBaseType.Array,
@@ -100,21 +102,36 @@ export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
100
102
  return thisTc
101
103
  },
102
104
 
103
- (array: unknown[]) => {
104
- return array.map((item, i) => {
105
- return withErrorPathSegment(i, () => checkers[i].fromSnapshotProcessor(item))
106
- })
107
- },
108
-
109
- (array: unknown[]) => {
110
- return array.map((item, i) => {
111
- return withErrorPathSegment(i, () => checkers[i].toSnapshotProcessor(item))
112
- })
113
- }
105
+ snapshotProcessorPlan(
106
+ () => checkers,
107
+ (processors) =>
108
+ processors.some(Boolean)
109
+ ? (array: unknown[]) => {
110
+ return array.map((item, i) => {
111
+ const processor = processors[i]
112
+ return processor ? withErrorPathSegment(i, () => processor(item)) : item
113
+ })
114
+ }
115
+ : undefined
116
+ ),
117
+
118
+ snapshotProcessorPlan(
119
+ () => checkers,
120
+ (processors) =>
121
+ processors.some(Boolean)
122
+ ? (array: unknown[]) => {
123
+ return array.map((item, i) => {
124
+ const processor = processors[i]
125
+ return processor ? withErrorPathSegment(i, () => processor(item)) : item
126
+ })
127
+ }
128
+ : undefined
129
+ )
114
130
  )
115
131
 
116
132
  return thisTc
117
- }, typeInfoGen) as any
133
+ }, typeInfoGen)
134
+ return typeChecker as any
118
135
  }
119
136
 
120
137
  /**