mobx-keystone 1.22.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +1 -1
  4. package/dist/mobx-keystone.esm.js +7198 -6242
  5. package/dist/mobx-keystone.esm.mjs +7198 -6242
  6. package/dist/mobx-keystone.umd.js +7217 -6243
  7. package/dist/types/action/runUnprotected.d.ts +8 -4
  8. package/dist/types/model/Model.d.ts +5 -0
  9. package/dist/types/modelShared/modelDecorator.d.ts +1 -1
  10. package/dist/types/modelShared/prop.d.ts +8 -4
  11. package/dist/types/snapshot/modelInitialData.d.ts +1 -0
  12. package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
  13. package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
  14. package/dist/types/utils/index.d.ts +24 -0
  15. package/package.json +15 -11
  16. package/src/action/middleware.ts +5 -18
  17. package/src/action/mutationBatch.ts +72 -0
  18. package/src/action/runUnprotected.ts +21 -8
  19. package/src/action/wrapInAction.ts +45 -15
  20. package/src/actionMiddlewares/undoMiddleware.ts +19 -13
  21. package/src/dataModel/BaseDataModel.ts +4 -8
  22. package/src/deepChange/onDeepChange.ts +246 -16
  23. package/src/model/BaseModel.ts +4 -12
  24. package/src/model/Model.ts +5 -0
  25. package/src/model/newModel.ts +68 -36
  26. package/src/modelShared/modelDecorator.ts +64 -53
  27. package/src/modelShared/prop.ts +105 -63
  28. package/src/modelShared/sharedInternalModel.ts +91 -60
  29. package/src/parent/core.ts +16 -10
  30. package/src/parent/coreObjectChildren.ts +219 -21
  31. package/src/parent/path.ts +6 -5
  32. package/src/parent/setParent.ts +5 -5
  33. package/src/patch/emitPatch.ts +102 -20
  34. package/src/ref/Ref.ts +4 -5
  35. package/src/ref/core.ts +2 -2
  36. package/src/snapshot/fromArraySnapshot.ts +11 -2
  37. package/src/snapshot/fromModelSnapshot.ts +10 -3
  38. package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
  39. package/src/snapshot/fromSnapshot.ts +31 -9
  40. package/src/snapshot/getSnapshot.ts +8 -3
  41. package/src/snapshot/internal.ts +243 -82
  42. package/src/snapshot/modelInitialData.ts +41 -0
  43. package/src/snapshot/reconcileArraySnapshot.ts +9 -3
  44. package/src/snapshot/reconcileModelSnapshot.ts +97 -17
  45. package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
  46. package/src/treeUtils/draft.ts +7 -8
  47. package/src/tweaker/core.ts +30 -5
  48. package/src/tweaker/treeNodeMetadata.ts +38 -0
  49. package/src/tweaker/tweak.ts +15 -4
  50. package/src/tweaker/tweakArray.ts +159 -121
  51. package/src/tweaker/tweakFrozen.ts +2 -2
  52. package/src/tweaker/tweakModel.ts +2 -2
  53. package/src/tweaker/tweakPlainObject.ts +117 -124
  54. package/src/tweaker/typeChecking.ts +10 -1
  55. package/src/types/TypeChecker.ts +261 -15
  56. package/src/types/arrayBased/typesArray.ts +38 -40
  57. package/src/types/arrayBased/typesTuple.ts +51 -34
  58. package/src/types/createCachedTypeCheck.ts +181 -0
  59. package/src/types/objectBased/typesArraySet.ts +42 -32
  60. package/src/types/objectBased/typesDataModelData.ts +18 -11
  61. package/src/types/objectBased/typesModel.ts +1 -1
  62. package/src/types/objectBased/typesObject.ts +57 -28
  63. package/src/types/objectBased/typesObjectMap.ts +51 -40
  64. package/src/types/objectBased/typesRecord.ts +59 -39
  65. package/src/types/objectBased/typesRef.ts +1 -1
  66. package/src/types/primitiveBased/typesPrimitive.ts +9 -9
  67. package/src/types/tProp.ts +80 -44
  68. package/src/types/utility/typesOr.ts +35 -24
  69. package/src/types/utility/typesRefinement.ts +21 -8
  70. package/src/types/utility/typesSkipCheck.ts +21 -8
  71. package/src/types/utility/typesTag.ts +21 -9
  72. package/src/types/utility/typesUnchecked.ts +2 -3
  73. package/src/utils/ModelPool.ts +34 -19
  74. package/src/utils/errorDiagnostics.ts +60 -18
  75. package/src/utils/index.ts +142 -9
  76. package/src/types/createPerEntryCachedCheck.ts +0 -166
  77. /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
@@ -15,10 +15,11 @@ import {
15
15
  } from "../modelShared/modelSymbols"
16
16
  import { refreshInternalSnapshot } from "../snapshot/internal"
17
17
  import {
18
+ addLateInitializationFunction,
18
19
  failure,
19
20
  getMobxVersion,
20
21
  logWarning,
21
- mobx6,
22
+ makeObservableCompat,
22
23
  runAfterNewSymbol,
23
24
  runBeforeOnInitSymbol,
24
25
  runLateInitializationFunctions,
@@ -34,11 +35,11 @@ import type { AnyFunction } from "../utils/AnyFunction"
34
35
  */
35
36
  export const model =
36
37
  (name: string) =>
37
- <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, ...args: any[]): MC => {
38
- const ctx = typeof args[1] === "object" ? (args[1] as ClassDecoratorContext) : undefined
39
-
40
- return internalModel(name, clazz, ctx?.addInitializer) as any
41
- }
38
+ <MC extends ModelClass<AnyModel | AnyDataModel>>(
39
+ clazz: MC,
40
+ _context?: ClassDecoratorContext<MC>
41
+ ): MC =>
42
+ internalModel(name, clazz) as any
42
43
 
43
44
  const afterClassInitializationData = new WeakMap<
44
45
  ModelClass<AnyModel | AnyDataModel>,
@@ -56,14 +57,14 @@ const runAfterClassInitialization = (
56
57
 
57
58
  const tag = afterClassInitializationData.get(target)!
58
59
 
59
- // compatibility with mobx 6
60
+ // compatibility with mobx 6 and 7
60
61
  if (tag.needsMakeObservable) {
61
62
  // we know it can be done and shouldn't fail
62
- mobx6.makeObservable(instance)
63
+ makeObservableCompat(instance)
63
64
  } else if (tag.needsMakeObservable === undefined) {
64
65
  if (getMobxVersion() >= 6) {
65
66
  try {
66
- mobx6.makeObservable(instance)
67
+ makeObservableCompat(instance)
67
68
  tag.needsMakeObservable = true
68
69
  } catch (e) {
69
70
  const err = e as Error
@@ -121,8 +122,7 @@ const proxyClassHandler: ProxyHandler<ModelClass<AnyModel | AnyDataModel>> = {
121
122
 
122
123
  const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>(
123
124
  name: string,
124
- clazz: MC,
125
- addInitializer: ((initializer: (this: any) => void) => void) | undefined
125
+ clazz: MC
126
126
  ): MC | void => {
127
127
  const type = isModelClass(clazz) ? "class" : isDataModelClass(clazz) ? "data" : undefined
128
128
  if (!type) {
@@ -151,48 +151,24 @@ const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>(
151
151
  // track if we fail so we only try it once per class
152
152
  afterClassInitializationData.set(clazz, { needsMakeObservable: undefined, type })
153
153
 
154
- if (addInitializer) {
155
- // standard decorator API, avoid proxies
156
- addInitializer(function (this: any) {
157
- runAfterClassInitialization(clazz, this)
158
- })
159
-
160
- const modelInfo = {
161
- name,
162
- class: clazz,
163
- }
164
-
165
- modelInfoByName[name] = modelInfo
154
+ const decoratedClass = new Proxy<MC>(clazz, proxyClassHandler)
155
+ ;(decoratedClass as any)[modelUnwrappedClassSymbol] = clazz
166
156
 
167
- modelInfoByClass.set(clazz, modelInfo)
157
+ decoratedClass.prototype.constructor = decoratedClass
168
158
 
169
- runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
170
-
171
- return undefined // use same class
172
- } else {
173
- // non-standard decorator API, use proxies
174
-
175
- // trick so plain new works
176
- const proxyClass = new Proxy<MC>(clazz, proxyClassHandler)
177
-
178
- // set or else it points to the undecorated class
179
- proxyClass.prototype.constructor = proxyClass
180
- ;(proxyClass as any)[modelUnwrappedClassSymbol] = clazz
181
-
182
- const modelInfo = {
183
- name,
184
- class: proxyClass,
185
- }
159
+ const modelInfo = {
160
+ name,
161
+ class: decoratedClass,
162
+ }
186
163
 
187
- modelInfoByName[name] = modelInfo
164
+ modelInfoByName[name] = modelInfo
188
165
 
189
- modelInfoByClass.set(proxyClass, modelInfo)
190
- modelInfoByClass.set(clazz, modelInfo)
166
+ modelInfoByClass.set(decoratedClass, modelInfo)
167
+ modelInfoByClass.set(clazz, modelInfo)
191
168
 
192
- runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
169
+ runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
193
170
 
194
- return proxyClass
195
- }
171
+ return decoratedClass
196
172
  }
197
173
 
198
174
  // basically taken from TS
@@ -243,18 +219,53 @@ export function decoratedModel<M, MC extends abstract new (...ags: any) => M>(
243
219
  [k in keyof M]?: AnyFunction | ReadonlyArray<AnyFunction>
244
220
  }
245
221
  ): MC {
222
+ const mobx7Annotations: Record<string, unknown> = {}
223
+
246
224
  // decorate class members
247
225
  for (const [k, decorator] of Object.entries(decorators)) {
226
+ const decoratorList = Array.isArray(decorator) ? decorator : [decorator]
227
+ const mobx7Annotation = decoratorList.find(isMobxAnnotation)
228
+ if (getMobxVersion() >= 7 && mobx7Annotation) {
229
+ if (decoratorList.length !== 1) {
230
+ throw failure("MobX 7 decorators cannot be combined in decoratedModel")
231
+ }
232
+ mobx7Annotations[k] = mobx7Annotation
233
+ continue
234
+ }
235
+
248
236
  const prototypeValueDesc = Object.getOwnPropertyDescriptor(clazz.prototype, k)
249
237
  // TS seems to send null for methods in the prototype
250
238
  // (which we substitute for the descriptor to avoid a double look-up) and void 0 (undefined) for props
251
- tsDecorate(
252
- Array.isArray(decorator) ? decorator : [decorator],
253
- clazz.prototype,
254
- k,
255
- prototypeValueDesc ? prototypeValueDesc : void 0
256
- )
239
+ tsDecorate(decoratorList, clazz.prototype, k, prototypeValueDesc ? prototypeValueDesc : void 0)
240
+ }
241
+
242
+ if (getMobxVersion() >= 7 && Object.keys(mobx7Annotations).length > 0) {
243
+ addLateInitializationFunction(clazz.prototype, runAfterNewSymbol, (instance) => {
244
+ makeObservableCompat(instance, mobx7Annotations)
245
+ })
257
246
  }
258
247
 
259
248
  return (name ? model(name)(clazz as unknown as ModelClass<AnyModel>) : clazz) as MC
260
249
  }
250
+
251
+ type MobxAnnotation = {
252
+ annotationType_: string
253
+ make_: AnyFunction
254
+ extend_: AnyFunction
255
+ }
256
+
257
+ // MobX uses this same structural check in its internal `isAnnotation` helper.
258
+ // That helper is not part of MobX's public root API, so mirror it here instead
259
+ // of depending on a private deep import that may not exist in every supported version.
260
+ function isMobxAnnotation(value: unknown): value is MobxAnnotation {
261
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
262
+ return false
263
+ }
264
+
265
+ const annotation = value as Partial<MobxAnnotation>
266
+ return (
267
+ typeof annotation.annotationType_ === "string" &&
268
+ typeof annotation.make_ === "function" &&
269
+ typeof annotation.extend_ === "function"
270
+ )
271
+ }
@@ -1,5 +1,6 @@
1
1
  import type { SnapshotInOf, SnapshotOutOf } from "../snapshot/SnapshotOf"
2
2
  import type { AnyStandardType } from "../types/schemas"
3
+ import { lazy } from "../utils"
3
4
  import { runWithErrorDiagnosticsContext, withErrorPathSegment } from "../utils/errorDiagnostics"
4
5
  import { getOrCreate } from "../utils/mapUtils"
5
6
  import type { Flatten, IsNeverType, IsOptionalValue } from "../utils/types"
@@ -13,6 +14,17 @@ type SetterMode = boolean | "assign"
13
14
 
14
15
  export type ModelPropSetterValueTransform<T> = (value: T) => T
15
16
 
17
+ /** @internal */
18
+ export interface FullModelPropTransform {
19
+ transform: (
20
+ original: unknown,
21
+ model: object,
22
+ propName: PropertyKey,
23
+ setOriginalValue: (newOriginalValue: unknown) => void
24
+ ) => unknown
25
+ untransform: (transformed: unknown, model: object, propName: PropertyKey) => unknown
26
+ }
27
+
16
28
  /**
17
29
  * A model property.
18
30
  */
@@ -42,24 +54,18 @@ export interface ModelProp<
42
54
  // True when the declared default is a runtime/transformed value and must be
43
55
  // converted before it can be stored in `$`.
44
56
  _defaultValueIsTransformed: boolean
57
+ _getDefaultValueIsTransformed?: () => boolean
45
58
  _typeChecker: AnyStandardType | undefined
46
59
  _setter: SetterMode
47
60
  _setterValueTransform: ((value: unknown) => unknown) | undefined
48
61
  _isId: boolean
49
62
  _idGenerator?: (() => string) | undefined
50
- _transform:
51
- | {
52
- transform: (
53
- original: unknown,
54
- model: object,
55
- propName: PropertyKey,
56
- setOriginalValue: (newOriginalValue: unknown) => void
57
- ) => unknown
58
- untransform: (transformed: unknown, model: object, propName: PropertyKey) => unknown
59
- }
60
- | undefined
63
+ _transform: FullModelPropTransform | undefined
64
+ _getTransform?: () => FullModelPropTransform | undefined
61
65
  _fromSnapshotProcessor?: (sn: unknown) => unknown
62
66
  _toSnapshotProcessor?: (sn: unknown) => unknown
67
+ _getFromSnapshotProcessor?: () => ((sn: unknown) => unknown) | undefined
68
+ _getToSnapshotProcessor?: () => ((sn: unknown) => unknown) | undefined
63
69
 
64
70
  /**
65
71
  * Adds a setter to the property. The setter will be named `set${CapitalizedPropName}`
@@ -141,6 +147,9 @@ export interface ModelProp<
141
147
  *
142
148
  * `fromSnapshot` runs before assigning snapshot data into the model prop.
143
149
  * `toSnapshot` runs when exporting model data back to snapshot form.
150
+ * Both processors must be pure and deterministic, and their canonical output
151
+ * must round-trip to the same stored value. Reconciliation and snapshot
152
+ * generation may skip calls when that canonical snapshot is already current.
144
153
  */
145
154
  withSnapshotProcessor<
146
155
  FS = TFromSnapshotOverride,
@@ -400,13 +409,17 @@ const baseProp: AnyModelProp = {
400
409
  _defaultFn: noDefaultValue,
401
410
  _defaultValue: noDefaultValue,
402
411
  _defaultValueIsTransformed: false,
412
+ _getDefaultValueIsTransformed: undefined,
403
413
  _typeChecker: undefined,
404
414
  _setter: false,
405
415
  _setterValueTransform: undefined,
406
416
  _isId: false,
407
417
  _transform: undefined,
418
+ _getTransform: undefined,
408
419
  _fromSnapshotProcessor: undefined,
409
420
  _toSnapshotProcessor: undefined,
421
+ _getFromSnapshotProcessor: undefined,
422
+ _getToSnapshotProcessor: undefined,
410
423
 
411
424
  withSetter(modeOrValueTransform?: SetterMode | ModelPropSetterValueTransform<unknown>) {
412
425
  const obj: AnyModelProp = Object.create(this)
@@ -418,43 +431,88 @@ const baseProp: AnyModelProp = {
418
431
 
419
432
  withTransform(transform: ModelPropTransform<unknown, unknown>) {
420
433
  const obj: AnyModelProp = Object.create(this)
421
- obj._transform = composeFullTransforms(this._transform, toFullTransform(transform))
434
+ const innerTransform = toFullModelPropTransform(transform)
435
+ if (this._getTransform) {
436
+ const oldGet = this._getTransform
437
+ obj._getTransform = lazy(() => composeFullTransforms(oldGet(), innerTransform))
438
+ obj._transform = undefined
439
+ } else {
440
+ obj._transform = composeFullTransforms(this._transform, innerTransform)
441
+ }
422
442
  return obj
423
443
  },
424
444
 
425
445
  withSnapshotProcessor({ fromSnapshot, toSnapshot }) {
426
- let newFromSnapshot: ((sn: any) => any) | undefined
427
-
428
- if (this._fromSnapshotProcessor && fromSnapshot) {
429
- const oldFn = this._fromSnapshotProcessor
430
- const newFn = fromSnapshot
431
- newFromSnapshot = (sn: any) => oldFn(newFn(sn))
432
- } else if (fromSnapshot) {
433
- newFromSnapshot = fromSnapshot
434
- } else {
435
- newFromSnapshot = this._fromSnapshotProcessor
436
- }
437
-
438
- let newToSnapshot: ((sn: any) => any) | undefined
446
+ const obj: AnyModelProp = Object.create(this)
439
447
 
440
- if (this._toSnapshotProcessor && toSnapshot) {
441
- const oldFn: (sn: unknown) => any = this._toSnapshotProcessor
442
- const newFn = toSnapshot
443
- newToSnapshot = (sn: unknown) => newFn(oldFn(sn))
444
- } else if (toSnapshot) {
445
- newToSnapshot = toSnapshot
446
- } else {
447
- newToSnapshot = this._toSnapshotProcessor
448
+ if (fromSnapshot) {
449
+ const newProcessor = fromSnapshot as (sn: unknown) => unknown
450
+ if (this._getFromSnapshotProcessor) {
451
+ const oldGet = this._getFromSnapshotProcessor
452
+ obj._getFromSnapshotProcessor = lazy(() => {
453
+ const oldProcessor = oldGet()
454
+ return oldProcessor ? (sn: unknown) => oldProcessor(newProcessor(sn)) : newProcessor
455
+ })
456
+ obj._fromSnapshotProcessor = undefined
457
+ } else {
458
+ const oldProcessor = this._fromSnapshotProcessor
459
+ obj._fromSnapshotProcessor = oldProcessor
460
+ ? (sn: unknown) => oldProcessor(newProcessor(sn))
461
+ : newProcessor
462
+ }
448
463
  }
449
464
 
450
- const obj: AnyModelProp = Object.create(this)
451
- obj._fromSnapshotProcessor = newFromSnapshot
452
- obj._toSnapshotProcessor = newToSnapshot
465
+ if (toSnapshot) {
466
+ const newProcessor = toSnapshot as (sn: unknown) => unknown
467
+ if (this._getToSnapshotProcessor) {
468
+ const oldGet = this._getToSnapshotProcessor
469
+ obj._getToSnapshotProcessor = lazy(() => {
470
+ const oldProcessor = oldGet()
471
+ return oldProcessor ? (sn: unknown) => newProcessor(oldProcessor(sn)) : newProcessor
472
+ })
473
+ obj._toSnapshotProcessor = undefined
474
+ } else {
475
+ const oldProcessor = this._toSnapshotProcessor
476
+ obj._toSnapshotProcessor = oldProcessor
477
+ ? (sn: unknown) => newProcessor(oldProcessor(sn))
478
+ : newProcessor
479
+ }
480
+ }
453
481
 
454
482
  return obj
455
483
  },
456
484
  }
457
485
 
486
+ /** @internal */
487
+ export function getModelPropFromSnapshotProcessor(
488
+ modelProp: AnyModelProp
489
+ ): ((sn: unknown) => unknown) | undefined {
490
+ return modelProp._getFromSnapshotProcessor
491
+ ? modelProp._getFromSnapshotProcessor()
492
+ : modelProp._fromSnapshotProcessor
493
+ }
494
+
495
+ /** @internal */
496
+ export function getModelPropToSnapshotProcessor(
497
+ modelProp: AnyModelProp
498
+ ): ((sn: unknown) => unknown) | undefined {
499
+ return modelProp._getToSnapshotProcessor
500
+ ? modelProp._getToSnapshotProcessor()
501
+ : modelProp._toSnapshotProcessor
502
+ }
503
+
504
+ /** @internal */
505
+ export function getModelPropTransform(modelProp: AnyModelProp): FullModelPropTransform | undefined {
506
+ return modelProp._getTransform ? modelProp._getTransform() : modelProp._transform
507
+ }
508
+
509
+ /** @internal */
510
+ export function getModelPropDefaultValueIsTransformed(modelProp: AnyModelProp): boolean {
511
+ return modelProp._getDefaultValueIsTransformed
512
+ ? modelProp._getDefaultValueIsTransformed()
513
+ : modelProp._defaultValueIsTransformed
514
+ }
515
+
458
516
  /**
459
517
  * Defines a model property, with an optional function to generate a default value
460
518
  * if the input snapshot / model creation data is `null` or `undefined`.
@@ -565,7 +623,10 @@ const cacheTransformedValueFn = () => {
565
623
  cacheTransformResult = true
566
624
  }
567
625
 
568
- function toFullTransform(transformObject: ModelPropTransform<unknown, unknown>) {
626
+ /** @internal */
627
+ export function toFullModelPropTransform(
628
+ transformObject: ModelPropTransform<unknown, unknown>
629
+ ): FullModelPropTransform {
569
630
  const cache = new WeakMap<
570
631
  object,
571
632
  Map<PropertyKey, { originalValue: unknown; transformedValue: unknown }>
@@ -630,27 +691,9 @@ function toFullTransform(transformObject: ModelPropTransform<unknown, unknown>)
630
691
  }
631
692
 
632
693
  function composeFullTransforms(
633
- outerTransform:
634
- | {
635
- transform(
636
- originalValue: unknown,
637
- model: object,
638
- propName: PropertyKey,
639
- setOriginalValue: (newOriginalValue: unknown) => void
640
- ): unknown
641
- untransform(transformedValue: unknown, model: object, propName: PropertyKey): unknown
642
- }
643
- | undefined,
644
- innerTransform: {
645
- transform(
646
- originalValue: unknown,
647
- model: object,
648
- propName: PropertyKey,
649
- setOriginalValue: (newOriginalValue: unknown) => void
650
- ): unknown
651
- untransform(transformedValue: unknown, model: object, propName: PropertyKey): unknown
652
- }
653
- ) {
694
+ outerTransform: FullModelPropTransform | undefined,
695
+ innerTransform: FullModelPropTransform
696
+ ): FullModelPropTransform {
654
697
  if (!outerTransform) {
655
698
  return innerTransform
656
699
  }
@@ -699,15 +742,14 @@ export function getModelPropStoredDefaultValue(
699
742
  propName: string
700
743
  ): unknown | typeof noDefaultValue {
701
744
  const defaultValue = getModelPropDefaultValue(propData)
702
- if (defaultValue === noDefaultValue || !propData._defaultValueIsTransformed) {
745
+ if (defaultValue === noDefaultValue || !getModelPropDefaultValueIsTransformed(propData)) {
703
746
  return defaultValue
704
747
  }
705
748
 
706
- return propData._transform
749
+ const transform = getModelPropTransform(propData)
750
+ return transform
707
751
  ? runWithErrorDiagnosticsContext(() =>
708
- withErrorPathSegment(propName, () =>
709
- propData._transform!.untransform(defaultValue, model, propName)
710
- )
752
+ withErrorPathSegment(propName, () => transform.untransform(defaultValue, model, propName))
711
753
  )
712
754
  : defaultValue
713
755
  }
@@ -28,7 +28,10 @@ import { getInternalModelClassPropsInfo, setInternalModelClassPropsInfo } from "
28
28
  import { modelMetadataSymbol, modelUnwrappedClassSymbol } from "./modelSymbols"
29
29
  import {
30
30
  type AnyModelProp,
31
+ getModelPropFromSnapshotProcessor,
31
32
  getModelPropStoredDefaultValue,
33
+ getModelPropToSnapshotProcessor,
34
+ getModelPropTransform,
32
35
  type ModelProps,
33
36
  noDefaultValue,
34
37
  prop,
@@ -39,21 +42,36 @@ function createGetModelInstanceDataField<M extends AnyModel | AnyDataModel>(
39
42
  modelProp: AnyModelProp,
40
43
  modelPropName: string
41
44
  ): (this: M) => unknown {
42
- const transformFn = modelProp._transform?.transform
45
+ const fixedTransform = modelProp._getTransform ? undefined : modelProp._transform
43
46
 
44
- if (!transformFn) {
47
+ if (!modelProp._getTransform && !fixedTransform) {
45
48
  // no need to use get since these vars always get on the initial $
46
49
  return function (this) {
47
50
  return this.$[modelPropName]
48
51
  }
49
52
  }
50
53
 
51
- const transformValue = (model: M, value: unknown) =>
52
- transformFn(value, model, modelPropName, (newValue) => {
53
- // use apply set instead to wrap it in an action
54
- // set the $ object to set the original value directly
55
- applySet(model.$, modelPropName, newValue)
56
- })
54
+ type TransformValue = (model: M, value: unknown) => unknown
55
+ const createTransformValue =
56
+ (transform: NonNullable<ReturnType<typeof getModelPropTransform>>): TransformValue =>
57
+ (model, value) =>
58
+ transform.transform(value, model, modelPropName, (newValue) => {
59
+ // use apply set instead to wrap it in an action
60
+ // set the $ object to set the original value directly
61
+ applySet(model.$, modelPropName, newValue)
62
+ })
63
+ let transformValue: TransformValue
64
+ if (fixedTransform) {
65
+ transformValue = createTransformValue(fixedTransform)
66
+ } else {
67
+ transformValue = (model, value) => {
68
+ const transform = getModelPropTransform(modelProp)
69
+ transformValue = transform
70
+ ? createTransformValue(transform)
71
+ : (_model, storedValue) => storedValue
72
+ return transformValue(model, value)
73
+ }
74
+ }
57
75
 
58
76
  return function (this) {
59
77
  // no need to use get since these vars always get on the initial $
@@ -85,10 +103,9 @@ const setModelInstanceDataField: SetModelInstanceDataFieldFn = (
85
103
  value = modelProp._setterValueTransform(value)
86
104
  }
87
105
 
88
- let untransformedValue = modelProp._transform
89
- ? withErrorPathSegment(modelPropName, () =>
90
- modelProp._transform!.untransform(value, model, modelPropName)
91
- )
106
+ const transform = modelProp._getTransform ? modelProp._getTransform() : modelProp._transform
107
+ let untransformedValue = transform
108
+ ? withErrorPathSegment(modelPropName, () => transform.untransform(value, model, modelPropName))
92
109
  : value
93
110
 
94
111
  // apply default value if applicable
@@ -128,6 +145,27 @@ propForDefaultId._isId = true
128
145
  type FromSnapshotProcessorFn = (sn: any) => any
129
146
  type ToSnapshotProcessorFn = (sn: any, instance: any) => any
130
147
 
148
+ function defineLazySnapshotProcessor(
149
+ modelClass: object,
150
+ property: "fromSnapshotProcessor" | "toSnapshotProcessor",
151
+ compile: () => FromSnapshotProcessorFn | ToSnapshotProcessorFn | undefined
152
+ ): void {
153
+ Object.defineProperty(modelClass, property, {
154
+ configurable: true,
155
+ enumerable: true,
156
+ get() {
157
+ const processor = compile()
158
+ Object.defineProperty(modelClass, property, {
159
+ configurable: true,
160
+ enumerable: true,
161
+ writable: true,
162
+ value: processor,
163
+ })
164
+ return processor
165
+ },
166
+ })
167
+ }
168
+
131
169
  export function sharedInternalModel<
132
170
  TProps extends ModelProps,
133
171
  TBaseModel extends AnyModel | AnyDataModel,
@@ -234,13 +272,13 @@ export function sharedInternalModel<
234
272
  // create type checker if needed
235
273
  let dataTypeChecker: AnyType | undefined
236
274
  if (needsTypeChecker) {
237
- const typeCheckerObj: {
238
- [k: string]: any
239
- } = {}
240
- for (const [k, mp] of Object.entries(composedModelProps)) {
241
- typeCheckerObj[k] = mp._typeChecker ? resolveStoredType(mp._typeChecker) : typesUnchecked()
242
- }
243
- dataTypeChecker = typesObject(() => typeCheckerObj)
275
+ dataTypeChecker = typesObject(() => {
276
+ const typeCheckerObj: Record<string, AnyType> = {}
277
+ for (const [k, mp] of Object.entries(composedModelProps)) {
278
+ typeCheckerObj[k] = mp._typeChecker ? resolveStoredType(mp._typeChecker) : typesUnchecked()
279
+ }
280
+ return typeCheckerObj
281
+ })
244
282
  }
245
283
 
246
284
  const base: any = baseModel ?? (type === "class" ? BaseModel : BaseDataModel)
@@ -279,8 +317,17 @@ export function sharedInternalModel<
279
317
  return baseModel
280
318
  }
281
319
 
282
- // copy static props from base
283
- Object.assign(ThisModel, base)
320
+ // Copy static props from the base without reading its lazy snapshot processor
321
+ // accessors. This keeps subclass declaration from compiling the parent model's
322
+ // processor graph before snapshot processing is actually needed.
323
+ for (const key of Reflect.ownKeys(base)) {
324
+ if (key === "fromSnapshotProcessor" || key === "toSnapshotProcessor") {
325
+ continue
326
+ }
327
+ if (Object.prototype.propertyIsEnumerable.call(base, key)) {
328
+ ;(ThisModel as any)[key] = (base as any)[key]
329
+ }
330
+ }
284
331
  delete (ThisModel as any)[modelUnwrappedClassSymbol]
285
332
 
286
333
  const initializers: ModelClassInitializer[] = base[modelInitializersSymbol]
@@ -345,10 +392,6 @@ export function sharedInternalModel<
345
392
  }
346
393
  }
347
394
 
348
- const modelPropsFromSnapshotProcessor = getModelPropsFromSnapshotProcessor(composedModelProps)
349
-
350
- const modelPropsToSnapshotProcessor = getModelPropsToSnapshotProcessor(composedModelProps)
351
-
352
395
  if (fromSnapshotProcessor) {
353
396
  const fn = fromSnapshotProcessor
354
397
  fromSnapshotProcessor = (sn) => {
@@ -373,54 +416,42 @@ export function sharedInternalModel<
373
416
  }
374
417
  }
375
418
 
376
- ThisModel.fromSnapshotProcessor = chainFns(fromSnapshotProcessor, modelPropsFromSnapshotProcessor)
377
- ThisModel.toSnapshotProcessor = chainFns(modelPropsToSnapshotProcessor, toSnapshotProcessor)
419
+ defineLazySnapshotProcessor(ThisModel, "fromSnapshotProcessor", () =>
420
+ chainFns(
421
+ fromSnapshotProcessor,
422
+ getModelPropsSnapshotProcessor(composedModelProps, getModelPropFromSnapshotProcessor)
423
+ )
424
+ )
425
+ defineLazySnapshotProcessor(ThisModel, "toSnapshotProcessor", () =>
426
+ chainFns(
427
+ getModelPropsSnapshotProcessor(composedModelProps, getModelPropToSnapshotProcessor),
428
+ toSnapshotProcessor
429
+ )
430
+ )
378
431
 
379
432
  return ThisModel
380
433
  }
381
434
 
382
- function getModelPropsFromSnapshotProcessor(
383
- composedModelProps: ModelProps
435
+ function getModelPropsSnapshotProcessor(
436
+ composedModelProps: ModelProps,
437
+ getProcessor: (prop: AnyModelProp) => ((sn: unknown) => unknown) | undefined
384
438
  ): FromSnapshotProcessorFn | undefined {
385
- const propsWithFromSnapshotProcessor = Object.entries(composedModelProps).filter(
386
- ([_propName, propData]) => propData._fromSnapshotProcessor
387
- )
388
- if (propsWithFromSnapshotProcessor.length <= 0) {
389
- return undefined
390
- }
391
-
392
- return (sn) => {
393
- const newSn = { ...sn }
394
- for (const [propName, propData] of propsWithFromSnapshotProcessor) {
395
- if (propData._fromSnapshotProcessor) {
396
- newSn[propName] = withErrorPathSegment(propName, () =>
397
- propData._fromSnapshotProcessor!(sn[propName])
398
- )
399
- }
439
+ const propsWithProcessor: Array<readonly [string, (sn: unknown) => unknown]> = []
440
+ for (const [propName, propData] of Object.entries(composedModelProps)) {
441
+ const processor = getProcessor(propData)
442
+ if (processor) {
443
+ propsWithProcessor.push([propName, processor])
400
444
  }
401
- return newSn
402
445
  }
403
- }
404
-
405
- function getModelPropsToSnapshotProcessor(
406
- composedModelProps: ModelProps
407
- ): ToSnapshotProcessorFn | undefined {
408
- const propsWithToSnapshotProcessor = Object.entries(composedModelProps).filter(
409
- ([_propName, propData]) => propData._toSnapshotProcessor
410
- )
411
446
 
412
- if (propsWithToSnapshotProcessor.length <= 0) {
447
+ if (propsWithProcessor.length === 0) {
413
448
  return undefined
414
449
  }
415
450
 
416
451
  return (sn) => {
417
452
  const newSn = { ...sn }
418
- for (const [propName, propData] of propsWithToSnapshotProcessor) {
419
- if (propData._toSnapshotProcessor) {
420
- newSn[propName] = withErrorPathSegment(propName, () =>
421
- propData._toSnapshotProcessor!(sn[propName])
422
- )
423
- }
453
+ for (const [propName, processor] of propsWithProcessor) {
454
+ newSn[propName] = withErrorPathSegment(propName, () => processor(sn[propName]))
424
455
  }
425
456
  return newSn
426
457
  }