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
@@ -4,28 +4,27 @@ import { modelIdKey, modelTypeKey } from "../model/metadata"
4
4
  import { getSnapshotModelType, isModel } from "../model/utils"
5
5
  import type { ModelClass } from "../modelShared/BaseModelShared"
6
6
  import { getModelInfoForName } from "../modelShared/modelInfo"
7
- import { dataObjectParent } from "../parent/core"
7
+ import { dataToModelNode } from "../parent/core"
8
8
  import {
9
9
  getDeepObjectChildren,
10
10
  registerDeepObjectChildrenExtension,
11
11
  } from "../parent/coreObjectChildren"
12
12
 
13
- function byModelTypeAndIdKey(modelType: string, modelId: string) {
14
- return JSON.stringify([modelType, modelId])
15
- }
13
+ type ModelsById = Map<string, AnyModel>
14
+ type ModelsByTypeAndId = Map<string, ModelsById>
16
15
 
17
16
  export class ModelPool {
18
- private pool: ReadonlyMap<string, AnyModel>
17
+ private pool: ReadonlyMap<string, ReadonlyMap<string, AnyModel>>
19
18
 
20
19
  constructor(root: object) {
21
20
  // make sure we don't use the sub-data $ object
22
- root = dataObjectParent.get(root) ?? root
21
+ root = dataToModelNode(root)
23
22
 
24
23
  this.pool = getDeepChildrenModels(getDeepObjectChildren(root))
25
24
  }
26
25
 
27
26
  findModelByTypeAndId(modelType: string, modelId: string | undefined): AnyModel | undefined {
28
- return modelId ? this.pool.get(byModelTypeAndIdKey(modelType, modelId)) : undefined
27
+ return modelId ? this.pool.get(modelType)?.get(modelId) : undefined
29
28
  }
30
29
 
31
30
  findModelForSnapshot(sn: any): AnyModel | undefined {
@@ -43,17 +42,33 @@ export class ModelPool {
43
42
  }
44
43
  }
45
44
 
46
- const getDeepChildrenModels = registerDeepObjectChildrenExtension<Map<string, AnyModel>>({
47
- initData() {
48
- return new Map()
49
- },
45
+ type GetDeepChildrenModels = (data: ReturnType<typeof getDeepObjectChildren>) => ModelsByTypeAndId
46
+
47
+ let getDeepChildrenModels: GetDeepChildrenModels = (data) => {
48
+ // Register on first use to keep ModelPool out of the coreObjectChildren
49
+ // module-initialization cycle. The extension getter backfills its index from
50
+ // data.deep, so registering after a deep-children set already exists is safe.
51
+ const getRegisteredDeepChildrenModels = registerDeepObjectChildrenExtension<ModelsByTypeAndId>({
52
+ initData() {
53
+ return new Map()
54
+ },
50
55
 
51
- addNode(node, data) {
52
- if (isModel(node)) {
53
- const id = node[modelIdKey]
54
- if (id) {
55
- data.set(byModelTypeAndIdKey(node[modelTypeKey], id), node)
56
+ addNode(node, extensionData) {
57
+ if (isModel(node)) {
58
+ const id = node[modelIdKey]
59
+ if (id) {
60
+ const modelType = node[modelTypeKey]
61
+ let modelsById = extensionData.get(modelType)
62
+ if (!modelsById) {
63
+ modelsById = new Map()
64
+ extensionData.set(modelType, modelsById)
65
+ }
66
+ modelsById.set(id, node)
67
+ }
56
68
  }
57
- }
58
- },
59
- })
69
+ },
70
+ })
71
+
72
+ getDeepChildrenModels = getRegisteredDeepChildrenModels
73
+ return getRegisteredDeepChildrenModels(data)
74
+ }
@@ -1,13 +1,59 @@
1
1
  import type { Path, PathElement, WritablePath } from "../parent/pathTypes"
2
2
 
3
- interface ErrorDiagnosticsContext {
4
- path: WritablePath
5
- modelTrail: string[]
3
+ /**
4
+ * @internal
5
+ */
6
+ export class ErrorDiagnosticsContext {
7
+ private readonly path: WritablePath = []
8
+ private readonly modelTrailTypes: string[] = []
9
+ private readonly modelTrailIds: (string | undefined)[] = []
10
+
11
+ pushPath(segment: PathElement): void {
12
+ this.path.push(segment)
13
+ }
14
+
15
+ popPath(): void {
16
+ this.path.pop()
17
+ }
18
+
19
+ getPathLength(): number {
20
+ return this.path.length
21
+ }
22
+
23
+ truncatePath(length: number): void {
24
+ this.path.length = length
25
+ }
26
+
27
+ pushModelTrail(modelType: string, modelId: string | undefined): void {
28
+ this.modelTrailTypes.push(modelType)
29
+ this.modelTrailIds.push(modelId)
30
+ }
31
+
32
+ popModelTrail(): void {
33
+ this.modelTrailTypes.pop()
34
+ this.modelTrailIds.pop()
35
+ }
36
+
37
+ getPathSnapshot(): Path {
38
+ return [...this.path]
39
+ }
40
+
41
+ getModelTrailSnapshot(): readonly string[] | undefined {
42
+ if (this.modelTrailTypes.length <= 0) {
43
+ return undefined
44
+ }
45
+ return this.modelTrailTypes.map((modelType, index) =>
46
+ formatErrorModelTrailEntry(modelType, this.modelTrailIds[index])
47
+ )
48
+ }
6
49
  }
7
50
 
8
51
  const errorDiagnosticsContextStack: ErrorDiagnosticsContext[] = []
9
52
 
10
- function getCurrentErrorDiagnosticsContext(): ErrorDiagnosticsContext | undefined {
53
+ /**
54
+ * @internal
55
+ */
56
+ export function getCurrentErrorDiagnosticsContext(): ErrorDiagnosticsContext | undefined {
11
57
  return errorDiagnosticsContextStack.length > 0
12
58
  ? errorDiagnosticsContextStack[errorDiagnosticsContextStack.length - 1]
13
59
  : undefined
@@ -22,10 +68,7 @@ export function runWithErrorDiagnosticsContext<T>(fn: () => T): T {
22
68
  return fn()
23
69
  }
24
70
 
25
- const ctx: ErrorDiagnosticsContext = {
26
- path: [],
27
- modelTrail: [],
28
- }
71
+ const ctx = new ErrorDiagnosticsContext()
29
72
  errorDiagnosticsContextStack.push(ctx)
30
73
  try {
31
74
  return fn()
@@ -43,11 +86,11 @@ export function withErrorPathSegment<T>(segment: PathElement, fn: () => T): T {
43
86
  return fn()
44
87
  }
45
88
 
46
- ctx.path.push(segment)
89
+ ctx.pushPath(segment)
47
90
  try {
48
91
  return fn()
49
92
  } finally {
50
- ctx.path.pop()
93
+ ctx.popPath()
51
94
  }
52
95
  }
53
96
 
@@ -64,16 +107,16 @@ export function withErrorPathSegments<T>(segments: readonly PathElement[], fn: (
64
107
  return fn()
65
108
  }
66
109
 
67
- const initialLen = ctx.path.length
110
+ const initialLen = ctx.getPathLength()
68
111
  const segmentsLen = segments.length
69
112
  for (let i = 0; i < segmentsLen; i++) {
70
- ctx.path.push(segments[i]!)
113
+ ctx.pushPath(segments[i]!)
71
114
  }
72
115
 
73
116
  try {
74
117
  return fn()
75
118
  } finally {
76
- ctx.path.length = initialLen
119
+ ctx.truncatePath(initialLen)
77
120
  }
78
121
  }
79
122
 
@@ -112,12 +155,11 @@ export function withErrorModelTrailEntry<T>(
112
155
  return fn()
113
156
  }
114
157
 
115
- const entry = formatErrorModelTrailEntry(modelType, modelId)
116
- ctx.modelTrail.push(entry)
158
+ ctx.pushModelTrail(modelType, modelId)
117
159
  try {
118
160
  return fn()
119
161
  } finally {
120
- ctx.modelTrail.pop()
162
+ ctx.popModelTrail()
121
163
  }
122
164
  }
123
165
 
@@ -126,7 +168,7 @@ export function withErrorModelTrailEntry<T>(
126
168
  */
127
169
  export function getErrorPathSnapshot(): Path | undefined {
128
170
  const ctx = getCurrentErrorDiagnosticsContext()
129
- return ctx ? [...ctx.path] : undefined
171
+ return ctx?.getPathSnapshot()
130
172
  }
131
173
 
132
174
  /**
@@ -134,7 +176,7 @@ export function getErrorPathSnapshot(): Path | undefined {
134
176
  */
135
177
  export function getErrorModelTrailSnapshot(): readonly string[] | undefined {
136
178
  const ctx = getCurrentErrorDiagnosticsContext()
137
- return ctx && ctx.modelTrail.length > 0 ? [...ctx.modelTrail] : undefined
179
+ return ctx?.getModelTrailSnapshot()
138
180
  }
139
181
 
140
182
  /**
@@ -1,5 +1,6 @@
1
1
  import * as mobx from "mobx"
2
2
  import {
3
+ type IComputedValue,
3
4
  type IObservableArray,
4
5
  isObservableArray,
5
6
  isObservableMap,
@@ -109,12 +110,7 @@ export let hasOwnProp: (object: object, propName: PropertyKey) => boolean = (
109
110
  /**
110
111
  * @internal
111
112
  */
112
- export function setOwnProp(target: any, propName: PropertyKey, value: unknown): void {
113
- if (propName !== "__proto__") {
114
- target[propName] = value
115
- return
116
- }
117
-
113
+ export function setProtoProp(target: object, value: unknown): void {
118
114
  Object.defineProperty(target, "__proto__", {
119
115
  enumerable: true,
120
116
  writable: true,
@@ -136,7 +132,12 @@ export function copyOwnEnumerableProps<TTarget extends Record<string, any>>(
136
132
  for (let i = 0; i < len; i++) {
137
133
  const key = keys[i]
138
134
  const value = source[key]
139
- setOwnProp(target, key, mapValue ? mapValue(value, key) : value)
135
+ const mappedValue = mapValue ? mapValue(value, key) : value
136
+ if (key === "__proto__") {
137
+ setProtoProp(target, mappedValue)
138
+ } else {
139
+ ;(target as Record<PropertyKey, unknown>)[key] = mappedValue
140
+ }
140
141
  }
141
142
 
142
143
  return target
@@ -419,6 +420,127 @@ export const mobx6 = {
419
420
  ],
420
421
  }
421
422
 
423
+ /**
424
+ * MobX method action decorator compatible with both legacy decorators and
425
+ * MobX 7's standard decorator API.
426
+ *
427
+ * This is a decorator compatibility helper, not a replacement for other
428
+ * `action` overloads such as `action(fn)`. Field decorators are not supported.
429
+ *
430
+ * @hidden
431
+ */
432
+ export function mobxAction<This, Args extends unknown[], Return>(
433
+ value: (this: This, ...args: Args) => Return,
434
+ context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
435
+ ): (this: This, ...args: Args) => Return
436
+ export function mobxAction<This, Args extends unknown[], Return>(
437
+ target: object,
438
+ propertyKey: string | symbol,
439
+ descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Return>
440
+ ): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> | void
441
+ export function mobxAction(...args: any[]): any {
442
+ if (isStandardDecoratorContext(args[1])) {
443
+ return (mobx.action as any)(args[0], args[1])
444
+ }
445
+
446
+ if (getMobxVersion() < 7) {
447
+ return (mobx.action as any)(...args)
448
+ }
449
+
450
+ const descriptor = args[2] as PropertyDescriptor | undefined
451
+ if (!descriptor?.value) {
452
+ return descriptor
453
+ }
454
+
455
+ if (mobx.isAction(descriptor.value)) {
456
+ return descriptor
457
+ }
458
+
459
+ return {
460
+ ...descriptor,
461
+ value: mobx.action(String(args[1]), descriptor.value),
462
+ }
463
+ }
464
+
465
+ /**
466
+ * MobX getter computed decorator compatible with both legacy decorators and
467
+ * MobX 7's standard decorator API.
468
+ *
469
+ * This is a decorator compatibility helper, not a replacement for other
470
+ * `computed` overloads. Field decorators and decorator options are not supported.
471
+ * Under MobX 7's legacy decorator transform the getter is backed by a computed
472
+ * value, but is not registered as a computed property for MobX introspection.
473
+ *
474
+ * @hidden
475
+ */
476
+ export function mobxComputed<This, Value>(
477
+ value: (this: This) => Value,
478
+ context: ClassGetterDecoratorContext<This, Value>
479
+ ): ((this: This) => Value) | void
480
+ export function mobxComputed<Value>(
481
+ target: object,
482
+ propertyKey: string | symbol,
483
+ descriptor: TypedPropertyDescriptor<Value>
484
+ ): TypedPropertyDescriptor<Value> | void
485
+ export function mobxComputed(...args: any[]): any {
486
+ if (isStandardDecoratorContext(args[1])) {
487
+ return (mobx.computed as any)(args[0], args[1])
488
+ }
489
+
490
+ if (getMobxVersion() < 7) {
491
+ return (mobx.computed as any)(...args)
492
+ }
493
+
494
+ const descriptor = args[2] as PropertyDescriptor | undefined
495
+ if (!descriptor?.get) {
496
+ return descriptor
497
+ }
498
+
499
+ const getter = descriptor.get
500
+ const computedValues = new WeakMap<object, IComputedValue<unknown>>()
501
+
502
+ return {
503
+ ...descriptor,
504
+ get(this: object) {
505
+ let computedValue = computedValues.get(this)
506
+ if (!computedValue) {
507
+ const newComputedValue = mobx.computed(() => getter.call(this), {
508
+ name: String(args[1]),
509
+ })
510
+ computedValues.set(this, newComputedValue)
511
+ computedValue = newComputedValue
512
+ }
513
+ return computedValue.get()
514
+ },
515
+ }
516
+ }
517
+
518
+ // Mirrors MobX's standard-decorator call detection. Legacy decorator calls use
519
+ // a string or symbol property key as their second argument, so checking `kind`
520
+ // distinguishes the call shapes without claiming to validate the full context.
521
+ function isStandardDecoratorContext(value: unknown): boolean {
522
+ return (
523
+ typeof value === "object" &&
524
+ value !== null &&
525
+ typeof (value as { kind?: unknown }).kind === "string"
526
+ )
527
+ }
528
+
529
+ /**
530
+ * Calls `makeObservable` across MobX versions. MobX 7 requires an annotations
531
+ * object, while MobX 6 and older versions use decorator metadata when no
532
+ * annotations are passed.
533
+ *
534
+ * @internal
535
+ */
536
+ export function makeObservableCompat(target: object, annotations?: object): void {
537
+ if (getMobxVersion() >= 7) {
538
+ mobx6.makeObservable(target, annotations ?? {})
539
+ } else {
540
+ mobx6.makeObservable(target)
541
+ }
542
+ }
543
+
422
544
  /**
423
545
  * @internal
424
546
  */
@@ -426,15 +548,26 @@ export function propNameToSetterName(propName: string): string {
426
548
  return `set${propName[0].toUpperCase()}${propName.slice(1)}`
427
549
  }
428
550
 
551
+ let cachedMobxVersion: number | undefined
552
+
429
553
  /**
430
554
  * @internal
431
555
  */
432
556
  export function getMobxVersion(): number {
557
+ if (cachedMobxVersion !== undefined) {
558
+ return cachedMobxVersion
559
+ }
560
+
433
561
  if (mobx6.makeObservable!) {
434
- return 6
562
+ const version = (mobx as any)._getGlobalState?.().version
563
+ cachedMobxVersion = typeof version === "number" && version >= 7 ? version : 6
564
+ } else if ("extendShallowObservable" in mobx) {
565
+ cachedMobxVersion = 4
435
566
  } else {
436
- return 5
567
+ cachedMobxVersion = 5
437
568
  }
569
+
570
+ return cachedMobxVersion
438
571
  }
439
572
 
440
573
  /**
@@ -1,166 +0,0 @@
1
- import {
2
- computed,
3
- type IArrayDidChange,
4
- type IComputedValue,
5
- type IObjectDidChange,
6
- observe,
7
- } from "mobx"
8
- import type { Path } from "../parent/pathTypes"
9
- import { isTweakedObject } from "../tweaker/core"
10
- import type { TypeCheckError } from "./TypeCheckError"
11
-
12
- /**
13
- * @internal
14
- *
15
- * Per-entry cache map type used by `createPerEntryCachedCheck`.
16
- */
17
- export type PerEntryCache = Map<PropertyKey, IComputedValue<TypeCheckError | null>>
18
-
19
- /**
20
- * @internal
21
- *
22
- * Callback that sets up reactive pruning of stale per-entry cache entries.
23
- * Called once per observable value when the aggregation is first created.
24
- * Should install an observe listener that removes entries from the cache
25
- * when the collection structure changes (e.g. key removal, array shrink).
26
- */
27
- export type CachePruningSetup = (value: any, capturedEntries: PerEntryCache) => void
28
-
29
- /**
30
- * @internal
31
- *
32
- * Pruning strategy for record / plain-object types.
33
- * Removes a per-entry cached computed when an object key is removed.
34
- */
35
- export const recordCachePruning: CachePruningSetup = (value, capturedEntries) => {
36
- observe(value, (change: IObjectDidChange) => {
37
- if (change.type === "remove") {
38
- capturedEntries.delete(change.name)
39
- }
40
- })
41
- }
42
-
43
- /**
44
- * @internal
45
- *
46
- * Pruning strategy for array types.
47
- * After a splice that shrinks the array, removes per-entry cached computeds
48
- * for indices that no longer exist (i.e. index >= newLength).
49
- */
50
- export const arrayCachePruning: CachePruningSetup = (value, capturedEntries) => {
51
- observe(value, (change: IArrayDidChange) => {
52
- if (change.type === "splice") {
53
- const newLength = (value as unknown[]).length
54
- if (capturedEntries.size > newLength) {
55
- for (const key of capturedEntries.keys()) {
56
- if ((key as number) >= newLength) {
57
- capturedEntries.delete(key)
58
- }
59
- }
60
- }
61
- }
62
- })
63
- }
64
-
65
- /**
66
- * @internal
67
- *
68
- * Debug registry mapping observable values to their per-entry cache Maps.
69
- * Used by tests to verify that stale entries are actually pruned.
70
- */
71
- const __debugPerEntryCaches = new WeakMap<object, PerEntryCache>()
72
-
73
- /**
74
- * @internal
75
- *
76
- * Returns the number of entries in the per-entry cache for the given observable value,
77
- * or `undefined` if the value has no cached entries. Used by tests to verify pruning.
78
- */
79
- export function __getPerEntryCacheSize(value: object): number | undefined {
80
- return __debugPerEntryCaches.get(value)?.size
81
- }
82
-
83
- /**
84
- * @internal
85
- *
86
- * Creates a check function that uses per-entry MobX computeds + an aggregation computed
87
- * for fine-grained reactive type checking.
88
- *
89
- * For tweaked (observable) values:
90
- * 1. Per-entry computeds — each reads only one entry's observable, so changing entry A
91
- * only invalidates entry A's computed.
92
- * 2. Aggregation computed — iterates per-entry computeds. Thanks to MobX's change propagation,
93
- * if a per-entry computed re-evaluates but returns the same result (e.g. null → null),
94
- * the aggregation body is skipped entirely.
95
- * 3. Stale entries are pruned reactively via an optional `setupCachePruning` callback
96
- * (e.g. MobX `observe` listener) that fires on structural mutations, preventing
97
- * memory leaks for dynamic collections.
98
- *
99
- * For non-tweaked values (plain snapshots), falls back to imperative checking.
100
- *
101
- * @param iterateEntries Called both inside the aggregation computed (cached path) and
102
- * for the imperative fallback. Receives a `checkEntry(key)` callback that either
103
- * returns a cached computed result or performs a direct check, depending on the path.
104
- * Should call `checkEntry(key)` for each entry and return the first error, or null.
105
- * @param checkEntry Checks a single entry. For tweaked values, the utility wraps this
106
- * in a non-keepAlive computed automatically. For non-tweaked values, called directly.
107
- * @param setupCachePruning Optional callback to install reactive pruning of stale entries.
108
- * Called once per observable value. Use `recordCachePruning` for records/objects,
109
- * `arrayCachePruning` for arrays, or omit for fixed-schema types (objects, tuples).
110
- */
111
- export function createPerEntryCachedCheck<K extends PropertyKey>(
112
- iterateEntries: (
113
- value: any,
114
- checkEntry: (entryKey: K) => TypeCheckError | null
115
- ) => TypeCheckError | null,
116
- checkEntry: (value: any, entryKey: K, path: Path, typeCheckedValue: any) => TypeCheckError | null,
117
- setupCachePruning?: CachePruningSetup
118
- ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
119
- const perEntryCache = new WeakMap<object, PerEntryCache>()
120
- const aggregationCache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
121
-
122
- // Mutable call context updated before each `.get()` so computed closures
123
- // always use the latest path/typeCheckedValue. Safe because MobX computed
124
- // evaluation is synchronous and single-threaded.
125
- let currentPath: Path
126
- let currentTypeCheckedValue: any
127
-
128
- return (value: any, path: Path, typeCheckedValue: any): TypeCheckError | null => {
129
- if (!isTweakedObject(value, true)) {
130
- return iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
131
- }
132
-
133
- // Update mutable scope before evaluation.
134
- currentPath = path
135
- currentTypeCheckedValue = typeCheckedValue
136
-
137
- let aggr = aggregationCache.get(value)
138
- if (!aggr) {
139
- let entries = perEntryCache.get(value)
140
- if (!entries) {
141
- entries = new Map()
142
- perEntryCache.set(value, entries)
143
- __debugPerEntryCaches.set(value, entries)
144
- }
145
-
146
- const capturedEntries = entries
147
-
148
- // Install reactive pruning if a strategy was provided.
149
- setupCachePruning?.(value, capturedEntries)
150
-
151
- const cachedCheckEntry = (key: K): TypeCheckError | null => {
152
- let c = capturedEntries.get(key)
153
- if (!c) {
154
- c = computed(() => checkEntry(value, key, currentPath, currentTypeCheckedValue))
155
- capturedEntries.set(key, c)
156
- }
157
- return c.get()
158
- }
159
-
160
- aggr = computed(() => iterateEntries(value, cachedCheckEntry), { keepAlive: true })
161
- aggregationCache.set(value, aggr)
162
- }
163
-
164
- return aggr.get()
165
- }
166
- }