mobx-keystone 1.20.0 → 1.22.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 (174) hide show
  1. package/CHANGELOG.md +864 -853
  2. package/README.md +61 -61
  3. package/dist/mobx-keystone.esm.js +11336 -11148
  4. package/dist/mobx-keystone.esm.mjs +11336 -11148
  5. package/dist/mobx-keystone.umd.js +12229 -11738
  6. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +1 -1
  7. package/dist/types/modelShared/prop.d.ts +5 -0
  8. package/dist/types/redux/connectReduxDevTools.d.ts +17 -4
  9. package/dist/types/ref/Ref.d.ts +1 -1
  10. package/dist/types/treeUtils/sandbox.d.ts +1 -1
  11. package/dist/types/types/TypeChecker.d.ts +1 -1
  12. package/dist/types/types/arrayBased/typesArray.d.ts +1 -1
  13. package/dist/types/types/arrayBased/typesTuple.d.ts +1 -1
  14. package/dist/types/types/objectBased/typesArraySet.d.ts +1 -1
  15. package/dist/types/types/objectBased/typesDataModelData.d.ts +1 -1
  16. package/dist/types/types/objectBased/typesModel.d.ts +1 -1
  17. package/dist/types/types/objectBased/typesObject.d.ts +3 -3
  18. package/dist/types/types/objectBased/typesObjectMap.d.ts +1 -1
  19. package/dist/types/types/objectBased/typesRecord.d.ts +1 -1
  20. package/dist/types/types/primitiveBased/typesPrimitive.d.ts +1 -1
  21. package/dist/types/types/types.d.ts +3 -3
  22. package/dist/types/types/utility/typesCodec.d.ts +1 -6
  23. package/dist/types/types/utility/typesCodecCore.d.ts +1 -1
  24. package/dist/types/types/utility/typesOr.d.ts +1 -1
  25. package/dist/types/types/utility/typesRefinement.d.ts +1 -1
  26. package/dist/types/types/utility/typesSkipCheck.d.ts +1 -1
  27. package/dist/types/types/utility/typesTag.d.ts +1 -1
  28. package/dist/types/wrappers/ObjectMap.d.ts +2 -0
  29. package/dist/types/wrappers/asMap.d.ts +10 -6
  30. package/package.json +14 -12
  31. package/src/action/applyAction.ts +125 -114
  32. package/src/action/applyMethodCall.ts +37 -37
  33. package/src/action/isModelAction.ts +16 -16
  34. package/src/action/middleware.ts +137 -137
  35. package/src/action/modelFlow.ts +240 -240
  36. package/src/action/modelFlowPromiseGenerator.ts +127 -125
  37. package/src/action/pendingActions.ts +42 -42
  38. package/src/action/wrapInAction.ts +156 -156
  39. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +1 -1
  40. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +172 -172
  41. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +20 -20
  42. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +17 -17
  43. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +42 -42
  44. package/src/actionMiddlewares/actionSerialization/objectPathSerializer.ts +50 -50
  45. package/src/actionMiddlewares/actionSerialization/objectSnapshotSerializer.ts +21 -21
  46. package/src/actionMiddlewares/actionSerialization/plainObjectSerializer.ts +24 -32
  47. package/src/actionMiddlewares/actionSerialization/primitiveSerializer.ts +51 -51
  48. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +37 -37
  49. package/src/actionMiddlewares/actionTrackingMiddleware.ts +3 -3
  50. package/src/actionMiddlewares/onActionMiddleware.ts +2 -2
  51. package/src/actionMiddlewares/readonlyMiddleware.ts +2 -2
  52. package/src/actionMiddlewares/transactionMiddleware.ts +4 -4
  53. package/src/actionMiddlewares/undoMiddleware.ts +804 -796
  54. package/src/actionMiddlewares/utils.ts +2 -2
  55. package/src/computedTree/computedTree.ts +143 -143
  56. package/src/context/context.ts +1 -1
  57. package/src/dataModel/BaseDataModel.ts +194 -192
  58. package/src/dataModel/actions.ts +1 -1
  59. package/src/dataModel/newDataModel.ts +57 -57
  60. package/src/deepChange/index.ts +1 -1
  61. package/src/deepChange/onDeepChange.ts +244 -244
  62. package/src/frozen/Frozen.ts +146 -146
  63. package/src/globalConfig/globalConfig.ts +107 -107
  64. package/src/index.ts +24 -24
  65. package/src/model/BaseModel.ts +275 -273
  66. package/src/model/ModelConstructorOptions.ts +15 -15
  67. package/src/model/getModelMetadata.ts +51 -51
  68. package/src/model/index.ts +6 -6
  69. package/src/model/mixins.ts +288 -288
  70. package/src/model/newModel.ts +301 -301
  71. package/src/model/utils.ts +249 -249
  72. package/src/modelShared/modelDecorator.ts +260 -253
  73. package/src/modelShared/modelInfo.ts +50 -50
  74. package/src/modelShared/modelSymbols.ts +3 -3
  75. package/src/modelShared/newModel.ts +25 -25
  76. package/src/modelShared/prop.ts +713 -684
  77. package/src/modelShared/sharedInternalModel.ts +427 -417
  78. package/src/modelShared/utils.ts +24 -24
  79. package/src/parent/core.ts +65 -65
  80. package/src/parent/coreObjectChildren.ts +180 -180
  81. package/src/parent/detach.ts +47 -47
  82. package/src/parent/findParent.ts +1 -1
  83. package/src/parent/getChildrenObjects.ts +25 -25
  84. package/src/parent/path.ts +386 -381
  85. package/src/parent/setParent.ts +149 -149
  86. package/src/parent/walkTree.ts +1 -1
  87. package/src/patch/applyPatches.ts +187 -187
  88. package/src/patch/jsonPatch.ts +108 -108
  89. package/src/patch/patchRecorder.ts +6 -1
  90. package/src/redux/connectReduxDevTools.ts +39 -7
  91. package/src/redux/redux.ts +6 -2
  92. package/src/ref/Ref.ts +1 -1
  93. package/src/ref/core.ts +339 -345
  94. package/src/ref/customRef.ts +3 -3
  95. package/src/ref/rootRef.ts +100 -100
  96. package/src/rootStore/attachDetach.ts +89 -89
  97. package/src/rootStore/rootStore.ts +1 -1
  98. package/src/snapshot/SnapshotOf.ts +104 -104
  99. package/src/snapshot/SnapshotProcessingError.ts +45 -45
  100. package/src/snapshot/applySnapshot.ts +177 -181
  101. package/src/snapshot/cloneTreeValue.ts +30 -30
  102. package/src/snapshot/fromArraySnapshot.ts +27 -33
  103. package/src/snapshot/fromModelSnapshot.ts +70 -70
  104. package/src/snapshot/fromPlainObjectSnapshot.ts +47 -42
  105. package/src/snapshot/fromSnapshot.ts +198 -198
  106. package/src/snapshot/getSnapshot.ts +80 -79
  107. package/src/snapshot/index.ts +8 -8
  108. package/src/snapshot/internal.ts +230 -230
  109. package/src/snapshot/reconcileArraySnapshot.ts +70 -70
  110. package/src/snapshot/reconcileModelSnapshot.ts +155 -150
  111. package/src/snapshot/reconcilePlainObjectSnapshot.ts +72 -72
  112. package/src/snapshot/reconcileSnapshot.ts +88 -88
  113. package/src/standardActions/actions.ts +64 -64
  114. package/src/standardActions/objectActions.ts +60 -60
  115. package/src/transforms/ImmutableDate.ts +93 -93
  116. package/src/transforms/asMap.ts +50 -50
  117. package/src/transforms/asSet.ts +22 -22
  118. package/src/transforms/bigint.ts +20 -20
  119. package/src/transforms/date.ts +40 -40
  120. package/src/treeUtils/deepEquals.ts +55 -55
  121. package/src/treeUtils/sandbox.ts +6 -3
  122. package/src/tweaker/TweakerPriority.ts +9 -9
  123. package/src/tweaker/tweak.ts +4 -3
  124. package/src/tweaker/tweakArray.ts +530 -475
  125. package/src/tweaker/tweakFrozen.ts +42 -42
  126. package/src/tweaker/tweakModel.ts +1 -1
  127. package/src/tweaker/tweakPlainObject.ts +355 -355
  128. package/src/tweaker/typeChecking.ts +101 -101
  129. package/src/types/SnapshotTypeMismatchError.ts +46 -46
  130. package/src/types/TypeCheckError.ts +183 -184
  131. package/src/types/TypeChecker.ts +202 -185
  132. package/src/types/arrayBased/typesArray.ts +136 -137
  133. package/src/types/arrayBased/typesTuple.ts +138 -139
  134. package/src/types/createPerEntryCachedCheck.ts +166 -160
  135. package/src/types/index.ts +6 -6
  136. package/src/types/objectBased/typesArraySet.ts +133 -134
  137. package/src/types/objectBased/typesDataModelData.ts +203 -204
  138. package/src/types/objectBased/typesModel.ts +220 -221
  139. package/src/types/objectBased/typesObject.ts +308 -310
  140. package/src/types/objectBased/typesObjectMap.ts +142 -143
  141. package/src/types/objectBased/typesRecord.ts +156 -157
  142. package/src/types/objectBased/typesRef.ts +89 -89
  143. package/src/types/primitiveBased/typesEnum.ts +63 -63
  144. package/src/types/primitiveBased/typesPrimitive.ts +219 -220
  145. package/src/types/registerDefaultStandardTypeResolvers.ts +21 -21
  146. package/src/types/resolveTypeChecker.ts +78 -78
  147. package/src/types/schemas.ts +208 -208
  148. package/src/types/tProp.ts +277 -268
  149. package/src/types/typeCheck.ts +21 -21
  150. package/src/types/typeCheckErrorUtils.ts +23 -23
  151. package/src/types/types.ts +142 -142
  152. package/src/types/utility/typeToStoredData.ts +37 -37
  153. package/src/types/utility/typesBigInt.ts +15 -15
  154. package/src/types/utility/typesCodec.ts +41 -38
  155. package/src/types/utility/typesCodecCore.ts +178 -178
  156. package/src/types/utility/typesCodecSupport.ts +806 -806
  157. package/src/types/utility/typesDate.ts +27 -27
  158. package/src/types/utility/typesMap.ts +253 -222
  159. package/src/types/utility/typesOr.ts +263 -259
  160. package/src/types/utility/typesRefinement.ts +131 -125
  161. package/src/types/utility/typesSet.ts +110 -110
  162. package/src/types/utility/typesSkipCheck.ts +69 -70
  163. package/src/types/utility/typesTag.ts +88 -82
  164. package/src/types/utility/typesUnchecked.ts +40 -40
  165. package/src/utils/AnyFunction.ts +4 -4
  166. package/src/utils/ModelPool.ts +59 -59
  167. package/src/utils/chainFns.ts +18 -18
  168. package/src/utils/decorators.ts +274 -274
  169. package/src/utils/errorDiagnostics.ts +204 -204
  170. package/src/utils/index.ts +443 -372
  171. package/src/wrappers/ArraySet.ts +156 -156
  172. package/src/wrappers/ObjectMap.ts +155 -136
  173. package/src/wrappers/asMap.ts +65 -30
  174. package/src/wrappers/asSet.ts +3 -3
@@ -1,139 +1,138 @@
1
- import type { Path } from "../../parent/pathTypes"
2
- import { isArray, lazy } from "../../utils"
3
- import { withErrorPathSegment } from "../../utils/errorDiagnostics"
4
- import { createPerEntryCachedCheck } from "../createPerEntryCachedCheck"
5
- import { getTypeInfo } from "../getTypeInfo"
6
- import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
- import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
8
- import { TypeCheckError } from "../TypeCheckError"
9
- import {
10
- lateTypeChecker,
11
- TypeChecker,
12
- TypeCheckerBaseType,
13
- TypeInfo,
14
- TypeInfoGen,
15
- } from "../TypeChecker"
16
- import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
17
-
18
- /**
19
- * A type that represents an tuple of values of a given type.
20
- *
21
- * Example:
22
- * ```ts
23
- * const stringNumberTupleType = types.tuple(types.string, types.number)
24
- * ```
25
- *
26
- * @template T Item types.
27
- * @param itemTypes Type of inner items.
28
- * @returns
29
- */
30
- 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)
35
- const emptyChildPath: Path = []
36
-
37
- const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) => {
38
- const typeNames = checkers.map((tc) => {
39
- if (recursiveTypeCheckers.includes(tc)) {
40
- return "..."
41
- }
42
- return tc.getTypeName(...recursiveTypeCheckers, tc)
43
- })
44
-
45
- return "[" + typeNames.join(", ") + "]"
46
- }
47
-
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]
59
- if (checker.unchecked || checker.skipCheck) {
60
- return null
61
- }
62
- const error = checker.check(array[index], emptyChildPath, typeCheckedValue)
63
- return error
64
- ? prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
65
- : null
66
- }
67
- )
68
-
69
- const thisTc: TypeChecker = new TypeChecker(
70
- TypeCheckerBaseType.Array,
71
-
72
- (array, path, typeCheckedValue) => {
73
- if (!isArray(array) || array.length !== itemTypes.length) {
74
- return new TypeCheckError({
75
- path,
76
- expectedTypeName: getTypeName(thisTc),
77
- actualValue: array,
78
- typeCheckedValue,
79
- })
80
- }
81
-
82
- return checkTupleItems(array, path, typeCheckedValue)
83
- },
84
-
85
- getTypeName,
86
- typeInfoGen,
87
-
88
- (array) => {
89
- if (!isArray(array) || array.length !== itemTypes.length) {
90
- return null
91
- }
92
-
93
- for (let i = 0; i < array.length; i++) {
94
- const itemActualChecker = checkers[i].snapshotType(array[i])
95
- if (!itemActualChecker) {
96
- return null
97
- }
98
- }
99
-
100
- return thisTc
101
- },
102
-
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
- }
114
- )
115
-
116
- return thisTc
117
- }, typeInfoGen) as any
118
- }
119
-
120
- /**
121
- * `types.tuple` type info.
122
- */
123
- export class TupleTypeInfo extends TypeInfo {
124
- readonly kind = "tuple"
125
-
126
- // memoize to always return the same array on the getter
127
- private _itemTypeInfos = lazy(() => this.itemTypes.map(getTypeInfo))
128
-
129
- get itemTypeInfos(): ReadonlyArray<TypeInfo> {
130
- return this._itemTypeInfos()
131
- }
132
-
133
- constructor(
134
- thisType: AnyStandardType,
135
- readonly itemTypes: ReadonlyArray<AnyStandardType>
136
- ) {
137
- super(thisType)
138
- }
139
- }
1
+ import type { Path } from "../../parent/pathTypes"
2
+ import { isArray, lazy } from "../../utils"
3
+ import { withErrorPathSegment } from "../../utils/errorDiagnostics"
4
+ import { createPerEntryCachedCheck } from "../createPerEntryCachedCheck"
5
+ import { getTypeInfo } from "../getTypeInfo"
6
+ import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
+ import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
8
+ import { TypeCheckError } from "../TypeCheckError"
9
+ import {
10
+ lateTypeChecker,
11
+ TypeChecker,
12
+ TypeCheckerBaseType,
13
+ TypeInfo,
14
+ type TypeInfoGen,
15
+ } from "../TypeChecker"
16
+ import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
17
+
18
+ /**
19
+ * A type that represents an tuple of values of a given type.
20
+ *
21
+ * Example:
22
+ * ```ts
23
+ * const stringNumberTupleType = types.tuple(types.string, types.number)
24
+ * ```
25
+ *
26
+ * @template T Item types.
27
+ * @param itemTypes Type of inner items.
28
+ * @returns
29
+ */
30
+ 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)
35
+ const emptyChildPath: Path = []
36
+
37
+ const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) => {
38
+ const typeNames = checkers.map((tc) => {
39
+ if (recursiveTypeCheckers.includes(tc)) {
40
+ return "..."
41
+ }
42
+ return tc.getTypeName(...recursiveTypeCheckers, tc)
43
+ })
44
+
45
+ return "[" + typeNames.join(", ") + "]"
46
+ }
47
+
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]
59
+ if (checker.unchecked || checker.skipCheck) {
60
+ return null
61
+ }
62
+ const error = checker.check(array[index], emptyChildPath, typeCheckedValue)
63
+ return error
64
+ ? prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
65
+ : null
66
+ }
67
+ )
68
+
69
+ const thisTc: TypeChecker = new TypeChecker(
70
+ TypeCheckerBaseType.Array,
71
+
72
+ (array, path, typeCheckedValue) => {
73
+ if (!isArray(array) || array.length !== itemTypes.length) {
74
+ return new TypeCheckError({
75
+ path,
76
+ expectedTypeName: getTypeName(thisTc),
77
+ actualValue: array,
78
+ typeCheckedValue,
79
+ })
80
+ }
81
+
82
+ return checkTupleItems(array, path, typeCheckedValue)
83
+ },
84
+
85
+ getTypeName,
86
+ typeInfoGen,
87
+
88
+ (array) => {
89
+ if (!isArray(array) || array.length !== itemTypes.length) {
90
+ return null
91
+ }
92
+
93
+ for (let i = 0; i < array.length; i++) {
94
+ const itemActualChecker = checkers[i].snapshotType(array[i])
95
+ if (!itemActualChecker) {
96
+ return null
97
+ }
98
+ }
99
+
100
+ return thisTc
101
+ },
102
+
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
+ }
114
+ )
115
+
116
+ return thisTc
117
+ }, typeInfoGen) as any
118
+ }
119
+
120
+ /**
121
+ * `types.tuple` type info.
122
+ */
123
+ export class TupleTypeInfo extends TypeInfo {
124
+ readonly kind = "tuple"
125
+ readonly itemTypes: ReadonlyArray<AnyStandardType>
126
+
127
+ // memoize to always return the same array on the getter
128
+ private _itemTypeInfos = lazy(() => this.itemTypes.map(getTypeInfo))
129
+
130
+ get itemTypeInfos(): ReadonlyArray<TypeInfo> {
131
+ return this._itemTypeInfos()
132
+ }
133
+
134
+ constructor(thisType: AnyStandardType, itemTypes: ReadonlyArray<AnyStandardType>) {
135
+ super(thisType)
136
+ this.itemTypes = itemTypes
137
+ }
138
+ }
@@ -1,160 +1,166 @@
1
- import { computed, IArrayDidChange, type IComputedValue, IObjectDidChange, observe } from "mobx"
2
- import type { Path } from "../parent/pathTypes"
3
- import { isTweakedObject } from "../tweaker/core"
4
- import type { TypeCheckError } from "./TypeCheckError"
5
-
6
- /**
7
- * @internal
8
- *
9
- * Per-entry cache map type used by `createPerEntryCachedCheck`.
10
- */
11
- export type PerEntryCache = Map<PropertyKey, IComputedValue<TypeCheckError | null>>
12
-
13
- /**
14
- * @internal
15
- *
16
- * Callback that sets up reactive pruning of stale per-entry cache entries.
17
- * Called once per observable value when the aggregation is first created.
18
- * Should install an observe listener that removes entries from the cache
19
- * when the collection structure changes (e.g. key removal, array shrink).
20
- */
21
- export type CachePruningSetup = (value: any, capturedEntries: PerEntryCache) => void
22
-
23
- /**
24
- * @internal
25
- *
26
- * Pruning strategy for record / plain-object types.
27
- * Removes a per-entry cached computed when an object key is removed.
28
- */
29
- export const recordCachePruning: CachePruningSetup = (value, capturedEntries) => {
30
- observe(value, (change: IObjectDidChange) => {
31
- if (change.type === "remove") {
32
- capturedEntries.delete(change.name)
33
- }
34
- })
35
- }
36
-
37
- /**
38
- * @internal
39
- *
40
- * Pruning strategy for array types.
41
- * After a splice that shrinks the array, removes per-entry cached computeds
42
- * for indices that no longer exist (i.e. index >= newLength).
43
- */
44
- export const arrayCachePruning: CachePruningSetup = (value, capturedEntries) => {
45
- observe(value, (change: IArrayDidChange) => {
46
- if (change.type === "splice") {
47
- const newLength = (value as unknown[]).length
48
- if (capturedEntries.size > newLength) {
49
- for (const key of capturedEntries.keys()) {
50
- if ((key as number) >= newLength) {
51
- capturedEntries.delete(key)
52
- }
53
- }
54
- }
55
- }
56
- })
57
- }
58
-
59
- /**
60
- * @internal
61
- *
62
- * Debug registry mapping observable values to their per-entry cache Maps.
63
- * Used by tests to verify that stale entries are actually pruned.
64
- */
65
- const __debugPerEntryCaches = new WeakMap<object, PerEntryCache>()
66
-
67
- /**
68
- * @internal
69
- *
70
- * Returns the number of entries in the per-entry cache for the given observable value,
71
- * or `undefined` if the value has no cached entries. Used by tests to verify pruning.
72
- */
73
- export function __getPerEntryCacheSize(value: object): number | undefined {
74
- return __debugPerEntryCaches.get(value)?.size
75
- }
76
-
77
- /**
78
- * @internal
79
- *
80
- * Creates a check function that uses per-entry MobX computeds + an aggregation computed
81
- * for fine-grained reactive type checking.
82
- *
83
- * For tweaked (observable) values:
84
- * 1. Per-entry computeds — each reads only one entry's observable, so changing entry A
85
- * only invalidates entry A's computed.
86
- * 2. Aggregation computed iterates per-entry computeds. Thanks to MobX's change propagation,
87
- * if a per-entry computed re-evaluates but returns the same result (e.g. null → null),
88
- * the aggregation body is skipped entirely.
89
- * 3. Stale entries are pruned reactively via an optional `setupCachePruning` callback
90
- * (e.g. MobX `observe` listener) that fires on structural mutations, preventing
91
- * memory leaks for dynamic collections.
92
- *
93
- * For non-tweaked values (plain snapshots), falls back to imperative checking.
94
- *
95
- * @param iterateEntries Called both inside the aggregation computed (cached path) and
96
- * for the imperative fallback. Receives a `checkEntry(key)` callback that either
97
- * returns a cached computed result or performs a direct check, depending on the path.
98
- * Should call `checkEntry(key)` for each entry and return the first error, or null.
99
- * @param checkEntry Checks a single entry. For tweaked values, the utility wraps this
100
- * in a non-keepAlive computed automatically. For non-tweaked values, called directly.
101
- * @param setupCachePruning Optional callback to install reactive pruning of stale entries.
102
- * Called once per observable value. Use `recordCachePruning` for records/objects,
103
- * `arrayCachePruning` for arrays, or omit for fixed-schema types (objects, tuples).
104
- */
105
- export function createPerEntryCachedCheck<K extends PropertyKey>(
106
- iterateEntries: (
107
- value: any,
108
- checkEntry: (entryKey: K) => TypeCheckError | null
109
- ) => TypeCheckError | null,
110
- checkEntry: (value: any, entryKey: K, path: Path, typeCheckedValue: any) => TypeCheckError | null,
111
- setupCachePruning?: CachePruningSetup
112
- ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
113
- const perEntryCache = new WeakMap<object, PerEntryCache>()
114
- const aggregationCache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
115
-
116
- // Mutable call context updated before each `.get()` so computed closures
117
- // always use the latest path/typeCheckedValue. Safe because MobX computed
118
- // evaluation is synchronous and single-threaded.
119
- let currentPath: Path
120
- let currentTypeCheckedValue: any
121
-
122
- return (value: any, path: Path, typeCheckedValue: any): TypeCheckError | null => {
123
- if (!isTweakedObject(value, true)) {
124
- return iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
125
- }
126
-
127
- // Update mutable scope before evaluation.
128
- currentPath = path
129
- currentTypeCheckedValue = typeCheckedValue
130
-
131
- let aggr = aggregationCache.get(value)
132
- if (!aggr) {
133
- let entries = perEntryCache.get(value)
134
- if (!entries) {
135
- entries = new Map()
136
- perEntryCache.set(value, entries)
137
- __debugPerEntryCaches.set(value, entries)
138
- }
139
-
140
- const capturedEntries = entries
141
-
142
- // Install reactive pruning if a strategy was provided.
143
- setupCachePruning?.(value, capturedEntries)
144
-
145
- const cachedCheckEntry = (key: K): TypeCheckError | null => {
146
- let c = capturedEntries.get(key)
147
- if (!c) {
148
- c = computed(() => checkEntry(value, key, currentPath, currentTypeCheckedValue))
149
- capturedEntries.set(key, c)
150
- }
151
- return c.get()
152
- }
153
-
154
- aggr = computed(() => iterateEntries(value, cachedCheckEntry), { keepAlive: true })
155
- aggregationCache.set(value, aggr)
156
- }
157
-
158
- return aggr.get()
159
- }
160
- }
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
+ }
@@ -1,6 +1,6 @@
1
- export * from "./SnapshotTypeMismatchError"
2
- export * from "./schemas"
3
- export * from "./TypeCheckError"
4
- export * from "./tProp"
5
- export { typeCheck } from "./typeCheck"
6
- export * from "./types"
1
+ export * from "./SnapshotTypeMismatchError"
2
+ export * from "./schemas"
3
+ export * from "./TypeCheckError"
4
+ export * from "./tProp"
5
+ export { typeCheck } from "./typeCheck"
6
+ export * from "./types"