mobx-keystone 2.2.0 → 2.3.1

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 (168) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/mobx-keystone.esm.js +426 -334
  3. package/dist/mobx-keystone.esm.mjs +426 -334
  4. package/dist/mobx-keystone.umd.js +429 -333
  5. package/dist/types/action/applyAction.d.ts +1 -1
  6. package/dist/types/action/applyMethodCall.d.ts +1 -1
  7. package/dist/types/action/index.d.ts +12 -12
  8. package/dist/types/action/isModelAction.d.ts +1 -1
  9. package/dist/types/action/middleware.d.ts +1 -1
  10. package/dist/types/actionMiddlewares/actionSerialization/actionSerialization.d.ts +3 -3
  11. package/dist/types/actionMiddlewares/actionSerialization/applySerializedAction.d.ts +2 -2
  12. package/dist/types/actionMiddlewares/actionSerialization/arraySerializer.d.ts +2 -2
  13. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  14. package/dist/types/actionMiddlewares/actionSerialization/index.d.ts +3 -3
  15. package/dist/types/actionMiddlewares/actionSerialization/mapSerializer.d.ts +2 -2
  16. package/dist/types/actionMiddlewares/actionSerialization/objectPathSerializer.d.ts +2 -2
  17. package/dist/types/actionMiddlewares/actionSerialization/objectSnapshotSerializer.d.ts +1 -1
  18. package/dist/types/actionMiddlewares/actionSerialization/plainObjectSerializer.d.ts +1 -1
  19. package/dist/types/actionMiddlewares/actionSerialization/primitiveSerializer.d.ts +1 -1
  20. package/dist/types/actionMiddlewares/actionSerialization/setSerializer.d.ts +2 -2
  21. package/dist/types/actionMiddlewares/actionTrackingMiddleware.d.ts +3 -3
  22. package/dist/types/actionMiddlewares/index.d.ts +6 -6
  23. package/dist/types/actionMiddlewares/onActionMiddleware.d.ts +3 -3
  24. package/dist/types/actionMiddlewares/readonlyMiddleware.d.ts +1 -1
  25. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +2 -2
  26. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +8 -8
  27. package/dist/types/computedTree/index.d.ts +1 -1
  28. package/dist/types/context/index.d.ts +1 -1
  29. package/dist/types/dataModel/BaseDataModel.d.ts +3 -3
  30. package/dist/types/dataModel/DataModel.d.ts +3 -3
  31. package/dist/types/dataModel/getDataModelMetadata.d.ts +3 -3
  32. package/dist/types/dataModel/index.d.ts +4 -4
  33. package/dist/types/dataModel/utils.d.ts +1 -1
  34. package/dist/types/deepChange/index.d.ts +1 -1
  35. package/dist/types/deepChange/onDeepChange.d.ts +1 -1
  36. package/dist/types/frozen/Frozen.d.ts +1 -1
  37. package/dist/types/frozen/index.d.ts +1 -1
  38. package/dist/types/globalConfig/index.d.ts +1 -1
  39. package/dist/types/index.d.ts +24 -24
  40. package/dist/types/model/BaseModel.d.ts +5 -5
  41. package/dist/types/model/Model.d.ts +3 -3
  42. package/dist/types/model/getModelMetadata.d.ts +3 -3
  43. package/dist/types/model/index.d.ts +6 -6
  44. package/dist/types/model/mixins.d.ts +4 -4
  45. package/dist/types/model/utils.d.ts +4 -4
  46. package/dist/types/modelShared/BaseModelShared.d.ts +5 -5
  47. package/dist/types/modelShared/index.d.ts +4 -4
  48. package/dist/types/modelShared/modelDecorator.d.ts +3 -3
  49. package/dist/types/modelShared/modelInfo.d.ts +3 -3
  50. package/dist/types/modelShared/newModel.d.ts +3 -3
  51. package/dist/types/modelShared/prop.d.ts +3 -3
  52. package/dist/types/modelShared/sharedInternalModel.d.ts +4 -4
  53. package/dist/types/parent/findParent.d.ts +1 -1
  54. package/dist/types/parent/index.d.ts +9 -9
  55. package/dist/types/parent/path.d.ts +1 -1
  56. package/dist/types/patch/Patch.d.ts +1 -1
  57. package/dist/types/patch/applyPatches.d.ts +1 -1
  58. package/dist/types/patch/emitPatch.d.ts +1 -1
  59. package/dist/types/patch/index.d.ts +5 -5
  60. package/dist/types/patch/jsonPatch.d.ts +2 -2
  61. package/dist/types/patch/patchRecorder.d.ts +2 -2
  62. package/dist/types/redux/index.d.ts +2 -2
  63. package/dist/types/redux/redux.d.ts +3 -3
  64. package/dist/types/ref/Ref.d.ts +3 -3
  65. package/dist/types/ref/core.d.ts +2 -2
  66. package/dist/types/ref/customRef.d.ts +2 -2
  67. package/dist/types/ref/index.d.ts +4 -4
  68. package/dist/types/ref/rootRef.d.ts +2 -2
  69. package/dist/types/rootStore/index.d.ts +1 -1
  70. package/dist/types/snapshot/SnapshotOf.d.ts +5 -5
  71. package/dist/types/snapshot/SnapshotProcessingError.d.ts +2 -2
  72. package/dist/types/snapshot/applySnapshot.d.ts +1 -1
  73. package/dist/types/snapshot/cloneTreeValue.d.ts +1 -1
  74. package/dist/types/snapshot/fromSnapshot.d.ts +2 -2
  75. package/dist/types/snapshot/getSnapshot.d.ts +2 -2
  76. package/dist/types/snapshot/index.d.ts +8 -8
  77. package/dist/types/snapshot/onSnapshot.d.ts +1 -1
  78. package/dist/types/standardActions/index.d.ts +3 -3
  79. package/dist/types/standardActions/objectActions.d.ts +1 -1
  80. package/dist/types/transforms/asMap.d.ts +1 -1
  81. package/dist/types/transforms/asSet.d.ts +2 -2
  82. package/dist/types/transforms/bigint.d.ts +1 -1
  83. package/dist/types/transforms/date.d.ts +1 -1
  84. package/dist/types/transforms/index.d.ts +4 -4
  85. package/dist/types/treeUtils/draft.d.ts +1 -1
  86. package/dist/types/treeUtils/index.d.ts +3 -3
  87. package/dist/types/tweaker/index.d.ts +2 -2
  88. package/dist/types/tweaker/tweak.d.ts +1 -1
  89. package/dist/types/types/SnapshotTypeMismatchError.d.ts +5 -3
  90. package/dist/types/types/TypeCheckError.d.ts +2 -2
  91. package/dist/types/types/TypeChecker.d.ts +3 -3
  92. package/dist/types/types/arrayBased/typesArray.d.ts +2 -2
  93. package/dist/types/types/arrayBased/typesTuple.d.ts +2 -2
  94. package/dist/types/types/getTypeInfo.d.ts +2 -2
  95. package/dist/types/types/index.d.ts +7 -7
  96. package/dist/types/types/modelTypeMatching.d.ts +1 -0
  97. package/dist/types/types/objectBased/typesArraySet.d.ts +3 -3
  98. package/dist/types/types/objectBased/typesDataModelData.d.ts +4 -4
  99. package/dist/types/types/objectBased/typesModel.d.ts +4 -4
  100. package/dist/types/types/objectBased/typesObject.d.ts +3 -3
  101. package/dist/types/types/objectBased/typesObjectMap.d.ts +3 -3
  102. package/dist/types/types/objectBased/typesRecord.d.ts +2 -2
  103. package/dist/types/types/objectBased/typesRef.d.ts +3 -3
  104. package/dist/types/types/primitiveBased/typesEnum.d.ts +1 -1
  105. package/dist/types/types/primitiveBased/typesPrimitive.d.ts +3 -3
  106. package/dist/types/types/primitiveBased/typesRefinedPrimitive.d.ts +2 -2
  107. package/dist/types/types/schemas.d.ts +5 -5
  108. package/dist/types/types/tProp.d.ts +4 -4
  109. package/dist/types/types/typeCheck.d.ts +2 -2
  110. package/dist/types/types/types.d.ts +39 -39
  111. package/dist/types/types/utility/typeToStoredData.d.ts +2 -2
  112. package/dist/types/types/utility/typesBigInt.d.ts +1 -1
  113. package/dist/types/types/utility/typesCodec.d.ts +6 -6
  114. package/dist/types/types/utility/typesCodecCore.d.ts +4 -4
  115. package/dist/types/types/utility/typesCodecSupport.d.ts +4 -4
  116. package/dist/types/types/utility/typesDate.d.ts +2 -2
  117. package/dist/types/types/utility/typesMap.d.ts +2 -2
  118. package/dist/types/types/utility/typesMaybe.d.ts +1 -1
  119. package/dist/types/types/utility/typesOr.d.ts +2 -2
  120. package/dist/types/types/utility/typesRefinement.d.ts +3 -3
  121. package/dist/types/types/utility/typesSet.d.ts +3 -3
  122. package/dist/types/types/utility/typesSkipCheck.d.ts +2 -2
  123. package/dist/types/types/utility/typesTag.d.ts +2 -2
  124. package/dist/types/types/utility/typesUnchecked.d.ts +2 -2
  125. package/dist/types/utils/ModelPool.d.ts +1 -1
  126. package/dist/types/wrappers/ArraySet.d.ts +1 -1
  127. package/dist/types/wrappers/ObjectMap.d.ts +1 -1
  128. package/dist/types/wrappers/asMap.d.ts +1 -1
  129. package/dist/types/wrappers/asSet.d.ts +1 -1
  130. package/dist/types/wrappers/index.d.ts +4 -4
  131. package/package.json +10 -23
  132. package/src/action/modelAction.ts +1 -1
  133. package/src/action/modelFlow.ts +1 -1
  134. package/src/dataModel/BaseDataModel.ts +5 -5
  135. package/src/model/BaseModel.ts +7 -6
  136. package/src/model/mixins.ts +1 -1
  137. package/src/model/newModel.ts +1 -1
  138. package/src/modelShared/modelDecorator.ts +6 -11
  139. package/src/modelShared/modelInfo.ts +76 -0
  140. package/src/modelShared/sharedInternalModel.ts +1 -1
  141. package/src/ref/Ref.ts +1 -1
  142. package/src/snapshot/SnapshotterAndReconcilerPriority.ts +2 -1
  143. package/src/snapshot/fromModelSnapshot.ts +1 -1
  144. package/src/snapshot/fromSnapshot.ts +1 -1
  145. package/src/transforms/ImmutableDate.ts +19 -19
  146. package/src/tweaker/tweakArray.ts +1 -1
  147. package/src/tweaker/tweakFrozen.ts +1 -1
  148. package/src/tweaker/tweakModel.ts +1 -1
  149. package/src/tweaker/tweakPlainObject.ts +1 -1
  150. package/src/types/SnapshotTypeMismatchError.ts +3 -1
  151. package/src/types/arrayBased/typesArray.ts +1 -1
  152. package/src/types/arrayBased/typesTuple.ts +1 -1
  153. package/src/types/modelTypeMatching.ts +276 -0
  154. package/src/types/objectBased/typesArraySet.ts +1 -1
  155. package/src/types/objectBased/typesModel.ts +24 -7
  156. package/src/types/objectBased/typesObject.ts +1 -1
  157. package/src/types/objectBased/typesObjectMap.ts +1 -1
  158. package/src/types/objectBased/typesRecord.ts +1 -1
  159. package/src/types/objectBased/typesRef.ts +24 -5
  160. package/src/types/primitiveBased/typesPrimitive.ts +1 -1
  161. package/src/types/schemas.ts +1 -1
  162. package/src/types/utility/typesCodecCore.ts +4 -2
  163. package/src/types/utility/typesCodecSupport.ts +15 -7
  164. package/src/types/utility/typesOr.ts +40 -10
  165. package/src/types/utility/typesRefinement.ts +1 -1
  166. package/src/utils/index.ts +20 -13
  167. package/src/wrappers/ArraySet.ts +3 -5
  168. package/src/wrappers/ObjectMap.ts +3 -5
@@ -22,6 +22,82 @@ export const modelInfoByName: {
22
22
  */
23
23
  export const modelInfoByClass = new WeakMap<ModelClass<AnyModel | AnyDataModel>, ModelInfo>()
24
24
 
25
+ // registered model prototypes (looked up rather than reading `constructor`, since that might be
26
+ // a model prop)
27
+ const modelInfoByPrototype = new WeakMap<object, ModelInfo>()
28
+
29
+ // for each registered model, how many registered models below each of its base models
30
+ // (including itself, at 0) it is, by name
31
+ const ancestorDistancesByModelInfo = new WeakMap<ModelInfo, ReadonlyMap<string, number>>()
32
+ // the same, by name for the current registration of each model name
33
+ const ancestorDistancesByModelName = new Map<string, ReadonlyMap<string, number>>()
34
+
35
+ /**
36
+ * Finds the info of the first registered model class in a prototype chain.
37
+ *
38
+ * @internal
39
+ */
40
+ export function findModelInfoForPrototype(proto: object | null): ModelInfo | undefined {
41
+ for (let p = proto; p; p = Object.getPrototypeOf(p)) {
42
+ const modelInfo = modelInfoByPrototype.get(p)
43
+ if (modelInfo) {
44
+ return modelInfo
45
+ }
46
+ }
47
+ return undefined
48
+ }
49
+
50
+ /**
51
+ * Registers a model.
52
+ *
53
+ * @internal
54
+ */
55
+ export function setModelInfo(modelInfo: ModelInfo, unwrappedClass: ModelClass<any>): void {
56
+ // the base classes were registered before, so their distances are already known; names are
57
+ // used rather than classes since with hot reloading a subclass might still extend a previous
58
+ // registration of a model
59
+ const baseModelInfo = findModelInfoForPrototype(Object.getPrototypeOf(unwrappedClass.prototype))
60
+ const ancestorDistances = new Map<string, number>()
61
+ if (baseModelInfo) {
62
+ ancestorDistancesByModelInfo.get(baseModelInfo)?.forEach((distance, name) => {
63
+ ancestorDistances.set(name, distance + 1)
64
+ })
65
+ }
66
+ ancestorDistances.set(modelInfo.name, 0)
67
+ ancestorDistancesByModelInfo.set(modelInfo, ancestorDistances)
68
+ ancestorDistancesByModelName.set(modelInfo.name, ancestorDistances)
69
+
70
+ modelInfoByName[modelInfo.name] = modelInfo
71
+ modelInfoByClass.set(modelInfo.class, modelInfo)
72
+ modelInfoByClass.set(unwrappedClass, modelInfo)
73
+ modelInfoByPrototype.set(unwrappedClass.prototype, modelInfo)
74
+ }
75
+
76
+ /**
77
+ * How many registered models a model is below a base model (0 if it is that model), or -1 if
78
+ * it does not extend it.
79
+ *
80
+ * @internal
81
+ */
82
+ export function getModelSubclassDistance(modelInfo: ModelInfo, baseModelName: string): number {
83
+ return ancestorDistancesByModelInfo.get(modelInfo)?.get(baseModelName) ?? -1
84
+ }
85
+
86
+ /**
87
+ * How many registered models the model currently registered with a name is below a base model
88
+ * (0 if it is that model), -1 if it does not extend it, or `undefined` if the name is not
89
+ * registered.
90
+ *
91
+ * @internal
92
+ */
93
+ export function getModelSubclassDistanceForName(
94
+ modelName: string,
95
+ baseModelName: string
96
+ ): number | undefined {
97
+ const ancestorDistances = ancestorDistancesByModelName.get(modelName)
98
+ return ancestorDistances ? (ancestorDistances.get(baseModelName) ?? -1) : undefined
99
+ }
100
+
25
101
  /**
26
102
  * Returns the model info for a model type name, or `undefined` if not found.
27
103
  *
@@ -11,8 +11,8 @@ import type { DataModelMetadata } from "../dataModel/getDataModelMetadata"
11
11
  import { getGlobalConfig } from "../globalConfig/globalConfig"
12
12
  import { type AnyModel, BaseModel, baseModelPropNames } from "../model/BaseModel"
13
13
  import type { ModelMetadata } from "../model/getModelMetadata"
14
- import type { ModelConstructorOptions } from "../model/ModelConstructorOptions"
15
14
  import { modelTypeKey } from "../model/metadata"
15
+ import type { ModelConstructorOptions } from "../model/ModelConstructorOptions"
16
16
  import { typesObject } from "../types/objectBased/typesObject"
17
17
  import { typesString } from "../types/primitiveBased/typesPrimitive"
18
18
  import type { AnyType } from "../types/schemas"
package/src/ref/Ref.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Model } from "../model/Model"
2
1
  import { modelTypeKey } from "../model/metadata"
2
+ import { Model } from "../model/Model"
3
3
  import type { ModelClass } from "../modelShared/BaseModelShared"
4
4
  import { typesString } from "../types/primitiveBased/typesPrimitive"
5
5
  import { tProp } from "../types/tProp"
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @internal
3
3
  */
4
- // biome-ignore lint/style/useEnumInitializers: that's ok, it is not serialized
4
+ /* oxlint-disable typescript/prefer-enum-initializers -- that's ok, it is not serialized */
5
5
  export enum SnapshotterAndReconcilerPriority {
6
6
  Array,
7
7
  Frozen,
8
8
  Model,
9
9
  PlainObject,
10
10
  }
11
+ /* oxlint-enable typescript/prefer-enum-initializers */
@@ -1,7 +1,7 @@
1
1
  import type { AnyModel } from "../model/BaseModel"
2
2
  import { getModelIdPropertyName } from "../model/getModelMetadata"
3
- import type { ModelConstructorOptions } from "../model/ModelConstructorOptions"
4
3
  import { modelTypeKey } from "../model/metadata"
4
+ import type { ModelConstructorOptions } from "../model/ModelConstructorOptions"
5
5
  import { getModelOrSnapshotTypeAndId, getSnapshotModelType } from "../model/utils"
6
6
  import type { ModelClass } from "../modelShared/BaseModelShared"
7
7
  import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
@@ -83,7 +83,7 @@ export function fromSnapshot<T>(arg1: any, arg2: any, arg3?: any): T {
83
83
  let snapshot: any
84
84
  let unprocessedSnapshot: unknown
85
85
  let options: Partial<FromSnapshotOptions> | undefined
86
- // biome-ignore lint/complexity/noArguments: overload resolution needs the real call arity to distinguish omitted snapshots from omitted type args.
86
+ // oxlint-disable-next-line eslint/prefer-rest-params -- overload resolution needs the real call arity to distinguish omitted snapshots from omitted type args.
87
87
  const standardType = arguments.length >= 2 ? resolveStandardTypeNoThrow(arg1) : undefined
88
88
 
89
89
  if (isLateTypeChecker(arg1) || arg1 instanceof TypeChecker || standardType) {
@@ -3,25 +3,25 @@ import { failure } from "../utils"
3
3
  /**
4
4
  * @internal
5
5
  */
6
- export interface ImmutableDate
7
- extends Omit<
8
- Date,
9
- | "setTime"
10
- | "setMilliseconds"
11
- | "setUTCMilliseconds"
12
- | "setSeconds"
13
- | "setUTCSeconds"
14
- | "setMinutes"
15
- | "setUTCMinutes"
16
- | "setHours"
17
- | "setUTCHours"
18
- | "setDate"
19
- | "setUTCDate"
20
- | "setMonth"
21
- | "setUTCMonth"
22
- | "setFullYear"
23
- | "setUTCFullYear"
24
- > {}
6
+ // oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- the class implements this narrowed Date API.
7
+ export interface ImmutableDate extends Omit<
8
+ Date,
9
+ | "setTime"
10
+ | "setMilliseconds"
11
+ | "setUTCMilliseconds"
12
+ | "setSeconds"
13
+ | "setUTCSeconds"
14
+ | "setMinutes"
15
+ | "setUTCMinutes"
16
+ | "setHours"
17
+ | "setUTCHours"
18
+ | "setDate"
19
+ | "setUTCDate"
20
+ | "setMonth"
21
+ | "setUTCMonth"
22
+ | "setFullYear"
23
+ | "setUTCFullYear"
24
+ > {}
25
25
 
26
26
  const errMessage = "this Date object is immutable"
27
27
 
@@ -36,7 +36,6 @@ import { failure, inDevMode, isArray, isPrimitive } from "../utils"
36
36
  import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
37
37
  import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
38
38
  import { activateHandlers, areHandlersActive, runningWithoutSnapshotOrPatches } from "./core"
39
- import { TweakerPriority } from "./TweakerPriority"
40
39
  import { markAsTweakedObject } from "./treeNodeMetadata"
41
40
  import {
42
41
  assertCanAttach,
@@ -46,6 +45,7 @@ import {
46
45
  tweak,
47
46
  } from "./tweak"
48
47
  import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
48
+ import { TweakerPriority } from "./TweakerPriority"
49
49
  import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
50
50
 
51
51
  /**
@@ -2,9 +2,9 @@ import { Frozen, frozenKey } from "../frozen/Frozen"
2
2
  import type { ParentPath } from "../parent/path"
3
3
  import { setParent } from "../parent/setParent"
4
4
  import { setNewInternalSnapshot } from "../snapshot/internal"
5
- import { TweakerPriority } from "./TweakerPriority"
6
5
  import { markAsTweakedObject } from "./treeNodeMetadata"
7
6
  import { registerTweaker } from "./tweak"
7
+ import { TweakerPriority } from "./TweakerPriority"
8
8
 
9
9
  /**
10
10
  * @internal
@@ -1,9 +1,9 @@
1
1
  import { isModel } from "../model/utils"
2
2
  import type { ParentPath } from "../parent/path"
3
3
  import { setParent } from "../parent/setParent"
4
- import { TweakerPriority } from "./TweakerPriority"
5
4
  import { markAsTweakedObject } from "./treeNodeMetadata"
6
5
  import { registerTweaker } from "./tweak"
6
+ import { TweakerPriority } from "./TweakerPriority"
7
7
 
8
8
  /**
9
9
  * @internal
@@ -43,10 +43,10 @@ import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedT
43
43
  import { setIfDifferent } from "../utils/setIfDifferent"
44
44
  import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
45
45
  import { activateHandlers, runningWithoutSnapshotOrPatches } from "./core"
46
- import { TweakerPriority } from "./TweakerPriority"
47
46
  import { markAsTweakedObject, treeNodeMetadata } from "./treeNodeMetadata"
48
47
  import { assertCanAttachValue, registerTweaker, tweak } from "./tweak"
49
48
  import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
49
+ import { TweakerPriority } from "./TweakerPriority"
50
50
  import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
51
51
 
52
52
  /**
@@ -14,7 +14,9 @@ export interface SnapshotTypeMismatchErrorData {
14
14
  }
15
15
 
16
16
  /**
17
- * Thrown when a snapshot value does not match any of the expected types in a union (extends `MobxKeystoneError`).
17
+ * Thrown when a snapshot value does not match any of the expected types in a union, or when a
18
+ * model snapshot names a model (`$modelType`) that is not the expected model or a subclass of it
19
+ * (extends `MobxKeystoneError`).
18
20
  *
19
21
  * Use `instanceof SnapshotTypeMismatchError` to distinguish snapshot type-mismatch errors
20
22
  * from other `MobxKeystoneError` instances.
@@ -5,7 +5,6 @@ import { createChunkedArrayCachedCheck } from "../createCachedTypeCheck"
5
5
  import { getTypeInfo } from "../getTypeInfo"
6
6
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
7
  import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
8
- import { TypeCheckError } from "../TypeCheckError"
9
8
  import {
10
9
  lateTypeChecker,
11
10
  snapshotProcessorPlan,
@@ -14,6 +13,7 @@ import {
14
13
  TypeInfo,
15
14
  type TypeInfoGen,
16
15
  } from "../TypeChecker"
16
+ import { TypeCheckError } from "../TypeCheckError"
17
17
  import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
18
18
 
19
19
  /**
@@ -5,7 +5,6 @@ import { createWholeContainerCachedCheck } from "../createCachedTypeCheck"
5
5
  import { getTypeInfo } from "../getTypeInfo"
6
6
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
7
  import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
8
- import { TypeCheckError } from "../TypeCheckError"
9
8
  import {
10
9
  lateTypeChecker,
11
10
  snapshotProcessorPlan,
@@ -14,6 +13,7 @@ import {
14
13
  TypeInfo,
15
14
  type TypeInfoGen,
16
15
  } from "../TypeChecker"
16
+ import { TypeCheckError } from "../TypeCheckError"
17
17
  import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
18
18
 
19
19
  /**
@@ -0,0 +1,276 @@
1
+ import { modelTypeKey } from "../model/metadata"
2
+ import {
3
+ findModelInfoForPrototype,
4
+ getModelSubclassDistance,
5
+ getModelSubclassDistanceForName,
6
+ } from "../modelShared/modelInfo"
7
+ import { isObject } from "../utils"
8
+
9
+ /**
10
+ * Model type matching pass used while unions match a value against their branches, so they can
11
+ * prefer the branches whose models are the closest to the ones named by the snapshot
12
+ * (`$modelType`):
13
+ * - `none`: not matching union branches; model types match snapshots of their own model and of
14
+ * its subclasses.
15
+ * - `exact`: model types only match snapshots of their own model.
16
+ * - `closest`: model types also match snapshots of subclasses, adding how far the subclass is
17
+ * from the model to `subclassDistance`, so unions can pick the branch with the lowest total.
18
+ *
19
+ * @internal
20
+ */
21
+ export type ModelTypeMatchingPass = "none" | "exact" | "closest"
22
+
23
+ let currentPass: ModelTypeMatchingPass = "none"
24
+ let subclassDistance = 0
25
+ let rejectedModelSnapshot = false
26
+
27
+ /**
28
+ * @internal
29
+ */
30
+ export function getModelTypeMatchingPass(): ModelTypeMatchingPass {
31
+ return currentPass
32
+ }
33
+
34
+ /**
35
+ * Records that a model type matched a snapshot of a subclass `distance` levels below it.
36
+ *
37
+ * @internal
38
+ */
39
+ export function addModelSubclassDistance(distance: number): void {
40
+ if (currentPass === "closest") {
41
+ subclassDistance += distance
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Records, while unions match their branches, that a model type (or ref) rejected a model
47
+ * snapshot that some other branch could accept: one of a subclass in the exact pass (it would
48
+ * match it in the closest pass), or one of another registered model.
49
+ *
50
+ * @internal
51
+ */
52
+ export function rejectModelSnapshot(): void {
53
+ if (currentPass !== "none") {
54
+ rejectedModelSnapshot = true
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Runs a value match, returning whether it matched, or `undefined` if it did not because a model
60
+ * type (or ref) rejected a model snapshot (see `rejectModelSnapshot`).
61
+ *
62
+ * @internal
63
+ */
64
+ export function matchUnlessModelSnapshotRejected<A, B>(
65
+ fn: (a: A, b: B) => unknown,
66
+ a: A,
67
+ b: B
68
+ ): boolean | undefined {
69
+ const prevRejectedModelSnapshot = rejectedModelSnapshot
70
+ rejectedModelSnapshot = false
71
+ try {
72
+ const matched = !!fn(a, b)
73
+ return matched || !rejectedModelSnapshot ? matched : undefined
74
+ } finally {
75
+ // let outer matches know too
76
+ rejectedModelSnapshot ||= prevRejectedModelSnapshot
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Whether a snapshot `$modelType` matches a model (`modelName`): its own model or, outside the
82
+ * exact pass, a subclass of it. Registered names are compared rather than classes, since with
83
+ * hot reloading a subclass might still extend a previous registration of the model.
84
+ *
85
+ * @internal
86
+ */
87
+ export function snapshotModelTypeMatches(snModelType: unknown, modelName: string): boolean {
88
+ if (typeof snModelType !== "string") {
89
+ return false
90
+ }
91
+ if (snModelType === modelName) {
92
+ return true
93
+ }
94
+ const distance = getModelSubclassDistanceForName(snModelType, modelName)
95
+ if (distance === undefined) {
96
+ return false
97
+ }
98
+ if (distance < 0 || currentPass === "exact") {
99
+ rejectModelSnapshot()
100
+ return false
101
+ }
102
+ addModelSubclassDistance(distance)
103
+ return true
104
+ }
105
+
106
+ /**
107
+ * Whether a snapshot `$modelType` names a registered model that is neither a model
108
+ * (`modelName`) nor a subclass of it, so processing the snapshot as that model would silently
109
+ * create an unrelated model (unregistered names are reported later by the model snapshotter).
110
+ *
111
+ * @internal
112
+ */
113
+ export function isUnrelatedSnapshotModelType(snModelType: unknown, modelName: string): boolean {
114
+ return (
115
+ typeof snModelType === "string" &&
116
+ snModelType !== modelName &&
117
+ (getModelSubclassDistanceForName(snModelType, modelName) ?? 0) < 0
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Whether a model instance is of a model (`modelName`) or of a subclass of it, comparing
123
+ * registered names, since with hot reloading the instance model might still extend a previous
124
+ * registration of the model.
125
+ *
126
+ * @internal
127
+ */
128
+ export function isModelInstanceOfModelName(value: object, modelName: string): boolean {
129
+ const valueModelInfo = findModelInfoForPrototype(Object.getPrototypeOf(value))
130
+ return !!valueModelInfo && getModelSubclassDistance(valueModelInfo, modelName) >= 0
131
+ }
132
+
133
+ /**
134
+ * Whether a value is a snapshot that names its model (`$modelType`).
135
+ *
136
+ * @internal
137
+ */
138
+ export function hasSnapshotModelType(value: unknown): boolean {
139
+ return isObject(value) && typeof value[modelTypeKey] === "string"
140
+ }
141
+
142
+ /**
143
+ * Runs a function in the given model type matching pass, restoring the current one afterwards.
144
+ *
145
+ * @internal
146
+ */
147
+ export function runInModelTypeMatchingPass<A, B, R>(
148
+ pass: ModelTypeMatchingPass,
149
+ fn: (a: A, b: B) => R,
150
+ a: A,
151
+ b: B
152
+ ): R {
153
+ if (pass === "none" && currentPass === "none") {
154
+ return fn(a, b)
155
+ }
156
+
157
+ const prevPass = currentPass
158
+ const prevDistance = subclassDistance
159
+ const prevRejectedModelSnapshot = rejectedModelSnapshot
160
+ currentPass = pass
161
+ subclassDistance = 0
162
+ rejectedModelSnapshot = false
163
+ try {
164
+ return fn(a, b)
165
+ } finally {
166
+ currentPass = prevPass
167
+ subclassDistance = prevDistance
168
+ rejectedModelSnapshot = prevRejectedModelSnapshot
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Finds the union branch (candidate) that matches a value, preferring branches that match the
174
+ * exact models named by the snapshot (branches without models count as exact matches), then
175
+ * the branches whose models are the closest base models of the snapshot ones (the fewest
176
+ * subclass levels away in total), and the first one on ties.
177
+ *
178
+ * @internal
179
+ */
180
+ export function findClosestModelTypeMatch<T, R>(
181
+ candidates: ReadonlyArray<T>,
182
+ match: (candidate: T, value: unknown) => R | null | undefined,
183
+ value: unknown
184
+ ): R | undefined {
185
+ switch (currentPass) {
186
+ case "exact":
187
+ // nested inside an exact pass
188
+ return findNestedMatch(findFirstMatch, candidates, match, value, 0)
189
+ case "closest":
190
+ // nested inside a closest pass
191
+ return findNestedMatch(findClosestMatch, candidates, match, value, 0)
192
+ default:
193
+ try {
194
+ currentPass = "exact"
195
+ const exactMatch = findFirstMatch(candidates, match, value)
196
+ if (exactMatch != null) {
197
+ return exactMatch
198
+ }
199
+
200
+ currentPass = "closest"
201
+ subclassDistance = 0
202
+ // no branch matched exactly, so none can be less than one subclass level away
203
+ return findClosestMatch(candidates, match, value, 1)
204
+ } finally {
205
+ currentPass = "none"
206
+ subclassDistance = 0
207
+ rejectedModelSnapshot = false
208
+ }
209
+ }
210
+ }
211
+
212
+ function findNestedMatch<T, R>(
213
+ find: (
214
+ candidates: ReadonlyArray<T>,
215
+ match: (candidate: T, value: unknown) => R | null | undefined,
216
+ value: unknown,
217
+ minPossibleDistance: number
218
+ ) => R | undefined,
219
+ candidates: ReadonlyArray<T>,
220
+ match: (candidate: T, value: unknown) => R | null | undefined,
221
+ value: unknown,
222
+ minPossibleDistance: number
223
+ ): R | undefined {
224
+ const prevRejectedModelSnapshot = rejectedModelSnapshot
225
+ const result = find(candidates, match, value, minPossibleDistance)
226
+ if (result != null) {
227
+ // model snapshots rejected by other branches do not matter then
228
+ rejectedModelSnapshot = prevRejectedModelSnapshot
229
+ }
230
+ return result
231
+ }
232
+
233
+ /**
234
+ * Finds the first candidate that matches a value.
235
+ *
236
+ * @internal
237
+ */
238
+ export function findFirstMatch<T, R>(
239
+ candidates: ReadonlyArray<T>,
240
+ match: (candidate: T, value: unknown) => R | null | undefined,
241
+ value: unknown
242
+ ): R | undefined {
243
+ for (let i = 0; i < candidates.length; i++) {
244
+ const result = match(candidates[i], value)
245
+ if (result != null) {
246
+ return result
247
+ }
248
+ }
249
+ return undefined
250
+ }
251
+
252
+ function findClosestMatch<T, R>(
253
+ candidates: ReadonlyArray<T>,
254
+ match: (candidate: T, value: unknown) => R | null | undefined,
255
+ value: unknown,
256
+ minPossibleDistance: number
257
+ ): R | undefined {
258
+ const prevDistance = subclassDistance
259
+ let best: R | undefined
260
+ let bestDistance = Number.POSITIVE_INFINITY
261
+
262
+ for (let i = 0; i < candidates.length; i++) {
263
+ subclassDistance = 0
264
+ const result = match(candidates[i], value)
265
+ if (result != null && subclassDistance < bestDistance) {
266
+ best = result
267
+ bestDistance = subclassDistance
268
+ if (bestDistance <= minPossibleDistance) {
269
+ break
270
+ }
271
+ }
272
+ }
273
+
274
+ subclassDistance = prevDistance + (best != null ? bestDistance : 0)
275
+ return best
276
+ }
@@ -7,7 +7,6 @@ import { typesArray } from "../arrayBased/typesArray"
7
7
  import { getTypeInfo } from "../getTypeInfo"
8
8
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
9
9
  import type { AnyStandardType, AnyType, ModelType, TypeToData } from "../schemas"
10
- import { TypeCheckError } from "../TypeCheckError"
11
10
  import {
12
11
  lateTypeChecker,
13
12
  snapshotProcessorPlan,
@@ -16,6 +15,7 @@ import {
16
15
  TypeInfo,
17
16
  type TypeInfoGen,
18
17
  } from "../TypeChecker"
18
+ import { TypeCheckError } from "../TypeCheckError"
19
19
  import { resolveCodecSupport } from "../utility/typesCodec"
20
20
  import { typesObject } from "./typesObject"
21
21
 
@@ -2,16 +2,21 @@ import type { O } from "ts-toolbelt"
2
2
  import type { AnyModel } from "../../model/BaseModel"
3
3
  import { getModelMetadata } from "../../model/getModelMetadata"
4
4
  import { modelTypeKey } from "../../model/metadata"
5
- import { isModelClass } from "../../model/utils"
5
+ import { isModel, isModelClass } from "../../model/utils"
6
6
  import type { ModelClass } from "../../modelShared/BaseModelShared"
7
7
  import { modelInfoByClass } from "../../modelShared/modelInfo"
8
8
  import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo"
9
9
  import { noDefaultValue } from "../../modelShared/prop"
10
10
  import { isObject, lazy, setProtoProp } from "../../utils"
11
11
  import { getTypeInfo } from "../getTypeInfo"
12
+ import {
13
+ isModelInstanceOfModelName,
14
+ isUnrelatedSnapshotModelType,
15
+ snapshotModelTypeMatches,
16
+ } from "../modelTypeMatching"
12
17
  import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker"
13
18
  import type { AnyStandardType, ModelType } from "../schemas"
14
- import { TypeCheckError } from "../TypeCheckError"
19
+ import { SnapshotTypeMismatchError } from "../SnapshotTypeMismatchError"
15
20
  import {
16
21
  lateTypeChecker,
17
22
  TypeChecker,
@@ -19,6 +24,7 @@ import {
19
24
  TypeInfo,
20
25
  type TypeInfoGen,
21
26
  } from "../TypeChecker"
27
+ import { TypeCheckError } from "../TypeCheckError"
22
28
 
23
29
  const cachedModelTypeChecker = new WeakMap<ModelClass<AnyModel>, TypeChecker>()
24
30
  const modelStandardTypes = new WeakSet<object>()
@@ -73,7 +79,10 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
73
79
  TypeCheckerBaseType.Object,
74
80
 
75
81
  (value, path, typeCheckedValue) => {
76
- if (!(value instanceof modelClazz)) {
82
+ if (
83
+ !(value instanceof modelClazz) &&
84
+ !(isModel(value) && isModelInstanceOfModelName(value, modelInfo.name))
85
+ ) {
77
86
  return new TypeCheckError({
78
87
  path,
79
88
  expectedTypeName: typeName,
@@ -96,9 +105,9 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
96
105
  return null
97
106
  }
98
107
 
99
- if (value[modelTypeKey] !== undefined) {
100
- // fast check
101
- return value[modelTypeKey] === modelInfo.name ? thisTc : null
108
+ const snModelType = value[modelTypeKey]
109
+ if (snModelType !== undefined) {
110
+ return snapshotModelTypeMatches(snModelType, modelInfo.name) ? thisTc : null
102
111
  }
103
112
 
104
113
  if (resolvedDataTypeChecker) {
@@ -112,7 +121,15 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
112
121
  // No withErrorPathSegment wrapping needed here — model snapshot processors
113
122
  // delegate to the data type checker, which handles path segments at the property level.
114
123
  (sn) => {
115
- if (sn[modelTypeKey]) {
124
+ const snModelType = sn[modelTypeKey]
125
+ if (typeof snModelType === "string") {
126
+ // the snapshot names its own model; make sure it is this model or a subclass of it
127
+ if (isUnrelatedSnapshotModelType(snModelType, modelInfo.name)) {
128
+ throw new SnapshotTypeMismatchError({
129
+ expectedTypeName: typeName,
130
+ actualValue: sn,
131
+ })
132
+ }
116
133
  return sn
117
134
  } else {
118
135
  return {
@@ -23,7 +23,6 @@ import type {
23
23
  ObjectTypeFunction,
24
24
  TypeToData,
25
25
  } from "../schemas"
26
- import { TypeCheckError } from "../TypeCheckError"
27
26
  import {
28
27
  type LateTypeChecker,
29
28
  lateTypeChecker,
@@ -34,6 +33,7 @@ import {
34
33
  TypeInfo,
35
34
  type TypeInfoGen,
36
35
  } from "../TypeChecker"
36
+ import { TypeCheckError } from "../TypeCheckError"
37
37
  import { prependPathElementToTypeCheckError } from "../typeCheckErrorUtils"
38
38
 
39
39
  const needsObservableObjectKeyTracking = getMobxVersion() < 5