mobx-keystone 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/mobx-keystone.esm.js +6702 -5797
  3. package/dist/mobx-keystone.esm.mjs +6702 -5797
  4. package/dist/mobx-keystone.umd.js +6718 -5798
  5. package/dist/types/action/runUnprotected.d.ts +8 -4
  6. package/dist/types/model/Model.d.ts +5 -0
  7. package/dist/types/modelShared/prop.d.ts +8 -4
  8. package/dist/types/snapshot/modelInitialData.d.ts +1 -0
  9. package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
  10. package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
  11. package/package.json +11 -10
  12. package/src/action/middleware.ts +5 -18
  13. package/src/action/mutationBatch.ts +72 -0
  14. package/src/action/runUnprotected.ts +21 -8
  15. package/src/action/wrapInAction.ts +45 -15
  16. package/src/dataModel/BaseDataModel.ts +4 -8
  17. package/src/deepChange/onDeepChange.ts +246 -16
  18. package/src/model/BaseModel.ts +4 -12
  19. package/src/model/Model.ts +5 -0
  20. package/src/model/newModel.ts +68 -36
  21. package/src/modelShared/prop.ts +105 -63
  22. package/src/modelShared/sharedInternalModel.ts +91 -60
  23. package/src/parent/core.ts +16 -10
  24. package/src/parent/coreObjectChildren.ts +219 -21
  25. package/src/parent/path.ts +6 -5
  26. package/src/parent/setParent.ts +5 -5
  27. package/src/patch/emitPatch.ts +102 -20
  28. package/src/snapshot/fromArraySnapshot.ts +11 -2
  29. package/src/snapshot/fromModelSnapshot.ts +10 -3
  30. package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
  31. package/src/snapshot/fromSnapshot.ts +31 -9
  32. package/src/snapshot/getSnapshot.ts +8 -3
  33. package/src/snapshot/internal.ts +243 -82
  34. package/src/snapshot/modelInitialData.ts +41 -0
  35. package/src/snapshot/reconcileArraySnapshot.ts +9 -3
  36. package/src/snapshot/reconcileModelSnapshot.ts +97 -17
  37. package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
  38. package/src/tweaker/core.ts +30 -5
  39. package/src/tweaker/treeNodeMetadata.ts +38 -0
  40. package/src/tweaker/tweak.ts +15 -4
  41. package/src/tweaker/tweakArray.ts +159 -121
  42. package/src/tweaker/tweakFrozen.ts +2 -2
  43. package/src/tweaker/tweakModel.ts +2 -2
  44. package/src/tweaker/tweakPlainObject.ts +114 -122
  45. package/src/tweaker/typeChecking.ts +10 -1
  46. package/src/types/TypeChecker.ts +261 -15
  47. package/src/types/arrayBased/typesArray.ts +38 -40
  48. package/src/types/arrayBased/typesTuple.ts +51 -34
  49. package/src/types/createCachedTypeCheck.ts +181 -0
  50. package/src/types/objectBased/typesArraySet.ts +42 -32
  51. package/src/types/objectBased/typesDataModelData.ts +18 -11
  52. package/src/types/objectBased/typesModel.ts +1 -1
  53. package/src/types/objectBased/typesObject.ts +57 -28
  54. package/src/types/objectBased/typesObjectMap.ts +51 -40
  55. package/src/types/objectBased/typesRecord.ts +59 -39
  56. package/src/types/objectBased/typesRef.ts +1 -1
  57. package/src/types/primitiveBased/typesPrimitive.ts +9 -9
  58. package/src/types/tProp.ts +80 -44
  59. package/src/types/utility/typesOr.ts +35 -24
  60. package/src/types/utility/typesRefinement.ts +21 -8
  61. package/src/types/utility/typesSkipCheck.ts +21 -8
  62. package/src/types/utility/typesTag.ts +21 -9
  63. package/src/types/utility/typesUnchecked.ts +2 -3
  64. package/src/utils/ModelPool.ts +34 -19
  65. package/src/utils/errorDiagnostics.ts +60 -18
  66. package/src/utils/index.ts +9 -7
  67. package/src/types/createPerEntryCachedCheck.ts +0 -166
  68. /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
@@ -0,0 +1,181 @@
1
+ import { computed, type IComputedValue, type IObjectDidChange, observe } from "mobx"
2
+ import type { Path } from "../parent/pathTypes"
3
+ import { isTweakedObject } from "../tweaker/core"
4
+ import { TypeCheckError } from "./TypeCheckError"
5
+
6
+ const emptyPath: Path = []
7
+
8
+ function contextualizeCachedError(
9
+ error: TypeCheckError | null,
10
+ path: Path,
11
+ typeCheckedValue: any
12
+ ): TypeCheckError | null {
13
+ if (!error) {
14
+ return null
15
+ }
16
+
17
+ return new TypeCheckError({
18
+ path: [...path, ...error.path],
19
+ expectedTypeName: error.expectedTypeName,
20
+ actualValue: error.actualValue,
21
+ typeCheckedValue,
22
+ })
23
+ }
24
+
25
+ type CachedEntry = IComputedValue<TypeCheckError | null>
26
+
27
+ interface EntryCache<K> {
28
+ get(key: K): CachedEntry | undefined
29
+ set(key: K, entry: CachedEntry): void
30
+ }
31
+
32
+ type IterateEntries<K> = (
33
+ value: any,
34
+ checkEntry: (entryKey: K) => TypeCheckError | null
35
+ ) => TypeCheckError | null
36
+
37
+ type CheckEntry<K> = (
38
+ value: any,
39
+ entryKey: K,
40
+ path: Path,
41
+ typeCheckedValue: any
42
+ ) => TypeCheckError | null
43
+
44
+ function createAdaptivePerEntryCachedCheck<K>(
45
+ iterateEntries: IterateEntries<K>,
46
+ checkEntry: CheckEntry<K>,
47
+ createEntryCache: (value: object) => EntryCache<K>
48
+ ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
49
+ const aggregationCache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
50
+
51
+ const checkAllEntries = (value: any, path: Path, typeCheckedValue: any) =>
52
+ iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
53
+
54
+ return (value, path, typeCheckedValue) => {
55
+ if (!isTweakedObject(value, true)) {
56
+ return checkAllEntries(value, path, typeCheckedValue)
57
+ }
58
+
59
+ let aggregation = aggregationCache.get(value)
60
+ if (!aggregation) {
61
+ let entries: EntryCache<K> | undefined
62
+ let isFirstEvaluation = true
63
+ const cachedCheckEntry = (key: K): TypeCheckError | null => {
64
+ if (!entries) {
65
+ entries = createEntryCache(value)
66
+ }
67
+
68
+ let entry = entries.get(key)
69
+ if (!entry) {
70
+ entry = computed(() => checkEntry(value, key, emptyPath, undefined))
71
+ entries.set(key, entry)
72
+ }
73
+ return entry.get()
74
+ }
75
+
76
+ aggregation = computed(
77
+ () => {
78
+ if (isFirstEvaluation) {
79
+ isFirstEvaluation = false
80
+ return checkAllEntries(value, emptyPath, undefined)
81
+ }
82
+ return iterateEntries(value, cachedCheckEntry)
83
+ },
84
+ { keepAlive: true }
85
+ )
86
+ aggregationCache.set(value, aggregation)
87
+ }
88
+
89
+ return contextualizeCachedError(aggregation.get(), path, typeCheckedValue)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @internal
95
+ *
96
+ * Creates an adaptive check for dynamic records.
97
+ *
98
+ * For tweaked (observable) values:
99
+ * 1. The first complete check is cached without allocating per-entry state.
100
+ * 2. After the first invalidation, per-entry computeds each read one property, so
101
+ * changing property A only invalidates property A's computed.
102
+ * 3. The aggregation computed iterates per-entry computeds. Thanks to MobX's propagation,
103
+ * if a per-entry computed re-evaluates but returns the same result (e.g. null → null),
104
+ * the aggregation body is skipped entirely.
105
+ * 4. A pruning observer is installed when the promoted entry cache is first used
106
+ * and removes stale entries immediately when record keys are deleted.
107
+ *
108
+ * Cached checks produce context-free errors. A fresh error is contextualized
109
+ * with the current path and checked root without executing user checkers again.
110
+ *
111
+ * For non-tweaked values (plain snapshots), the entry iterator and checker run
112
+ * directly with the caller's context.
113
+ *
114
+ * @param iterateEntries Iterates the promoted per-entry computeds and returns
115
+ * the first error, or null.
116
+ * @param checkEntry Checks a single entry. The utility wraps it in a
117
+ * non-keepAlive computed automatically after promotion.
118
+ */
119
+ export function createAdaptiveRecordCachedCheck(
120
+ iterateEntries: IterateEntries<PropertyKey>,
121
+ checkEntry: CheckEntry<PropertyKey>
122
+ ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
123
+ return createAdaptivePerEntryCachedCheck(iterateEntries, checkEntry, (value) => {
124
+ const entries = new Map<PropertyKey, CachedEntry>()
125
+ observe(value, (change: IObjectDidChange) => {
126
+ if (change.type === "remove") {
127
+ entries.delete(change.name)
128
+ }
129
+ })
130
+ return entries
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Creates one reactive computed for a complete container check.
136
+ *
137
+ * @internal
138
+ */
139
+ export function createWholeContainerCachedCheck(
140
+ check: (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null
141
+ ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
142
+ const cache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
143
+
144
+ return (value, path, typeCheckedValue) => {
145
+ if (!isTweakedObject(value, true)) {
146
+ return check(value, path, typeCheckedValue)
147
+ }
148
+
149
+ let cached = cache.get(value)
150
+ if (!cached) {
151
+ cached = computed(() => check(value, emptyPath, undefined), { keepAlive: true })
152
+ cache.set(value, cached)
153
+ }
154
+ const error = cached.get()
155
+ return contextualizeCachedError(error, path, typeCheckedValue)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Fixed-schema per-entry cache. Numeric entry keys use
161
+ * array slots, and the aggregation closure owns that array, so only one
162
+ * WeakMap is needed for each checked object. The aggregation caches one
163
+ * complete check first and promotes itself to per-entry computeds after its
164
+ * first dependency invalidation.
165
+ *
166
+ * @internal
167
+ */
168
+ export function createIndexedPerEntryCachedCheck(
169
+ iterateEntries: IterateEntries<number>,
170
+ checkEntry: CheckEntry<number>
171
+ ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
172
+ return createAdaptivePerEntryCachedCheck(iterateEntries, checkEntry, () => {
173
+ const entries: Array<CachedEntry | undefined> = []
174
+ return {
175
+ get: (index) => entries[index],
176
+ set: (index, entry) => {
177
+ entries[index] = entry
178
+ },
179
+ }
180
+ })
181
+ }
@@ -10,6 +10,7 @@ import type { AnyStandardType, AnyType, ModelType, TypeToData } from "../schemas
10
10
  import { TypeCheckError } from "../TypeCheckError"
11
11
  import {
12
12
  lateTypeChecker,
13
+ snapshotProcessorPlan,
13
14
  TypeChecker,
14
15
  TypeCheckerBaseType,
15
16
  TypeInfo,
@@ -31,13 +32,13 @@ import { typesObject } from "./typesObject"
31
32
  * @returns
32
33
  */
33
34
  export function typesArraySet<T extends AnyType>(valueType: T): ModelType<ArraySet<TypeToData<T>>> {
34
- const typeInfoGen: TypeInfoGen = (t) => new ArraySetTypeInfo(t, resolveStandardType(valueType))
35
-
35
+ const resolvedValueType = resolveStandardType(valueType)
36
+ const typeInfoGen: TypeInfoGen = (t) => new ArraySetTypeInfo(t, resolvedValueType)
36
37
  return lateTypeChecker(() => {
37
38
  const modelInfo = modelInfoByClass.get(ArraySet)!
38
39
 
39
- const valueChecker = resolveTypeChecker(valueType)
40
- const valueSupport = resolveCodecSupport(valueType)
40
+ const valueChecker = resolveTypeChecker(resolvedValueType)
41
+ const valueSupport = resolveCodecSupport(resolvedValueType)
41
42
  const storedValueChecker = resolveTypeChecker(valueSupport.storedType)
42
43
 
43
44
  const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) =>
@@ -81,34 +82,43 @@ export function typesArraySet<T extends AnyType>(valueType: T): ModelType<ArrayS
81
82
  return resolvedTc.snapshotType(obj) ? thisTc : null
82
83
  },
83
84
 
84
- (sn: { items: unknown[] }) => {
85
- const items = withErrorPathSegment("items", () =>
86
- sn.items.map((v, i) =>
87
- withErrorPathSegment(i, () => storedValueChecker.fromSnapshotProcessor(v))
88
- )
89
- )
90
-
91
- return {
92
- ...sn,
93
- [modelTypeKey]: modelInfo.name,
94
- items,
95
- }
96
- },
97
-
98
- (sn: { items: unknown[]; [modelTypeKey]?: string }) => {
99
- const items = withErrorPathSegment("items", () =>
100
- sn.items.map((v, i) =>
101
- withErrorPathSegment(i, () => storedValueChecker.toSnapshotProcessor(v))
102
- )
103
- )
104
-
105
- const snCopy = {
106
- ...sn,
107
- items,
108
- }
109
-
110
- return snCopy
111
- }
85
+ snapshotProcessorPlan(
86
+ () => [storedValueChecker],
87
+ ([itemProcessor]) =>
88
+ (sn: { items: unknown[] }) => {
89
+ const items = itemProcessor
90
+ ? withErrorPathSegment("items", () =>
91
+ sn.items.map((v, i) => withErrorPathSegment(i, () => itemProcessor(v)))
92
+ )
93
+ : sn.items
94
+
95
+ return {
96
+ ...sn,
97
+ [modelTypeKey]: modelInfo.name,
98
+ items,
99
+ }
100
+ },
101
+ true
102
+ ),
103
+
104
+ snapshotProcessorPlan(
105
+ () => [storedValueChecker],
106
+ ([itemProcessor]) =>
107
+ itemProcessor
108
+ ? (sn: { items: unknown[]; [modelTypeKey]?: string }) => {
109
+ const items = withErrorPathSegment("items", () =>
110
+ sn.items.map((v, i) => withErrorPathSegment(i, () => itemProcessor(v)))
111
+ )
112
+
113
+ const snCopy = {
114
+ ...sn,
115
+ items,
116
+ }
117
+
118
+ return snCopy
119
+ }
120
+ : undefined
121
+ )
112
122
  )
113
123
 
114
124
  return thisTc
@@ -12,10 +12,15 @@ import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo
12
12
  import { noDefaultValue } from "../../modelShared/prop"
13
13
  import { failure, lazy } from "../../utils"
14
14
  import { getTypeInfo } from "../getTypeInfo"
15
- import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker"
15
+ import {
16
+ registerStandardTypeResolver,
17
+ resolveStandardType,
18
+ resolveTypeChecker,
19
+ } from "../resolveTypeChecker"
16
20
  import type { AnyStandardType, IdentityType } from "../schemas"
17
21
  import {
18
22
  lateTypeChecker,
23
+ snapshotProcessorPlan,
19
24
  TypeChecker,
20
25
  TypeCheckerBaseType,
21
26
  TypeInfo,
@@ -71,19 +76,19 @@ export function typesDataModelData<M = never, K = M>(
71
76
  }
72
77
 
73
78
  const typeInfoGen: TypeInfoGen = (t) => new DataModelDataTypeInfo(t, modelClazz)
74
-
79
+ const dataType = getDataModelMetadata(modelClazz).dataType
80
+ const standardDataType = dataType ? resolveStandardType(dataType) : undefined
75
81
  const tc = lateTypeChecker(() => {
76
82
  const modelInfo = modelInfoByClass.get(modelClazz)!
77
83
  const typeName = `DataModelData(${modelInfo.name})`
78
84
 
79
- const dataTypeChecker = getDataModelMetadata(modelClazz).dataType
80
- if (!dataTypeChecker) {
85
+ if (!standardDataType) {
81
86
  throw failure(
82
87
  `type checking cannot be performed over data model data of type '${modelInfo.name}' since that model type has no data type declared, consider adding a data type or using types.unchecked() instead`
83
88
  )
84
89
  }
85
90
 
86
- const resolvedDataTypeChecker = resolveTypeChecker(dataTypeChecker)
91
+ const resolvedDataTypeChecker = resolveTypeChecker(standardDataType)
87
92
 
88
93
  const thisTc: TypeChecker = new TypeChecker(
89
94
  TypeCheckerBaseType.Object,
@@ -99,13 +104,15 @@ export function typesDataModelData<M = never, K = M>(
99
104
  return resolvedDataTypeChecker.snapshotType(value) ? thisTc : null
100
105
  },
101
106
 
102
- (sn: Record<string, unknown>) => {
103
- return resolvedDataTypeChecker.fromSnapshotProcessor(sn)
104
- },
107
+ snapshotProcessorPlan(
108
+ () => [resolvedDataTypeChecker],
109
+ ([processor]) => processor
110
+ ),
105
111
 
106
- (sn: Record<string, unknown>) => {
107
- return resolvedDataTypeChecker.toSnapshotProcessor(sn)
108
- }
112
+ snapshotProcessorPlan(
113
+ () => [resolvedDataTypeChecker],
114
+ ([processor]) => processor
115
+ )
109
116
  )
110
117
 
111
118
  return thisTc
@@ -122,7 +122,7 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
122
122
  }
123
123
  },
124
124
 
125
- (sn) => sn
125
+ undefined
126
126
  )
127
127
 
128
128
  return thisTc
@@ -1,9 +1,10 @@
1
+ import { isObservableObject, keys } from "mobx"
1
2
  import type { O } from "ts-toolbelt"
2
3
  import { Frozen } from "../../frozen/Frozen"
3
4
  import type { Path } from "../../parent/pathTypes"
4
- import { assertIsFunction, assertIsObject, isObject, lazy } from "../../utils"
5
+ import { assertIsFunction, assertIsObject, getMobxVersion, isObject, lazy } from "../../utils"
5
6
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
6
- import { createPerEntryCachedCheck } from "../createPerEntryCachedCheck"
7
+ import { createIndexedPerEntryCachedCheck } from "../createCachedTypeCheck"
7
8
  import { getTypeInfo } from "../getTypeInfo"
8
9
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
9
10
  import type {
@@ -17,6 +18,8 @@ import { TypeCheckError } from "../TypeCheckError"
17
18
  import {
18
19
  type LateTypeChecker,
19
20
  lateTypeChecker,
21
+ type SnapshotProcessor,
22
+ snapshotProcessorPlan,
20
23
  TypeChecker,
21
24
  TypeCheckerBaseType,
22
25
  TypeInfo,
@@ -24,6 +27,8 @@ import {
24
27
  } from "../TypeChecker"
25
28
  import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
26
29
 
30
+ const needsObservableObjectKeyTracking = getMobxVersion() < 5
31
+
27
32
  function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGen): S {
28
33
  assertIsFunction(objFn, "objFn")
29
34
 
@@ -33,6 +38,7 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
33
38
 
34
39
  type SchemaEntry = Readonly<{
35
40
  propName: string
41
+ processorIndex: number
36
42
  getResolvedChecker: () => TypeChecker
37
43
  }>
38
44
  type CheckedSchemaEntry = Readonly<{
@@ -41,10 +47,11 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
41
47
  }>
42
48
 
43
49
  const schemaEntries: ReadonlyArray<SchemaEntry> = Object.entries(objectSchema).map(
44
- ([propName, unresolvedChecker]) => {
50
+ ([propName, unresolvedChecker], processorIndex) => {
45
51
  let resolvedChecker: TypeChecker | undefined
46
52
  return {
47
53
  propName,
54
+ processorIndex,
48
55
  getResolvedChecker: () => {
49
56
  if (!resolvedChecker) {
50
57
  resolvedChecker = resolveTypeChecker(unresolvedChecker)
@@ -95,7 +102,10 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
95
102
  return `{ ${propsMsg.join(" ")} }`
96
103
  }
97
104
 
98
- const applySnapshotProcessor = (obj: Record<string, unknown>, mode: "from" | "to") => {
105
+ const applySnapshotProcessor = (
106
+ obj: Record<string, unknown>,
107
+ processors: ReadonlyArray<SnapshotProcessor | undefined>
108
+ ) => {
99
109
  const newObj: typeof obj = {}
100
110
 
101
111
  // note: we allow excess properties when checking objects
@@ -104,10 +114,8 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
104
114
  const k = keys[i]
105
115
  const schemaEntry = schemaEntryByPropName[k]
106
116
  if (schemaEntry) {
107
- const tc = schemaEntry.getResolvedChecker()
108
- newObj[k] = withErrorPathSegment(k, () =>
109
- mode === "from" ? tc.fromSnapshotProcessor(obj[k]) : tc.toSnapshotProcessor(obj[k])
110
- )
117
+ const processor = processors[schemaEntry.processorIndex]
118
+ newObj[k] = processor ? withErrorPathSegment(k, () => processor(obj[k])) : obj[k]
111
119
  } else {
112
120
  // unknown prop, copy as is
113
121
  newObj[k] = obj[k]
@@ -130,17 +138,26 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
130
138
  return null
131
139
  }
132
140
 
133
- // No setupCachePruning needed: iterates fixed schemaEntries, not dynamic object keys,
134
- // so entries never become stale.
135
- const checkObjectProps = createPerEntryCachedCheck<number>(
136
- (_obj, checkEntry) => {
137
- const entries = checkedSchemaEntries()
138
- for (let i = 0; i < entries.length; i++) {
139
- const error = checkEntry(i)
140
- if (error) return error
141
- }
142
- return null
143
- },
141
+ const iterateObjectProps = (
142
+ obj: any,
143
+ checkEntry: (entryIndex: number) => TypeCheckError | null
144
+ ): TypeCheckError | null => {
145
+ const entries = checkedSchemaEntries()
146
+ // MobX 4 does not track reads of absent properties. Newer versions can
147
+ // avoid the extra key-set read.
148
+ if (needsObservableObjectKeyTracking && isObservableObject(obj)) {
149
+ void keys(obj)
150
+ }
151
+
152
+ for (let i = 0; i < entries.length; i++) {
153
+ const error = checkEntry(i)
154
+ if (error) return error
155
+ }
156
+ return null
157
+ }
158
+
159
+ const checkObjectProps = createIndexedPerEntryCachedCheck(
160
+ iterateObjectProps,
144
161
  (obj, entryIndex, path, typeCheckedValue) => {
145
162
  const entry = checkedSchemaEntries()[entryIndex]
146
163
  if (!entry) {
@@ -189,13 +206,21 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
189
206
  return thisTc
190
207
  },
191
208
 
192
- (obj: Record<string, unknown>) => {
193
- return applySnapshotProcessor(obj, "from")
194
- },
195
-
196
- (obj: Record<string, unknown>) => {
197
- return applySnapshotProcessor(obj, "to")
198
- }
209
+ snapshotProcessorPlan(
210
+ () => schemaEntries.map((entry) => entry.getResolvedChecker()),
211
+ (processors) =>
212
+ processors.some(Boolean)
213
+ ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processors)
214
+ : undefined
215
+ ),
216
+
217
+ snapshotProcessorPlan(
218
+ () => schemaEntries.map((entry) => entry.getResolvedChecker()),
219
+ (processors) =>
220
+ processors.some(Boolean)
221
+ ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processors)
222
+ : undefined
223
+ )
199
224
  )
200
225
 
201
226
  return thisTc
@@ -223,6 +248,9 @@ export function typesObject<T>(objectFunction: T): T {
223
248
  // we can't type this function or else we won't be able to make it work recursively
224
249
  const typeInfoGen: TypeInfoGen = (t) => new ObjectTypeInfo(t, objectFunction as any)
225
250
 
251
+ // The schema callback cannot be evaluated here because it may reference the
252
+ // object type recursively. Keep the unresolved capability conservative; the
253
+ // concrete checker still omits processors when every schema entry is identity-only.
226
254
  return typesObjectHelper(objectFunction, false, typeInfoGen) as any
227
255
  }
228
256
 
@@ -281,12 +309,13 @@ export class ObjectTypeInfo extends TypeInfo {
281
309
  * @returns
282
310
  */
283
311
  export function typesFrozen<T extends AnyType>(dataType: T): ModelType<Frozen<TypeToData<T>>> {
312
+ const resolvedDataType = resolveStandardType(dataType)
284
313
  return typesObjectHelper(
285
314
  () => ({
286
- data: dataType,
315
+ data: resolvedDataType,
287
316
  }),
288
317
  true,
289
- (t) => new FrozenTypeInfo(t, resolveStandardType(dataType))
318
+ (t) => new FrozenTypeInfo(t, resolvedDataType)
290
319
  ) as any
291
320
  }
292
321
 
@@ -9,6 +9,7 @@ import type { AnyStandardType, AnyType, ModelType, TypeToData } from "../schemas
9
9
  import { TypeCheckError } from "../TypeCheckError"
10
10
  import {
11
11
  lateTypeChecker,
12
+ snapshotProcessorPlan,
12
13
  TypeChecker,
13
14
  TypeCheckerBaseType,
14
15
  TypeInfo,
@@ -33,13 +34,13 @@ import { typesRecord } from "./typesRecord"
33
34
  export function typesObjectMap<T extends AnyType>(
34
35
  valueType: T
35
36
  ): ModelType<ObjectMap<TypeToData<T>>> {
36
- const typeInfoGen: TypeInfoGen = (t) => new ObjectMapTypeInfo(t, resolveStandardType(valueType))
37
-
37
+ const resolvedValueType = resolveStandardType(valueType)
38
+ const typeInfoGen: TypeInfoGen = (t) => new ObjectMapTypeInfo(t, resolvedValueType)
38
39
  return lateTypeChecker(() => {
39
40
  const modelInfo = modelInfoByClass.get(ObjectMap)!
40
41
 
41
- const valueChecker = resolveTypeChecker(valueType)
42
- const valueSupport = resolveCodecSupport(valueType)
42
+ const valueChecker = resolveTypeChecker(resolvedValueType)
43
+ const valueSupport = resolveCodecSupport(resolvedValueType)
43
44
  const storedValueChecker = resolveTypeChecker(valueSupport.storedType)
44
45
 
45
46
  const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) =>
@@ -82,42 +83,52 @@ export function typesObjectMap<T extends AnyType>(
82
83
  return resolvedDataTypeChecker.snapshotType(obj) ? thisTc : null
83
84
  },
84
85
 
85
- (sn: { items: Record<string, unknown> }) => {
86
- const newItems: (typeof sn)["items"] = {}
87
-
88
- withErrorPathSegment("items", () => {
89
- for (const k of Object.keys(sn.items)) {
90
- newItems[k] = withErrorPathSegment(k, () =>
91
- storedValueChecker.fromSnapshotProcessor(sn.items[k])
92
- )
93
- }
94
- })
95
-
96
- return {
97
- ...sn,
98
- [modelTypeKey]: modelInfo.name,
99
- items: newItems,
100
- }
101
- },
102
-
103
- (sn: { items: Record<string, unknown>; [modelTypeKey]?: string }) => {
104
- const newItems: (typeof sn)["items"] = {}
105
-
106
- withErrorPathSegment("items", () => {
107
- for (const k of Object.keys(sn.items)) {
108
- newItems[k] = withErrorPathSegment(k, () =>
109
- storedValueChecker.toSnapshotProcessor(sn.items[k])
110
- )
111
- }
112
- })
113
-
114
- const snCopy = {
115
- ...sn,
116
- items: newItems,
117
- }
118
-
119
- return snCopy
120
- }
86
+ snapshotProcessorPlan(
87
+ () => [storedValueChecker],
88
+ ([itemProcessor]) =>
89
+ (sn: { items: Record<string, unknown> }) => {
90
+ let items = sn.items
91
+
92
+ if (itemProcessor) {
93
+ items = {}
94
+ withErrorPathSegment("items", () => {
95
+ for (const k of Object.keys(sn.items)) {
96
+ items[k] = withErrorPathSegment(k, () => itemProcessor(sn.items[k]))
97
+ }
98
+ })
99
+ }
100
+
101
+ return {
102
+ ...sn,
103
+ [modelTypeKey]: modelInfo.name,
104
+ items,
105
+ }
106
+ },
107
+ true
108
+ ),
109
+
110
+ snapshotProcessorPlan(
111
+ () => [storedValueChecker],
112
+ ([itemProcessor]) =>
113
+ itemProcessor
114
+ ? (sn: { items: Record<string, unknown>; [modelTypeKey]?: string }) => {
115
+ const newItems: (typeof sn)["items"] = {}
116
+
117
+ withErrorPathSegment("items", () => {
118
+ for (const k of Object.keys(sn.items)) {
119
+ newItems[k] = withErrorPathSegment(k, () => itemProcessor(sn.items[k]))
120
+ }
121
+ })
122
+
123
+ const snCopy = {
124
+ ...sn,
125
+ items: newItems,
126
+ }
127
+
128
+ return snCopy
129
+ }
130
+ : undefined
131
+ )
121
132
  )
122
133
 
123
134
  return thisTc