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,806 +1,806 @@
1
- import { isObservableArray, isObservableObject, remove } from "mobx"
2
- import type { ModelPropTransform } from "../../modelShared/prop"
3
- import { isArray, isObject, lazy } from "../../utils"
4
- import { setIfDifferent } from "../../utils/setIfDifferent"
5
- import { ArrayTypeInfo, typesArray } from "../arrayBased/typesArray"
6
- import { TupleTypeInfo, typesTuple } from "../arrayBased/typesTuple"
7
- import { getTypeInfo } from "../getTypeInfo"
8
- import { isDataModelDataStandardType } from "../objectBased/typesDataModelData"
9
- import { isModelStandardType } from "../objectBased/typesModel"
10
- import { ObjectTypeInfo, typesObject } from "../objectBased/typesObject"
11
- import { RecordTypeInfo, typesRecord } from "../objectBased/typesRecord"
12
- import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
13
- import type { AnyStandardType, AnyType } from "../schemas"
14
- import { lateTypeChecker } from "../TypeChecker"
15
- import {
16
- getCodecMetadata,
17
- identityRuntimeAdapter,
18
- type ResolvedCodecSupport,
19
- type RuntimeAdapter,
20
- } from "./typesCodecCore"
21
- import { OrTypeInfo, typesOr } from "./typesOr"
22
- import { RefinementTypeInfo } from "./typesRefinement"
23
- import { SkipCheckTypeInfo, typesSkipCheck } from "./typesSkipCheck"
24
- import { TagTypeInfo, typesTag } from "./typesTag"
25
-
26
- const codecSupportCache = new WeakMap<AnyStandardType, ResolvedCodecSupport>()
27
-
28
- function getArrayIndex(prop: PropertyKey): number | undefined {
29
- // Proxy get/set traps always receive string|symbol keys (ECMAScript ToPropertyKey),
30
- // so we only need to check for string numeric indices.
31
- if (typeof prop === "string" && /^(0|[1-9]\d*)$/.test(prop)) {
32
- return Number(prop)
33
- }
34
-
35
- return undefined
36
- }
37
-
38
- function normalizeArrayIndex(index: number, length: number): number {
39
- if (index < 0) {
40
- return Math.max(length + index, 0)
41
- }
42
-
43
- return Math.min(index, length)
44
- }
45
-
46
- function resolveArrayAtIndex(index: number, length: number): number | undefined {
47
- const resolvedIndex = index < 0 ? length + index : index
48
-
49
- if (resolvedIndex < 0 || resolvedIndex >= length) {
50
- return undefined
51
- }
52
-
53
- return resolvedIndex
54
- }
55
-
56
- /**
57
- * Common proxy traps shared between object and record runtime adapters.
58
- * Only deleteProperty needs special handling for MobX observable objects.
59
- */
60
- const objectRecordProxyTraps: Pick<ProxyHandler<Record<string, unknown>>, "deleteProperty"> = {
61
- deleteProperty(target, prop) {
62
- if (typeof prop === "string" && isObservableObject(target)) {
63
- remove(target, prop)
64
- return true
65
- }
66
-
67
- return Reflect.deleteProperty(target, prop)
68
- },
69
- }
70
-
71
- /**
72
- * Converts a runtime object to its stored form using a cache.
73
- * Shared by object and record adapters.
74
- */
75
- function objectLikeToStored(
76
- runtime: unknown,
77
- storedByRuntime: WeakMap<object, Record<string, unknown>>,
78
- convertValue: (key: string, value: unknown) => unknown
79
- ): Record<string, unknown> {
80
- if (isObject(runtime)) {
81
- const cachedStored = storedByRuntime.get(runtime)
82
- if (cachedStored) {
83
- return cachedStored
84
- }
85
-
86
- if (isArray(runtime)) {
87
- return runtime as never
88
- }
89
- } else {
90
- return runtime as never
91
- }
92
-
93
- const stored: Record<string, unknown> = {}
94
- for (const key of Object.keys(runtime)) {
95
- stored[key] = convertValue(key, runtime[key])
96
- }
97
- return stored
98
- }
99
-
100
- /**
101
- * Checks the stored cache for a runtime object.
102
- * Shared by object and record adapters.
103
- */
104
- function objectLikeToStoredIfCached(
105
- runtime: unknown,
106
- storedByRuntime: WeakMap<object, Record<string, unknown>>
107
- ): { found: true; value: Record<string, unknown> } | { found: false } {
108
- if (isObject(runtime) && !isArray(runtime)) {
109
- const cachedStored = storedByRuntime.get(runtime)
110
- if (cachedStored) {
111
- return { found: true, value: cachedStored }
112
- }
113
- }
114
-
115
- return { found: false }
116
- }
117
-
118
- function createObjectRuntimeAdapter(
119
- getChildSupports: () => Record<string, ResolvedCodecSupport>
120
- ): RuntimeAdapter<Record<string, unknown>, Record<string, unknown>> {
121
- const runtimeByStored = new WeakMap<object, Record<string, unknown>>()
122
- const storedByRuntime = new WeakMap<object, Record<string, unknown>>()
123
-
124
- return {
125
- toRuntime(stored) {
126
- if (!isObject(stored) || isArray(stored)) {
127
- return stored as never
128
- }
129
-
130
- const cachedRuntime = runtimeByStored.get(stored)
131
- if (cachedRuntime) {
132
- return cachedRuntime
133
- }
134
-
135
- const proxy = new Proxy(stored, {
136
- get(target, prop, receiver) {
137
- if (typeof prop === "string") {
138
- const childSupport = getChildSupports()[prop]
139
- if (childSupport && Object.hasOwn(target, prop)) {
140
- return childSupport.adapter.toRuntime(target[prop], (newStoredValue) => {
141
- target[prop] = newStoredValue
142
- })
143
- }
144
- }
145
-
146
- return Reflect.get(target, prop, receiver)
147
- },
148
-
149
- set(target, prop, value) {
150
- if (typeof prop === "string") {
151
- const childSupport = getChildSupports()[prop]
152
- if (childSupport) {
153
- setIfDifferent(target, prop, childSupport.adapter.toStored(value))
154
- return true
155
- }
156
- }
157
-
158
- Reflect.set(target, prop, value)
159
- return true
160
- },
161
-
162
- ...objectRecordProxyTraps,
163
- })
164
-
165
- runtimeByStored.set(stored, proxy)
166
- storedByRuntime.set(proxy, stored)
167
-
168
- return proxy
169
- },
170
-
171
- toStored(runtime) {
172
- const childSupports = getChildSupports()
173
- return objectLikeToStored(runtime, storedByRuntime, (key, value) => {
174
- const childSupport = childSupports[key]
175
- return childSupport ? childSupport.adapter.toStored(value) : value
176
- })
177
- },
178
-
179
- toStoredIfCached(runtime) {
180
- return objectLikeToStoredIfCached(runtime, storedByRuntime)
181
- },
182
- }
183
- }
184
-
185
- function createRecordRuntimeAdapter(
186
- getValueSupport: () => ResolvedCodecSupport
187
- ): RuntimeAdapter<Record<string, unknown>, Record<string, unknown>> {
188
- const runtimeByStored = new WeakMap<object, Record<string, unknown>>()
189
- const storedByRuntime = new WeakMap<object, Record<string, unknown>>()
190
-
191
- return {
192
- toRuntime(stored) {
193
- if (!isObject(stored) || isArray(stored)) {
194
- return stored as never
195
- }
196
-
197
- const cachedRuntime = runtimeByStored.get(stored)
198
- if (cachedRuntime) {
199
- return cachedRuntime
200
- }
201
-
202
- const proxy = new Proxy(stored, {
203
- get(target, prop, receiver) {
204
- if (typeof prop === "string" && Object.hasOwn(target, prop)) {
205
- const valueSupport = getValueSupport()
206
- return valueSupport.adapter.toRuntime(target[prop], (newStoredValue) => {
207
- target[prop] = newStoredValue
208
- })
209
- }
210
-
211
- return Reflect.get(target, prop, receiver)
212
- },
213
-
214
- set(target, prop, value) {
215
- if (typeof prop === "string") {
216
- setIfDifferent(target, prop, getValueSupport().adapter.toStored(value))
217
- return true
218
- }
219
-
220
- Reflect.set(target, prop, value)
221
- return true
222
- },
223
-
224
- ...objectRecordProxyTraps,
225
- })
226
-
227
- runtimeByStored.set(stored, proxy)
228
- storedByRuntime.set(proxy, stored)
229
-
230
- return proxy
231
- },
232
-
233
- toStored(runtime) {
234
- const valueSupport = getValueSupport()
235
- return objectLikeToStored(runtime, storedByRuntime, (_key, value) =>
236
- valueSupport.adapter.toStored(value)
237
- )
238
- },
239
-
240
- toStoredIfCached(runtime) {
241
- return objectLikeToStoredIfCached(runtime, storedByRuntime)
242
- },
243
- }
244
- }
245
-
246
- function createArrayLikeRuntimeAdapter(
247
- getItemAdapter: (index: number) => RuntimeAdapter<unknown, unknown>
248
- ): RuntimeAdapter<unknown[], unknown[]> {
249
- const runtimeByStored = new WeakMap<unknown[], unknown[]>()
250
- const storedByRuntime = new WeakMap<unknown[], unknown[]>()
251
-
252
- const decodeIndex = (target: unknown[], index: number) =>
253
- index < 0 || index >= target.length || !Reflect.has(target, index)
254
- ? undefined
255
- : getItemAdapter(index).toRuntime(target[index], (newStoredValue) => {
256
- target[index] = newStoredValue
257
- })
258
-
259
- const callReadOnlyRuntimeMethod = (
260
- receiver: unknown[],
261
- methodName: PropertyKey,
262
- args: unknown[]
263
- ) => {
264
- const method = Reflect.get(Array.prototype, methodName, receiver)
265
- return Reflect.apply(method, receiver, args)
266
- }
267
- const materializeRuntimeValues = (target: unknown[]) =>
268
- Array.from({ length: target.length }, (_, index) => decodeIndex(target, index))
269
- const replaceStoredValues = (target: unknown[], runtimeValues: unknown[]) => {
270
- target.splice(
271
- 0,
272
- target.length,
273
- ...runtimeValues.map((value, index) => getItemAdapter(index).toStored(value))
274
- )
275
- }
276
- const mutateViaRuntime = (
277
- target: unknown[],
278
- receiver: unknown[],
279
- fn: (values: unknown[]) => void
280
- ) => {
281
- const runtimeValues = materializeRuntimeValues(target)
282
- fn(runtimeValues)
283
- replaceStoredValues(target, runtimeValues)
284
- return receiver
285
- }
286
-
287
- return {
288
- toRuntime(stored) {
289
- if (!isArray(stored)) {
290
- return stored as never
291
- }
292
-
293
- const cachedRuntime = runtimeByStored.get(stored)
294
- if (cachedRuntime) {
295
- return cachedRuntime
296
- }
297
-
298
- const proxy: unknown[] = new Proxy(stored, {
299
- get(target, prop, receiver) {
300
- const index = getArrayIndex(prop)
301
- if (index !== undefined) {
302
- return decodeIndex(target, index)
303
- }
304
-
305
- // MobX 4 observable arrays are array-like but fail `Array.isArray`, so native
306
- // `Array.prototype.concat` won't spread this proxy unless we opt in explicitly.
307
- if (
308
- prop === Symbol.isConcatSpreadable &&
309
- isObservableArray(target) &&
310
- !Array.isArray(target)
311
- ) {
312
- return true
313
- }
314
-
315
- switch (prop) {
316
- case "at":
317
- return (indexValue: number) => {
318
- const resolvedIndex = resolveArrayAtIndex(indexValue, target.length)
319
- return resolvedIndex === undefined ? undefined : decodeIndex(target, resolvedIndex)
320
- }
321
- case "push":
322
- return (...items: unknown[]) =>
323
- target.push(
324
- ...items.map((item, offset) =>
325
- getItemAdapter(target.length + offset).toStored(item)
326
- )
327
- )
328
- case "pop":
329
- return () => {
330
- if (target.length <= 0) {
331
- return undefined
332
- }
333
-
334
- const indexValue = target.length - 1
335
- const value = target.pop()
336
- return getItemAdapter(indexValue).toRuntime(value)
337
- }
338
- case "shift":
339
- return () => {
340
- if (target.length <= 0) {
341
- return undefined
342
- }
343
-
344
- const value = target.shift()
345
- return getItemAdapter(0).toRuntime(value)
346
- }
347
- case "unshift":
348
- return (...items: unknown[]) =>
349
- target.unshift(
350
- ...items.map((item, indexValue) => getItemAdapter(indexValue).toStored(item))
351
- )
352
- case "splice":
353
- return (start: number, deleteCount?: number, ...items: unknown[]) => {
354
- const normalizedStart = normalizeArrayIndex(start, target.length)
355
- const actualDeleteCount =
356
- deleteCount === undefined
357
- ? target.length - normalizedStart
358
- : Math.max(0, Math.min(deleteCount, target.length - normalizedStart))
359
-
360
- const removedValues = target
361
- .slice(normalizedStart, normalizedStart + actualDeleteCount)
362
- .map((value, indexValue) =>
363
- getItemAdapter(normalizedStart + indexValue).toRuntime(value)
364
- )
365
-
366
- target.splice(
367
- normalizedStart,
368
- actualDeleteCount,
369
- ...items.map((item, indexValue) =>
370
- getItemAdapter(normalizedStart + indexValue).toStored(item)
371
- )
372
- )
373
-
374
- return removedValues
375
- }
376
- case "reverse":
377
- return () => mutateViaRuntime(target, receiver, (v) => v.reverse())
378
- case "sort":
379
- return (compareFn?: (a: unknown, b: unknown) => number) =>
380
- mutateViaRuntime(target, receiver, (v) => v.sort(compareFn))
381
- case "fill":
382
- return (value: unknown, start?: number, end?: number) =>
383
- mutateViaRuntime(target, receiver, (v) => v.fill(value, start, end))
384
- case "copyWithin":
385
- return (targetIndex: number, start?: number, end?: number) =>
386
- mutateViaRuntime(target, receiver, (v) =>
387
- v.copyWithin(targetIndex, start ?? 0, end)
388
- )
389
- case "concat":
390
- case "flat":
391
- case "flatMap":
392
- case "filter":
393
- case "find":
394
- case "findIndex":
395
- case "forEach":
396
- case "every":
397
- case "includes":
398
- case "indexOf":
399
- case "join":
400
- case "lastIndexOf":
401
- case "map":
402
- case "reduce":
403
- case "reduceRight":
404
- case "slice":
405
- case "some":
406
- case "findLast":
407
- case "findLastIndex":
408
- case "toReversed":
409
- case "toSorted":
410
- case "toSpliced":
411
- case "toString":
412
- case "toLocaleString":
413
- case "with":
414
- return (...args: unknown[]) => callReadOnlyRuntimeMethod(receiver, prop, args)
415
- case "entries":
416
- return function* () {
417
- for (let i = 0; i < target.length; i++) {
418
- yield [i, decodeIndex(target, i)] as const
419
- }
420
- }
421
- case "keys":
422
- return function* () {
423
- for (let i = 0; i < target.length; i++) {
424
- yield i
425
- }
426
- }
427
- case "values":
428
- case Symbol.iterator:
429
- return function* () {
430
- for (let i = 0; i < target.length; i++) {
431
- yield decodeIndex(target, i)
432
- }
433
- }
434
- default:
435
- return Reflect.get(target, prop, receiver)
436
- }
437
- },
438
-
439
- set(target, prop, value) {
440
- const index = getArrayIndex(prop)
441
- if (index !== undefined) {
442
- target[index] = getItemAdapter(index).toStored(value)
443
- return true
444
- }
445
-
446
- Reflect.set(target, prop, value)
447
- return true
448
- },
449
- })
450
-
451
- runtimeByStored.set(stored, proxy)
452
- storedByRuntime.set(proxy, stored)
453
-
454
- return proxy
455
- },
456
-
457
- toStored(runtime) {
458
- if (isArray(runtime)) {
459
- const cachedStored = storedByRuntime.get(runtime)
460
- if (cachedStored) {
461
- return cachedStored
462
- }
463
-
464
- return Array.from(runtime, (value, index) => getItemAdapter(index).toStored(value))
465
- }
466
-
467
- return runtime as never
468
- },
469
-
470
- toStoredIfCached(runtime) {
471
- if (isArray(runtime)) {
472
- const cachedStored = storedByRuntime.get(runtime)
473
- if (cachedStored) {
474
- return { found: true, value: cachedStored }
475
- }
476
- }
477
-
478
- return { found: false }
479
- },
480
- }
481
- }
482
-
483
- function createCodecLeafRuntimeAdapter(
484
- transform: ModelPropTransform<unknown, unknown>
485
- ): RuntimeAdapter<unknown, unknown> {
486
- const runtimeByStored = new WeakMap<object, unknown>()
487
- const storedByRuntime = new WeakMap<object, unknown>()
488
-
489
- return {
490
- toRuntime(stored, setStored) {
491
- const cachedRuntime = isObject(stored) ? runtimeByStored.get(stored) : undefined
492
-
493
- const runtime = transform.transform({
494
- originalValue: stored,
495
- cachedTransformedValue: cachedRuntime,
496
- setOriginalValue(newStoredValue) {
497
- if (setStored) {
498
- setStored(newStoredValue)
499
- }
500
- },
501
- })
502
-
503
- if (isObject(runtime)) {
504
- storedByRuntime.set(runtime, stored)
505
- }
506
-
507
- if (isObject(stored)) {
508
- runtimeByStored.set(stored, runtime)
509
- }
510
-
511
- return runtime
512
- },
513
-
514
- toStored(runtime) {
515
- const cachedStored = isObject(runtime) ? storedByRuntime.get(runtime) : undefined
516
- if (cachedStored !== undefined) {
517
- return cachedStored
518
- }
519
-
520
- const stored = transform.untransform({
521
- transformedValue: runtime,
522
- cacheTransformedValue: () => {},
523
- })
524
-
525
- if (isObject(runtime)) {
526
- storedByRuntime.set(runtime, stored)
527
- }
528
-
529
- if (isObject(stored)) {
530
- runtimeByStored.set(stored, runtime)
531
- }
532
-
533
- return stored
534
- },
535
-
536
- toStoredIfCached(runtime) {
537
- if (isObject(runtime)) {
538
- const cachedStored = storedByRuntime.get(runtime)
539
- if (cachedStored !== undefined) {
540
- return { found: true, value: cachedStored }
541
- }
542
- }
543
-
544
- return { found: false }
545
- },
546
- }
547
- }
548
-
549
- function createOrRuntimeAdapter(
550
- childTypes: ReadonlyArray<AnyStandardType>,
551
- getChildSupports: () => ReadonlyArray<ResolvedCodecSupport>,
552
- dispatcher: OrTypeInfo["dispatcher"]
553
- ): RuntimeAdapter<unknown, unknown> {
554
- const getRuntimeSupportForValue = (value: unknown) => {
555
- const supports = getChildSupports()
556
- for (let i = 0; i < supports.length; i++) {
557
- const childType = childTypes[i]
558
- const tc = resolveTypeChecker(childType)
559
- if (tc.skipCheck) {
560
- // For skipCheck branches, use snapshotType for structural matching
561
- // instead of check (which always passes and would match any value)
562
- if (tc.snapshotType(value)) {
563
- return supports[i]
564
- }
565
- } else if (!tc.check(value, [], value)) {
566
- return supports[i]
567
- }
568
- }
569
-
570
- return supports[0]
571
- }
572
-
573
- const getStoredSupportForValue = (value: unknown) => {
574
- if (dispatcher) {
575
- return resolveCodecSupport(dispatcher(value))
576
- }
577
-
578
- const supports = getChildSupports()
579
- for (let i = 0; i < supports.length; i++) {
580
- const storedTypeChecker = resolveTypeChecker(supports[i].storedType)
581
- if (storedTypeChecker.snapshotType(value)) {
582
- return supports[i]
583
- }
584
- }
585
-
586
- return supports[0]
587
- }
588
-
589
- return {
590
- toRuntime(stored, setStored) {
591
- return getStoredSupportForValue(stored).adapter.toRuntime(stored, setStored)
592
- },
593
-
594
- toStored(runtime) {
595
- return getRuntimeSupportForValue(runtime).adapter.toStored(runtime)
596
- },
597
-
598
- toStoredIfCached(runtime) {
599
- return getRuntimeSupportForValue(runtime).adapter.toStoredIfCached(runtime)
600
- },
601
- }
602
- }
603
-
604
- function noCodecSupport(type: AnyStandardType): ResolvedCodecSupport {
605
- return { hasCodec: false, storedType: type, adapter: identityRuntimeAdapter }
606
- }
607
-
608
- function resolveCodecSupportForStandardType(type: AnyStandardType): ResolvedCodecSupport {
609
- const cachedSupport = codecSupportCache.get(type)
610
- if (cachedSupport) {
611
- return cachedSupport
612
- }
613
-
614
- let resolvedSupport: ResolvedCodecSupport | undefined
615
- let resolvedStoredType: AnyStandardType | undefined
616
-
617
- const placeholderSupport: ResolvedCodecSupport = {
618
- hasCodec: false,
619
- storedType: lateTypeChecker(
620
- () => resolveTypeChecker(resolvedStoredType ?? type),
621
- () => getTypeInfo(resolvedStoredType ?? type)
622
- ) as any,
623
- adapter: {
624
- toRuntime(stored, setStored) {
625
- return (resolvedSupport?.adapter ?? identityRuntimeAdapter).toRuntime(stored, setStored)
626
- },
627
-
628
- toStored(runtime) {
629
- return (resolvedSupport?.adapter ?? identityRuntimeAdapter).toStored(runtime)
630
- },
631
-
632
- toStoredIfCached(runtime) {
633
- return (resolvedSupport?.adapter ?? identityRuntimeAdapter).toStoredIfCached(runtime)
634
- },
635
- },
636
- }
637
-
638
- codecSupportCache.set(type, placeholderSupport)
639
-
640
- const codecMetadata = getCodecMetadata(type)
641
- if (codecMetadata) {
642
- resolvedSupport = {
643
- hasCodec: true,
644
- storedType: resolveStandardType(codecMetadata.encodedType),
645
- adapter: createCodecLeafRuntimeAdapter(codecMetadata.transform),
646
- }
647
- } else if (isModelStandardType(type) || isDataModelDataStandardType(type)) {
648
- resolvedSupport = noCodecSupport(type)
649
- } else {
650
- const typeInfo = getTypeInfo(type)
651
-
652
- if (typeInfo instanceof ArrayTypeInfo) {
653
- const itemSupport = lazy(() => resolveCodecSupportForStandardType(typeInfo.itemType))
654
-
655
- resolvedSupport = !itemSupport().hasCodec
656
- ? noCodecSupport(type)
657
- : {
658
- hasCodec: true,
659
- storedType: typesArray(itemSupport().storedType),
660
- adapter: createArrayLikeRuntimeAdapter(() => itemSupport().adapter),
661
- }
662
- } else if (typeInfo instanceof TupleTypeInfo) {
663
- const itemSupports = lazy(() =>
664
- typeInfo.itemTypes.map((itemType) => resolveCodecSupportForStandardType(itemType))
665
- )
666
-
667
- resolvedSupport = !itemSupports().some((itemSupport) => itemSupport.hasCodec)
668
- ? noCodecSupport(type)
669
- : {
670
- hasCodec: true,
671
- storedType: typesTuple(...itemSupports().map((itemSupport) => itemSupport.storedType)),
672
- adapter: createArrayLikeRuntimeAdapter(
673
- (index) => itemSupports()[index]?.adapter ?? identityRuntimeAdapter
674
- ),
675
- }
676
- } else if (typeInfo instanceof ObjectTypeInfo) {
677
- const childSupports = lazy(() => {
678
- const supports: Record<string, ResolvedCodecSupport> = {}
679
- const props = typeInfo.props
680
- for (const propName of Object.keys(props)) {
681
- supports[propName] = resolveCodecSupportForStandardType(props[propName].type)
682
- }
683
- return supports
684
- })
685
-
686
- resolvedSupport = !Object.values(childSupports()).some(
687
- (childSupport) => childSupport.hasCodec
688
- )
689
- ? noCodecSupport(type)
690
- : {
691
- hasCodec: true,
692
- storedType: typesObject(() => {
693
- const storedProps: Record<string, AnyStandardType> = {}
694
- const props = typeInfo.props
695
- for (const propName of Object.keys(props)) {
696
- storedProps[propName] = childSupports()[propName].storedType
697
- }
698
- return storedProps
699
- }),
700
- adapter: createObjectRuntimeAdapter(childSupports),
701
- }
702
- } else if (typeInfo instanceof RecordTypeInfo) {
703
- const valueSupport = lazy(() => resolveCodecSupportForStandardType(typeInfo.valueType))
704
-
705
- resolvedSupport = !valueSupport().hasCodec
706
- ? noCodecSupport(type)
707
- : {
708
- hasCodec: true,
709
- storedType: typesRecord(valueSupport().storedType),
710
- adapter: createRecordRuntimeAdapter(valueSupport),
711
- }
712
- } else if (typeInfo instanceof TagTypeInfo) {
713
- const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
714
-
715
- resolvedSupport = !baseSupport.hasCodec
716
- ? noCodecSupport(type)
717
- : {
718
- hasCodec: true,
719
- storedType: typesTag(baseSupport.storedType, typeInfo.tag, typeInfo.typeName),
720
- adapter: baseSupport.adapter,
721
- }
722
- } else if (typeInfo instanceof RefinementTypeInfo) {
723
- const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
724
-
725
- resolvedSupport = baseSupport.hasCodec ? baseSupport : noCodecSupport(type)
726
- } else if (typeInfo instanceof SkipCheckTypeInfo) {
727
- const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
728
-
729
- // Always wrap storedType with skipCheck, even when hasCodec is false.
730
- // resolveStoredType() feeds model-internal automatic validation in sharedInternalModel.ts,
731
- // so the skipCheck wrapper must propagate to prevent the stored type from being validated.
732
- resolvedSupport = {
733
- hasCodec: baseSupport.hasCodec,
734
- storedType: typesSkipCheck(baseSupport.storedType),
735
- adapter: baseSupport.adapter,
736
- }
737
- } else if (typeInfo instanceof OrTypeInfo) {
738
- const childSupports = lazy(() =>
739
- typeInfo.orTypes.map((orType) => resolveCodecSupportForStandardType(orType))
740
- )
741
-
742
- resolvedSupport = !childSupports().some((childSupport) => childSupport.hasCodec)
743
- ? noCodecSupport(type)
744
- : {
745
- hasCodec: true,
746
- storedType: typeInfo.dispatcher
747
- ? typesOr(
748
- (snapshot) => resolveStoredType(typeInfo.dispatcher!(snapshot)),
749
- ...childSupports().map((childSupport) => childSupport.storedType)
750
- )
751
- : typesOr(...childSupports().map((childSupport) => childSupport.storedType)),
752
- adapter: createOrRuntimeAdapter(typeInfo.orTypes, childSupports, typeInfo.dispatcher),
753
- }
754
- } else {
755
- resolvedSupport = noCodecSupport(type)
756
- }
757
- }
758
-
759
- resolvedStoredType = resolvedSupport.storedType
760
- placeholderSupport.hasCodec = resolvedSupport.hasCodec
761
- placeholderSupport.storedType = resolvedSupport.storedType
762
- placeholderSupport.adapter = resolvedSupport.adapter
763
-
764
- return placeholderSupport
765
- }
766
-
767
- export function resolveCodecSupport(type: AnyType) {
768
- return resolveCodecSupportForStandardType(resolveStandardType(type))
769
- }
770
-
771
- export function resolveStoredType(type: AnyType) {
772
- return resolveCodecSupport(type).storedType
773
- }
774
-
775
- export function resolveStoredTypeChecker(type: AnyType) {
776
- return resolveTypeChecker(resolveCodecSupport(type).storedType)
777
- }
778
-
779
- export function createCodecPropTransform(type: AnyType): ModelPropTransform<any, unknown> {
780
- const adapter = resolveCodecSupport(type).adapter
781
-
782
- return {
783
- transform({ originalValue, cachedTransformedValue, setOriginalValue }) {
784
- // For leaf codecs backed by primitive stored data (e.g. timestamps, ISO strings),
785
- // the adapter's WeakMap cache can't work. Thread the model's cachedTransformedValue
786
- // to avoid creating new runtime objects when the stored value hasn't changed.
787
- if (cachedTransformedValue !== undefined) {
788
- const cachedStored = adapter.toStoredIfCached(cachedTransformedValue)
789
- if (cachedStored.found && cachedStored.value === originalValue) {
790
- return cachedTransformedValue
791
- }
792
- }
793
- return adapter.toRuntime(originalValue, setOriginalValue)
794
- },
795
-
796
- untransform({ transformedValue, cacheTransformedValue }) {
797
- const cachedStored = adapter.toStoredIfCached(transformedValue)
798
- if (cachedStored.found) {
799
- cacheTransformedValue()
800
- return cachedStored.value
801
- }
802
-
803
- return adapter.toStored(transformedValue)
804
- },
805
- }
806
- }
1
+ import { isObservableArray, isObservableObject, remove } from "mobx"
2
+ import type { ModelPropTransform } from "../../modelShared/prop"
3
+ import { isArray, isObject, lazy } from "../../utils"
4
+ import { setIfDifferent } from "../../utils/setIfDifferent"
5
+ import { ArrayTypeInfo, typesArray } from "../arrayBased/typesArray"
6
+ import { TupleTypeInfo, typesTuple } from "../arrayBased/typesTuple"
7
+ import { getTypeInfo } from "../getTypeInfo"
8
+ import { isDataModelDataStandardType } from "../objectBased/typesDataModelData"
9
+ import { isModelStandardType } from "../objectBased/typesModel"
10
+ import { ObjectTypeInfo, typesObject } from "../objectBased/typesObject"
11
+ import { RecordTypeInfo, typesRecord } from "../objectBased/typesRecord"
12
+ import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
13
+ import type { AnyStandardType, AnyType } from "../schemas"
14
+ import { lateTypeChecker } from "../TypeChecker"
15
+ import {
16
+ getCodecMetadata,
17
+ identityRuntimeAdapter,
18
+ type ResolvedCodecSupport,
19
+ type RuntimeAdapter,
20
+ } from "./typesCodecCore"
21
+ import { OrTypeInfo, typesOr } from "./typesOr"
22
+ import { RefinementTypeInfo } from "./typesRefinement"
23
+ import { SkipCheckTypeInfo, typesSkipCheck } from "./typesSkipCheck"
24
+ import { TagTypeInfo, typesTag } from "./typesTag"
25
+
26
+ const codecSupportCache = new WeakMap<AnyStandardType, ResolvedCodecSupport>()
27
+
28
+ function getArrayIndex(prop: PropertyKey): number | undefined {
29
+ // Proxy get/set traps always receive string|symbol keys (ECMAScript ToPropertyKey),
30
+ // so we only need to check for string numeric indices.
31
+ if (typeof prop === "string" && /^(0|[1-9]\d*)$/.test(prop)) {
32
+ return Number(prop)
33
+ }
34
+
35
+ return undefined
36
+ }
37
+
38
+ function normalizeArrayIndex(index: number, length: number): number {
39
+ if (index < 0) {
40
+ return Math.max(length + index, 0)
41
+ }
42
+
43
+ return Math.min(index, length)
44
+ }
45
+
46
+ function resolveArrayAtIndex(index: number, length: number): number | undefined {
47
+ const resolvedIndex = index < 0 ? length + index : index
48
+
49
+ if (resolvedIndex < 0 || resolvedIndex >= length) {
50
+ return undefined
51
+ }
52
+
53
+ return resolvedIndex
54
+ }
55
+
56
+ /**
57
+ * Common proxy traps shared between object and record runtime adapters.
58
+ * Only deleteProperty needs special handling for MobX observable objects.
59
+ */
60
+ const objectRecordProxyTraps: Pick<ProxyHandler<Record<string, unknown>>, "deleteProperty"> = {
61
+ deleteProperty(target, prop) {
62
+ if (typeof prop === "string" && isObservableObject(target)) {
63
+ remove(target, prop)
64
+ return true
65
+ }
66
+
67
+ return Reflect.deleteProperty(target, prop)
68
+ },
69
+ }
70
+
71
+ /**
72
+ * Converts a runtime object to its stored form using a cache.
73
+ * Shared by object and record adapters.
74
+ */
75
+ function objectLikeToStored(
76
+ runtime: unknown,
77
+ storedByRuntime: WeakMap<object, Record<string, unknown>>,
78
+ convertValue: (key: string, value: unknown) => unknown
79
+ ): Record<string, unknown> {
80
+ if (isObject(runtime)) {
81
+ const cachedStored = storedByRuntime.get(runtime)
82
+ if (cachedStored) {
83
+ return cachedStored
84
+ }
85
+
86
+ if (isArray(runtime)) {
87
+ return runtime as never
88
+ }
89
+ } else {
90
+ return runtime as never
91
+ }
92
+
93
+ const stored: Record<string, unknown> = {}
94
+ for (const key of Object.keys(runtime)) {
95
+ stored[key] = convertValue(key, runtime[key])
96
+ }
97
+ return stored
98
+ }
99
+
100
+ /**
101
+ * Checks the stored cache for a runtime object.
102
+ * Shared by object and record adapters.
103
+ */
104
+ function objectLikeToStoredIfCached(
105
+ runtime: unknown,
106
+ storedByRuntime: WeakMap<object, Record<string, unknown>>
107
+ ): { found: true; value: Record<string, unknown> } | { found: false } {
108
+ if (isObject(runtime) && !isArray(runtime)) {
109
+ const cachedStored = storedByRuntime.get(runtime)
110
+ if (cachedStored) {
111
+ return { found: true, value: cachedStored }
112
+ }
113
+ }
114
+
115
+ return { found: false }
116
+ }
117
+
118
+ function createObjectRuntimeAdapter(
119
+ getChildSupports: () => Record<string, ResolvedCodecSupport>
120
+ ): RuntimeAdapter<Record<string, unknown>, Record<string, unknown>> {
121
+ const runtimeByStored = new WeakMap<object, Record<string, unknown>>()
122
+ const storedByRuntime = new WeakMap<object, Record<string, unknown>>()
123
+
124
+ return {
125
+ toRuntime(stored) {
126
+ if (!isObject(stored) || isArray(stored)) {
127
+ return stored as never
128
+ }
129
+
130
+ const cachedRuntime = runtimeByStored.get(stored)
131
+ if (cachedRuntime) {
132
+ return cachedRuntime
133
+ }
134
+
135
+ const proxy = new Proxy(stored, {
136
+ get(target, prop, receiver) {
137
+ if (typeof prop === "string") {
138
+ const childSupport = getChildSupports()[prop]
139
+ if (childSupport && Object.hasOwn(target, prop)) {
140
+ return childSupport.adapter.toRuntime(target[prop], (newStoredValue) => {
141
+ target[prop] = newStoredValue
142
+ })
143
+ }
144
+ }
145
+
146
+ return Reflect.get(target, prop, receiver)
147
+ },
148
+
149
+ set(target, prop, value) {
150
+ if (typeof prop === "string") {
151
+ const childSupport = getChildSupports()[prop]
152
+ if (childSupport) {
153
+ setIfDifferent(target, prop, childSupport.adapter.toStored(value))
154
+ return true
155
+ }
156
+ }
157
+
158
+ Reflect.set(target, prop, value)
159
+ return true
160
+ },
161
+
162
+ ...objectRecordProxyTraps,
163
+ })
164
+
165
+ runtimeByStored.set(stored, proxy)
166
+ storedByRuntime.set(proxy, stored)
167
+
168
+ return proxy
169
+ },
170
+
171
+ toStored(runtime) {
172
+ const childSupports = getChildSupports()
173
+ return objectLikeToStored(runtime, storedByRuntime, (key, value) => {
174
+ const childSupport = childSupports[key]
175
+ return childSupport ? childSupport.adapter.toStored(value) : value
176
+ })
177
+ },
178
+
179
+ toStoredIfCached(runtime) {
180
+ return objectLikeToStoredIfCached(runtime, storedByRuntime)
181
+ },
182
+ }
183
+ }
184
+
185
+ function createRecordRuntimeAdapter(
186
+ getValueSupport: () => ResolvedCodecSupport
187
+ ): RuntimeAdapter<Record<string, unknown>, Record<string, unknown>> {
188
+ const runtimeByStored = new WeakMap<object, Record<string, unknown>>()
189
+ const storedByRuntime = new WeakMap<object, Record<string, unknown>>()
190
+
191
+ return {
192
+ toRuntime(stored) {
193
+ if (!isObject(stored) || isArray(stored)) {
194
+ return stored as never
195
+ }
196
+
197
+ const cachedRuntime = runtimeByStored.get(stored)
198
+ if (cachedRuntime) {
199
+ return cachedRuntime
200
+ }
201
+
202
+ const proxy = new Proxy(stored, {
203
+ get(target, prop, receiver) {
204
+ if (typeof prop === "string" && Object.hasOwn(target, prop)) {
205
+ const valueSupport = getValueSupport()
206
+ return valueSupport.adapter.toRuntime(target[prop], (newStoredValue) => {
207
+ target[prop] = newStoredValue
208
+ })
209
+ }
210
+
211
+ return Reflect.get(target, prop, receiver)
212
+ },
213
+
214
+ set(target, prop, value) {
215
+ if (typeof prop === "string") {
216
+ setIfDifferent(target, prop, getValueSupport().adapter.toStored(value))
217
+ return true
218
+ }
219
+
220
+ Reflect.set(target, prop, value)
221
+ return true
222
+ },
223
+
224
+ ...objectRecordProxyTraps,
225
+ })
226
+
227
+ runtimeByStored.set(stored, proxy)
228
+ storedByRuntime.set(proxy, stored)
229
+
230
+ return proxy
231
+ },
232
+
233
+ toStored(runtime) {
234
+ const valueSupport = getValueSupport()
235
+ return objectLikeToStored(runtime, storedByRuntime, (_key, value) =>
236
+ valueSupport.adapter.toStored(value)
237
+ )
238
+ },
239
+
240
+ toStoredIfCached(runtime) {
241
+ return objectLikeToStoredIfCached(runtime, storedByRuntime)
242
+ },
243
+ }
244
+ }
245
+
246
+ function createArrayLikeRuntimeAdapter(
247
+ getItemAdapter: (index: number) => RuntimeAdapter<unknown, unknown>
248
+ ): RuntimeAdapter<unknown[], unknown[]> {
249
+ const runtimeByStored = new WeakMap<unknown[], unknown[]>()
250
+ const storedByRuntime = new WeakMap<unknown[], unknown[]>()
251
+
252
+ const decodeIndex = (target: unknown[], index: number) =>
253
+ index < 0 || index >= target.length || !Reflect.has(target, index)
254
+ ? undefined
255
+ : getItemAdapter(index).toRuntime(target[index], (newStoredValue) => {
256
+ target[index] = newStoredValue
257
+ })
258
+
259
+ const callReadOnlyRuntimeMethod = (
260
+ receiver: unknown[],
261
+ methodName: PropertyKey,
262
+ args: unknown[]
263
+ ) => {
264
+ const method = Reflect.get(Array.prototype, methodName, receiver)
265
+ return Reflect.apply(method, receiver, args)
266
+ }
267
+ const materializeRuntimeValues = (target: unknown[]) =>
268
+ Array.from({ length: target.length }, (_, index) => decodeIndex(target, index))
269
+ const replaceStoredValues = (target: unknown[], runtimeValues: unknown[]) => {
270
+ target.splice(
271
+ 0,
272
+ target.length,
273
+ ...runtimeValues.map((value, index) => getItemAdapter(index).toStored(value))
274
+ )
275
+ }
276
+ const mutateViaRuntime = (
277
+ target: unknown[],
278
+ receiver: unknown[],
279
+ fn: (values: unknown[]) => void
280
+ ) => {
281
+ const runtimeValues = materializeRuntimeValues(target)
282
+ fn(runtimeValues)
283
+ replaceStoredValues(target, runtimeValues)
284
+ return receiver
285
+ }
286
+
287
+ return {
288
+ toRuntime(stored) {
289
+ if (!isArray(stored)) {
290
+ return stored as never
291
+ }
292
+
293
+ const cachedRuntime = runtimeByStored.get(stored)
294
+ if (cachedRuntime) {
295
+ return cachedRuntime
296
+ }
297
+
298
+ const proxy: unknown[] = new Proxy(stored, {
299
+ get(target, prop, receiver) {
300
+ const index = getArrayIndex(prop)
301
+ if (index !== undefined) {
302
+ return decodeIndex(target, index)
303
+ }
304
+
305
+ // MobX 4 observable arrays are array-like but fail `Array.isArray`, so native
306
+ // `Array.prototype.concat` won't spread this proxy unless we opt in explicitly.
307
+ if (
308
+ prop === Symbol.isConcatSpreadable &&
309
+ isObservableArray(target) &&
310
+ !Array.isArray(target)
311
+ ) {
312
+ return true
313
+ }
314
+
315
+ switch (prop) {
316
+ case "at":
317
+ return (indexValue: number) => {
318
+ const resolvedIndex = resolveArrayAtIndex(indexValue, target.length)
319
+ return resolvedIndex === undefined ? undefined : decodeIndex(target, resolvedIndex)
320
+ }
321
+ case "push":
322
+ return (...items: unknown[]) =>
323
+ target.push(
324
+ ...items.map((item, offset) =>
325
+ getItemAdapter(target.length + offset).toStored(item)
326
+ )
327
+ )
328
+ case "pop":
329
+ return () => {
330
+ if (target.length <= 0) {
331
+ return undefined
332
+ }
333
+
334
+ const indexValue = target.length - 1
335
+ const value = target.pop()
336
+ return getItemAdapter(indexValue).toRuntime(value)
337
+ }
338
+ case "shift":
339
+ return () => {
340
+ if (target.length <= 0) {
341
+ return undefined
342
+ }
343
+
344
+ const value = target.shift()
345
+ return getItemAdapter(0).toRuntime(value)
346
+ }
347
+ case "unshift":
348
+ return (...items: unknown[]) =>
349
+ target.unshift(
350
+ ...items.map((item, indexValue) => getItemAdapter(indexValue).toStored(item))
351
+ )
352
+ case "splice":
353
+ return (start: number, deleteCount?: number, ...items: unknown[]) => {
354
+ const normalizedStart = normalizeArrayIndex(start, target.length)
355
+ const actualDeleteCount =
356
+ deleteCount === undefined
357
+ ? target.length - normalizedStart
358
+ : Math.max(0, Math.min(deleteCount, target.length - normalizedStart))
359
+
360
+ const removedValues = target
361
+ .slice(normalizedStart, normalizedStart + actualDeleteCount)
362
+ .map((value, indexValue) =>
363
+ getItemAdapter(normalizedStart + indexValue).toRuntime(value)
364
+ )
365
+
366
+ target.splice(
367
+ normalizedStart,
368
+ actualDeleteCount,
369
+ ...items.map((item, indexValue) =>
370
+ getItemAdapter(normalizedStart + indexValue).toStored(item)
371
+ )
372
+ )
373
+
374
+ return removedValues
375
+ }
376
+ case "reverse":
377
+ return () => mutateViaRuntime(target, receiver, (v) => v.reverse())
378
+ case "sort":
379
+ return (compareFn?: (a: unknown, b: unknown) => number) =>
380
+ mutateViaRuntime(target, receiver, (v) => v.sort(compareFn))
381
+ case "fill":
382
+ return (value: unknown, start?: number, end?: number) =>
383
+ mutateViaRuntime(target, receiver, (v) => v.fill(value, start, end))
384
+ case "copyWithin":
385
+ return (targetIndex: number, start?: number, end?: number) =>
386
+ mutateViaRuntime(target, receiver, (v) =>
387
+ v.copyWithin(targetIndex, start ?? 0, end)
388
+ )
389
+ case "concat":
390
+ case "flat":
391
+ case "flatMap":
392
+ case "filter":
393
+ case "find":
394
+ case "findIndex":
395
+ case "forEach":
396
+ case "every":
397
+ case "includes":
398
+ case "indexOf":
399
+ case "join":
400
+ case "lastIndexOf":
401
+ case "map":
402
+ case "reduce":
403
+ case "reduceRight":
404
+ case "slice":
405
+ case "some":
406
+ case "findLast":
407
+ case "findLastIndex":
408
+ case "toReversed":
409
+ case "toSorted":
410
+ case "toSpliced":
411
+ case "toString":
412
+ case "toLocaleString":
413
+ case "with":
414
+ return (...args: unknown[]) => callReadOnlyRuntimeMethod(receiver, prop, args)
415
+ case "entries":
416
+ return function* () {
417
+ for (let i = 0; i < target.length; i++) {
418
+ yield [i, decodeIndex(target, i)] as const
419
+ }
420
+ }
421
+ case "keys":
422
+ return function* () {
423
+ for (let i = 0; i < target.length; i++) {
424
+ yield i
425
+ }
426
+ }
427
+ case "values":
428
+ case Symbol.iterator:
429
+ return function* () {
430
+ for (let i = 0; i < target.length; i++) {
431
+ yield decodeIndex(target, i)
432
+ }
433
+ }
434
+ default:
435
+ return Reflect.get(target, prop, receiver)
436
+ }
437
+ },
438
+
439
+ set(target, prop, value) {
440
+ const index = getArrayIndex(prop)
441
+ if (index !== undefined) {
442
+ target[index] = getItemAdapter(index).toStored(value)
443
+ return true
444
+ }
445
+
446
+ Reflect.set(target, prop, value)
447
+ return true
448
+ },
449
+ })
450
+
451
+ runtimeByStored.set(stored, proxy)
452
+ storedByRuntime.set(proxy, stored)
453
+
454
+ return proxy
455
+ },
456
+
457
+ toStored(runtime) {
458
+ if (isArray(runtime)) {
459
+ const cachedStored = storedByRuntime.get(runtime)
460
+ if (cachedStored) {
461
+ return cachedStored
462
+ }
463
+
464
+ return Array.from(runtime, (value, index) => getItemAdapter(index).toStored(value))
465
+ }
466
+
467
+ return runtime as never
468
+ },
469
+
470
+ toStoredIfCached(runtime) {
471
+ if (isArray(runtime)) {
472
+ const cachedStored = storedByRuntime.get(runtime)
473
+ if (cachedStored) {
474
+ return { found: true, value: cachedStored }
475
+ }
476
+ }
477
+
478
+ return { found: false }
479
+ },
480
+ }
481
+ }
482
+
483
+ function createCodecLeafRuntimeAdapter(
484
+ transform: ModelPropTransform<unknown, unknown>
485
+ ): RuntimeAdapter<unknown, unknown> {
486
+ const runtimeByStored = new WeakMap<object, unknown>()
487
+ const storedByRuntime = new WeakMap<object, unknown>()
488
+
489
+ return {
490
+ toRuntime(stored, setStored) {
491
+ const cachedRuntime = isObject(stored) ? runtimeByStored.get(stored) : undefined
492
+
493
+ const runtime = transform.transform({
494
+ originalValue: stored,
495
+ cachedTransformedValue: cachedRuntime,
496
+ setOriginalValue(newStoredValue) {
497
+ if (setStored) {
498
+ setStored(newStoredValue)
499
+ }
500
+ },
501
+ })
502
+
503
+ if (isObject(runtime)) {
504
+ storedByRuntime.set(runtime, stored)
505
+ }
506
+
507
+ if (isObject(stored)) {
508
+ runtimeByStored.set(stored, runtime)
509
+ }
510
+
511
+ return runtime
512
+ },
513
+
514
+ toStored(runtime) {
515
+ const cachedStored = isObject(runtime) ? storedByRuntime.get(runtime) : undefined
516
+ if (cachedStored !== undefined) {
517
+ return cachedStored
518
+ }
519
+
520
+ const stored = transform.untransform({
521
+ transformedValue: runtime,
522
+ cacheTransformedValue: () => {},
523
+ })
524
+
525
+ if (isObject(runtime)) {
526
+ storedByRuntime.set(runtime, stored)
527
+ }
528
+
529
+ if (isObject(stored)) {
530
+ runtimeByStored.set(stored, runtime)
531
+ }
532
+
533
+ return stored
534
+ },
535
+
536
+ toStoredIfCached(runtime) {
537
+ if (isObject(runtime)) {
538
+ const cachedStored = storedByRuntime.get(runtime)
539
+ if (cachedStored !== undefined) {
540
+ return { found: true, value: cachedStored }
541
+ }
542
+ }
543
+
544
+ return { found: false }
545
+ },
546
+ }
547
+ }
548
+
549
+ function createOrRuntimeAdapter(
550
+ childTypes: ReadonlyArray<AnyStandardType>,
551
+ getChildSupports: () => ReadonlyArray<ResolvedCodecSupport>,
552
+ dispatcher: OrTypeInfo["dispatcher"]
553
+ ): RuntimeAdapter<unknown, unknown> {
554
+ const getRuntimeSupportForValue = (value: unknown) => {
555
+ const supports = getChildSupports()
556
+ for (let i = 0; i < supports.length; i++) {
557
+ const childType = childTypes[i]
558
+ const tc = resolveTypeChecker(childType)
559
+ if (tc.skipCheck) {
560
+ // For skipCheck branches, use snapshotType for structural matching
561
+ // instead of check (which always passes and would match any value)
562
+ if (tc.snapshotType(value)) {
563
+ return supports[i]
564
+ }
565
+ } else if (!tc.check(value, [], value)) {
566
+ return supports[i]
567
+ }
568
+ }
569
+
570
+ return supports[0]
571
+ }
572
+
573
+ const getStoredSupportForValue = (value: unknown) => {
574
+ if (dispatcher) {
575
+ return resolveCodecSupport(dispatcher(value))
576
+ }
577
+
578
+ const supports = getChildSupports()
579
+ for (let i = 0; i < supports.length; i++) {
580
+ const storedTypeChecker = resolveTypeChecker(supports[i].storedType)
581
+ if (storedTypeChecker.snapshotType(value)) {
582
+ return supports[i]
583
+ }
584
+ }
585
+
586
+ return supports[0]
587
+ }
588
+
589
+ return {
590
+ toRuntime(stored, setStored) {
591
+ return getStoredSupportForValue(stored).adapter.toRuntime(stored, setStored)
592
+ },
593
+
594
+ toStored(runtime) {
595
+ return getRuntimeSupportForValue(runtime).adapter.toStored(runtime)
596
+ },
597
+
598
+ toStoredIfCached(runtime) {
599
+ return getRuntimeSupportForValue(runtime).adapter.toStoredIfCached(runtime)
600
+ },
601
+ }
602
+ }
603
+
604
+ function noCodecSupport(type: AnyStandardType): ResolvedCodecSupport {
605
+ return { hasCodec: false, storedType: type, adapter: identityRuntimeAdapter }
606
+ }
607
+
608
+ function resolveCodecSupportForStandardType(type: AnyStandardType): ResolvedCodecSupport {
609
+ const cachedSupport = codecSupportCache.get(type)
610
+ if (cachedSupport) {
611
+ return cachedSupport
612
+ }
613
+
614
+ let resolvedSupport: ResolvedCodecSupport | undefined
615
+ let resolvedStoredType: AnyStandardType | undefined
616
+
617
+ const placeholderSupport: ResolvedCodecSupport = {
618
+ hasCodec: false,
619
+ storedType: lateTypeChecker(
620
+ () => resolveTypeChecker(resolvedStoredType ?? type),
621
+ () => getTypeInfo(resolvedStoredType ?? type)
622
+ ) as any,
623
+ adapter: {
624
+ toRuntime(stored, setStored) {
625
+ return (resolvedSupport?.adapter ?? identityRuntimeAdapter).toRuntime(stored, setStored)
626
+ },
627
+
628
+ toStored(runtime) {
629
+ return (resolvedSupport?.adapter ?? identityRuntimeAdapter).toStored(runtime)
630
+ },
631
+
632
+ toStoredIfCached(runtime) {
633
+ return (resolvedSupport?.adapter ?? identityRuntimeAdapter).toStoredIfCached(runtime)
634
+ },
635
+ },
636
+ }
637
+
638
+ codecSupportCache.set(type, placeholderSupport)
639
+
640
+ const codecMetadata = getCodecMetadata(type)
641
+ if (codecMetadata) {
642
+ resolvedSupport = {
643
+ hasCodec: true,
644
+ storedType: resolveStandardType(codecMetadata.encodedType),
645
+ adapter: createCodecLeafRuntimeAdapter(codecMetadata.transform),
646
+ }
647
+ } else if (isModelStandardType(type) || isDataModelDataStandardType(type)) {
648
+ resolvedSupport = noCodecSupport(type)
649
+ } else {
650
+ const typeInfo = getTypeInfo(type)
651
+
652
+ if (typeInfo instanceof ArrayTypeInfo) {
653
+ const itemSupport = lazy(() => resolveCodecSupportForStandardType(typeInfo.itemType))
654
+
655
+ resolvedSupport = !itemSupport().hasCodec
656
+ ? noCodecSupport(type)
657
+ : {
658
+ hasCodec: true,
659
+ storedType: typesArray(itemSupport().storedType),
660
+ adapter: createArrayLikeRuntimeAdapter(() => itemSupport().adapter),
661
+ }
662
+ } else if (typeInfo instanceof TupleTypeInfo) {
663
+ const itemSupports = lazy(() =>
664
+ typeInfo.itemTypes.map((itemType) => resolveCodecSupportForStandardType(itemType))
665
+ )
666
+
667
+ resolvedSupport = !itemSupports().some((itemSupport) => itemSupport.hasCodec)
668
+ ? noCodecSupport(type)
669
+ : {
670
+ hasCodec: true,
671
+ storedType: typesTuple(...itemSupports().map((itemSupport) => itemSupport.storedType)),
672
+ adapter: createArrayLikeRuntimeAdapter(
673
+ (index) => itemSupports()[index]?.adapter ?? identityRuntimeAdapter
674
+ ),
675
+ }
676
+ } else if (typeInfo instanceof ObjectTypeInfo) {
677
+ const childSupports = lazy(() => {
678
+ const supports: Record<string, ResolvedCodecSupport> = {}
679
+ const props = typeInfo.props
680
+ for (const propName of Object.keys(props)) {
681
+ supports[propName] = resolveCodecSupportForStandardType(props[propName].type)
682
+ }
683
+ return supports
684
+ })
685
+
686
+ resolvedSupport = !Object.values(childSupports()).some(
687
+ (childSupport) => childSupport.hasCodec
688
+ )
689
+ ? noCodecSupport(type)
690
+ : {
691
+ hasCodec: true,
692
+ storedType: typesObject(() => {
693
+ const storedProps: Record<string, AnyStandardType> = {}
694
+ const props = typeInfo.props
695
+ for (const propName of Object.keys(props)) {
696
+ storedProps[propName] = childSupports()[propName].storedType
697
+ }
698
+ return storedProps
699
+ }),
700
+ adapter: createObjectRuntimeAdapter(childSupports),
701
+ }
702
+ } else if (typeInfo instanceof RecordTypeInfo) {
703
+ const valueSupport = lazy(() => resolveCodecSupportForStandardType(typeInfo.valueType))
704
+
705
+ resolvedSupport = !valueSupport().hasCodec
706
+ ? noCodecSupport(type)
707
+ : {
708
+ hasCodec: true,
709
+ storedType: typesRecord(valueSupport().storedType),
710
+ adapter: createRecordRuntimeAdapter(valueSupport),
711
+ }
712
+ } else if (typeInfo instanceof TagTypeInfo) {
713
+ const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
714
+
715
+ resolvedSupport = !baseSupport.hasCodec
716
+ ? noCodecSupport(type)
717
+ : {
718
+ hasCodec: true,
719
+ storedType: typesTag(baseSupport.storedType, typeInfo.tag, typeInfo.typeName),
720
+ adapter: baseSupport.adapter,
721
+ }
722
+ } else if (typeInfo instanceof RefinementTypeInfo) {
723
+ const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
724
+
725
+ resolvedSupport = baseSupport.hasCodec ? baseSupport : noCodecSupport(type)
726
+ } else if (typeInfo instanceof SkipCheckTypeInfo) {
727
+ const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
728
+
729
+ // Always wrap storedType with skipCheck, even when hasCodec is false.
730
+ // resolveStoredType() feeds model-internal automatic validation in sharedInternalModel.ts,
731
+ // so the skipCheck wrapper must propagate to prevent the stored type from being validated.
732
+ resolvedSupport = {
733
+ hasCodec: baseSupport.hasCodec,
734
+ storedType: typesSkipCheck(baseSupport.storedType),
735
+ adapter: baseSupport.adapter,
736
+ }
737
+ } else if (typeInfo instanceof OrTypeInfo) {
738
+ const childSupports = lazy(() =>
739
+ typeInfo.orTypes.map((orType) => resolveCodecSupportForStandardType(orType))
740
+ )
741
+
742
+ resolvedSupport = !childSupports().some((childSupport) => childSupport.hasCodec)
743
+ ? noCodecSupport(type)
744
+ : {
745
+ hasCodec: true,
746
+ storedType: typeInfo.dispatcher
747
+ ? typesOr(
748
+ (snapshot) => resolveStoredType(typeInfo.dispatcher!(snapshot)),
749
+ ...childSupports().map((childSupport) => childSupport.storedType)
750
+ )
751
+ : typesOr(...childSupports().map((childSupport) => childSupport.storedType)),
752
+ adapter: createOrRuntimeAdapter(typeInfo.orTypes, childSupports, typeInfo.dispatcher),
753
+ }
754
+ } else {
755
+ resolvedSupport = noCodecSupport(type)
756
+ }
757
+ }
758
+
759
+ resolvedStoredType = resolvedSupport.storedType
760
+ placeholderSupport.hasCodec = resolvedSupport.hasCodec
761
+ placeholderSupport.storedType = resolvedSupport.storedType
762
+ placeholderSupport.adapter = resolvedSupport.adapter
763
+
764
+ return placeholderSupport
765
+ }
766
+
767
+ export function resolveCodecSupport(type: AnyType) {
768
+ return resolveCodecSupportForStandardType(resolveStandardType(type))
769
+ }
770
+
771
+ export function resolveStoredType(type: AnyType) {
772
+ return resolveCodecSupport(type).storedType
773
+ }
774
+
775
+ export function resolveStoredTypeChecker(type: AnyType) {
776
+ return resolveTypeChecker(resolveCodecSupport(type).storedType)
777
+ }
778
+
779
+ export function createCodecPropTransform(type: AnyType): ModelPropTransform<any, unknown> {
780
+ const adapter = resolveCodecSupport(type).adapter
781
+
782
+ return {
783
+ transform({ originalValue, cachedTransformedValue, setOriginalValue }) {
784
+ // For leaf codecs backed by primitive stored data (e.g. timestamps, ISO strings),
785
+ // the adapter's WeakMap cache can't work. Thread the model's cachedTransformedValue
786
+ // to avoid creating new runtime objects when the stored value hasn't changed.
787
+ if (cachedTransformedValue !== undefined) {
788
+ const cachedStored = adapter.toStoredIfCached(cachedTransformedValue)
789
+ if (cachedStored.found && cachedStored.value === originalValue) {
790
+ return cachedTransformedValue
791
+ }
792
+ }
793
+ return adapter.toRuntime(originalValue, setOriginalValue)
794
+ },
795
+
796
+ untransform({ transformedValue, cacheTransformedValue }) {
797
+ const cachedStored = adapter.toStoredIfCached(transformedValue)
798
+ if (cachedStored.found) {
799
+ cacheTransformedValue()
800
+ return cachedStored.value
801
+ }
802
+
803
+ return adapter.toStored(transformedValue)
804
+ },
805
+ }
806
+ }