mobx-keystone 2.2.0 → 2.3.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.
- package/CHANGELOG.md +6 -0
- package/dist/mobx-keystone.esm.js +282 -29
- package/dist/mobx-keystone.esm.mjs +282 -29
- package/dist/mobx-keystone.umd.js +285 -28
- package/dist/types/types/SnapshotTypeMismatchError.d.ts +3 -1
- package/dist/types/types/modelTypeMatching.d.ts +1 -0
- package/package.json +10 -10
- package/src/modelShared/modelDecorator.ts +2 -5
- package/src/modelShared/modelInfo.ts +75 -0
- package/src/types/SnapshotTypeMismatchError.ts +3 -1
- package/src/types/modelTypeMatching.ts +276 -0
- package/src/types/objectBased/typesModel.ts +23 -6
- package/src/types/objectBased/typesRef.ts +23 -4
- package/src/types/utility/typesCodecCore.ts +3 -1
- package/src/types/utility/typesCodecSupport.ts +13 -7
- package/src/types/utility/typesOr.ts +38 -8
|
@@ -7,7 +7,9 @@ export interface SnapshotTypeMismatchErrorData {
|
|
|
7
7
|
modelTrail?: readonly string[];
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Thrown when a snapshot value does not match any of the expected types in a union
|
|
10
|
+
* Thrown when a snapshot value does not match any of the expected types in a union, or when a
|
|
11
|
+
* model snapshot names a model (`$modelType`) that is not the expected model or a subclass of it
|
|
12
|
+
* (extends `MobxKeystoneError`).
|
|
11
13
|
*
|
|
12
14
|
* Use `instanceof SnapshotTypeMismatchError` to distinguish snapshot type-mismatch errors
|
|
13
15
|
* from other `MobxKeystoneError` instances.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobx-keystone",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mobx",
|
|
@@ -67,17 +67,17 @@
|
|
|
67
67
|
"mobx": "^7.0.0 || ^6.0.0 || ^5.0.0 || ^4.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@babel/core": "^8.0.
|
|
70
|
+
"@babel/core": "^8.0.6",
|
|
71
71
|
"@babel/plugin-proposal-decorators": "^8.0.2",
|
|
72
72
|
"@babel/plugin-transform-class-properties": "^8.0.1",
|
|
73
|
-
"@babel/plugin-transform-class-static-block": "^8.0.
|
|
74
|
-
"@babel/plugin-transform-typescript": "^8.0.
|
|
75
|
-
"@babel/preset-env": "^8.0.
|
|
73
|
+
"@babel/plugin-transform-class-static-block": "^8.0.5",
|
|
74
|
+
"@babel/plugin-transform-typescript": "^8.0.6",
|
|
75
|
+
"@babel/preset-env": "^8.0.6",
|
|
76
76
|
"@swc/core": "^1.16.2",
|
|
77
77
|
"@types/babel__core": "^7.20.5",
|
|
78
|
-
"@types/node": "^26.
|
|
78
|
+
"@types/node": "^26.6.2",
|
|
79
79
|
"@typescript/typescript6": "^6.0.2",
|
|
80
|
-
"@vitest/coverage-v8": "^5.0.
|
|
80
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
81
81
|
"expect-type": "^1.4.0",
|
|
82
82
|
"mobx-v4": "npm:mobx@^4.15.7",
|
|
83
83
|
"mobx-v5": "npm:mobx@^5.15.7",
|
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
"typedoc-docusaurus-theme": "^1.4.3",
|
|
90
90
|
"typedoc-plugin-markdown": "^4.13.1",
|
|
91
91
|
"typescript": "^7.0.2",
|
|
92
|
-
"vite": "^8.
|
|
93
|
-
"vite-plugin-dts": "^5.1.
|
|
94
|
-
"vitest": "^5.0.
|
|
92
|
+
"vite": "^8.3.1",
|
|
93
|
+
"vite-plugin-dts": "^5.1.1",
|
|
94
|
+
"vitest": "^5.0.1"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
97
|
"fast-deep-equal": "^3.1.3",
|
|
@@ -9,7 +9,7 @@ import type { AnyModel } from "../model/BaseModel"
|
|
|
9
9
|
import { modelTypeKey } from "../model/metadata"
|
|
10
10
|
import { isModelClass } from "../model/utils"
|
|
11
11
|
import { type ModelClass, modelInitializedSymbol } from "../modelShared/BaseModelShared"
|
|
12
|
-
import { modelInfoByClass, modelInfoByName } from "../modelShared/modelInfo"
|
|
12
|
+
import { modelInfoByClass, modelInfoByName, setModelInfo } from "../modelShared/modelInfo"
|
|
13
13
|
import {
|
|
14
14
|
modelUnwrappedClassSymbol,
|
|
15
15
|
runAfterModelDecoratorSymbol,
|
|
@@ -167,10 +167,7 @@ const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>(
|
|
|
167
167
|
class: decoratedClass,
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
modelInfoByClass.set(decoratedClass, modelInfo)
|
|
173
|
-
modelInfoByClass.set(clazz, modelInfo)
|
|
170
|
+
setModelInfo(modelInfo, clazz)
|
|
174
171
|
|
|
175
172
|
runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
|
|
176
173
|
|
|
@@ -22,6 +22,81 @@ 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
|
+
baseModelInfo &&
|
|
62
|
+
ancestorDistancesByModelInfo.get(baseModelInfo)?.forEach((distance, name) => {
|
|
63
|
+
ancestorDistances.set(name, distance + 1)
|
|
64
|
+
})
|
|
65
|
+
ancestorDistances.set(modelInfo.name, 0)
|
|
66
|
+
ancestorDistancesByModelInfo.set(modelInfo, ancestorDistances)
|
|
67
|
+
ancestorDistancesByModelName.set(modelInfo.name, ancestorDistances)
|
|
68
|
+
|
|
69
|
+
modelInfoByName[modelInfo.name] = modelInfo
|
|
70
|
+
modelInfoByClass.set(modelInfo.class, modelInfo)
|
|
71
|
+
modelInfoByClass.set(unwrappedClass, modelInfo)
|
|
72
|
+
modelInfoByPrototype.set(unwrappedClass.prototype, modelInfo)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How many registered models a model is below a base model (0 if it is that model), or -1 if
|
|
77
|
+
* it does not extend it.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export function getModelSubclassDistance(modelInfo: ModelInfo, baseModelName: string): number {
|
|
82
|
+
return ancestorDistancesByModelInfo.get(modelInfo)?.get(baseModelName) ?? -1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* How many registered models the model currently registered with a name is below a base model
|
|
87
|
+
* (0 if it is that model), -1 if it does not extend it, or `undefined` if the name is not
|
|
88
|
+
* registered.
|
|
89
|
+
*
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function getModelSubclassDistanceForName(
|
|
93
|
+
modelName: string,
|
|
94
|
+
baseModelName: string
|
|
95
|
+
): number | undefined {
|
|
96
|
+
const ancestorDistances = ancestorDistancesByModelName.get(modelName)
|
|
97
|
+
return ancestorDistances ? (ancestorDistances.get(baseModelName) ?? -1) : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
25
100
|
/**
|
|
26
101
|
* Returns the model info for a model type name, or `undefined` if not found.
|
|
27
102
|
*
|
|
@@ -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
|
|
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.
|
|
@@ -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
|
+
}
|
|
@@ -2,14 +2,20 @@ 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"
|
|
18
|
+
import { SnapshotTypeMismatchError } from "../SnapshotTypeMismatchError"
|
|
13
19
|
import type { AnyStandardType, ModelType } from "../schemas"
|
|
14
20
|
import { TypeCheckError } from "../TypeCheckError"
|
|
15
21
|
import {
|
|
@@ -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 (
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
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
|
-
|
|
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 {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { modelTypeKey } from "../../model/metadata"
|
|
2
|
+
import { isModel } from "../../model/utils"
|
|
2
3
|
import { modelInfoByClass } from "../../modelShared/modelInfo"
|
|
3
4
|
import type { Ref, RefConstructor } from "../../ref/Ref"
|
|
4
5
|
import { isObject } from "../../utils"
|
|
6
|
+
import {
|
|
7
|
+
isModelInstanceOfModelName,
|
|
8
|
+
isUnrelatedSnapshotModelType,
|
|
9
|
+
snapshotModelTypeMatches,
|
|
10
|
+
} from "../modelTypeMatching"
|
|
5
11
|
import { typesString } from "../primitiveBased/typesPrimitive"
|
|
6
12
|
import { resolveTypeChecker } from "../resolveTypeChecker"
|
|
13
|
+
import { SnapshotTypeMismatchError } from "../SnapshotTypeMismatchError"
|
|
7
14
|
import type { ModelType } from "../schemas"
|
|
8
15
|
import { TypeCheckError } from "../TypeCheckError"
|
|
9
16
|
import { TypeChecker, TypeCheckerBaseType, TypeInfo } from "../TypeChecker"
|
|
@@ -36,7 +43,10 @@ export function typesRef<O extends object>(refConstructor: RefConstructor<O>): M
|
|
|
36
43
|
TypeCheckerBaseType.Object,
|
|
37
44
|
|
|
38
45
|
(value, path, typeCheckedValue) => {
|
|
39
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
!(value instanceof refConstructor.refClass) &&
|
|
48
|
+
!(isModel(value) && isModelInstanceOfModelName(value, modelInfo.name))
|
|
49
|
+
) {
|
|
40
50
|
return new TypeCheckError({
|
|
41
51
|
path,
|
|
42
52
|
expectedTypeName: typeName,
|
|
@@ -56,16 +66,25 @@ export function typesRef<O extends object>(refConstructor: RefConstructor<O>): M
|
|
|
56
66
|
return null
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
const snModelType = obj[modelTypeKey]
|
|
70
|
+
if (snModelType !== undefined) {
|
|
60
71
|
// fast check
|
|
61
|
-
return
|
|
72
|
+
return snapshotModelTypeMatches(snModelType, modelInfo.name) ? thisTc : null
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
return refDataTypeChecker.snapshotType(obj) ? thisTc : null
|
|
65
76
|
},
|
|
66
77
|
|
|
67
78
|
(sn: Record<string, unknown>) => {
|
|
68
|
-
|
|
79
|
+
const snModelType = sn[modelTypeKey]
|
|
80
|
+
if (typeof snModelType === "string") {
|
|
81
|
+
// the snapshot names its own model; make sure it is this ref or a subclass of it
|
|
82
|
+
if (isUnrelatedSnapshotModelType(snModelType, modelInfo.name)) {
|
|
83
|
+
throw new SnapshotTypeMismatchError({
|
|
84
|
+
expectedTypeName: typeName,
|
|
85
|
+
actualValue: sn,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
69
88
|
return sn
|
|
70
89
|
} else {
|
|
71
90
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModelPropTransform } from "../../modelShared/prop"
|
|
2
2
|
import { lazy } from "../../utils"
|
|
3
3
|
import { getTypeInfo } from "../getTypeInfo"
|
|
4
|
+
import { runInModelTypeMatchingPass } from "../modelTypeMatching"
|
|
4
5
|
import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
|
|
5
6
|
import type {
|
|
6
7
|
AnyStandardType,
|
|
@@ -145,7 +146,8 @@ export function createCodecType<TEncodedType extends AnyType, TRuntime>(
|
|
|
145
146
|
() => typeName,
|
|
146
147
|
typeInfoGen,
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
// user code, so it must not see the model type matching pass of an outer union
|
|
150
|
+
(value) => (runInModelTypeMatchingPass("none", is, value, undefined) ? thisTc : null),
|
|
149
151
|
|
|
150
152
|
(snapshot) => {
|
|
151
153
|
const processedSnapshot = encodedTypeChecker.fromSnapshotProcessor(snapshot)
|
|
@@ -5,6 +5,7 @@ import { setIfDifferent } from "../../utils/setIfDifferent"
|
|
|
5
5
|
import { ArrayTypeInfo, typesArray } from "../arrayBased/typesArray"
|
|
6
6
|
import { TupleTypeInfo, typesTuple } from "../arrayBased/typesTuple"
|
|
7
7
|
import { getTypeInfo } from "../getTypeInfo"
|
|
8
|
+
import { findClosestModelTypeMatch, findFirstMatch } from "../modelTypeMatching"
|
|
8
9
|
import { isDataModelDataStandardType } from "../objectBased/typesDataModelData"
|
|
9
10
|
import { isModelStandardType } from "../objectBased/typesModel"
|
|
10
11
|
import { ObjectTypeInfo, typesObject } from "../objectBased/typesObject"
|
|
@@ -577,6 +578,13 @@ function createCodecLeafRuntimeAdapter(
|
|
|
577
578
|
}
|
|
578
579
|
}
|
|
579
580
|
|
|
581
|
+
function matchCodecSupport(
|
|
582
|
+
support: ResolvedCodecSupport,
|
|
583
|
+
value: unknown
|
|
584
|
+
): ResolvedCodecSupport | undefined {
|
|
585
|
+
return resolveTypeChecker(support.storedType).snapshotType(value) ? support : undefined
|
|
586
|
+
}
|
|
587
|
+
|
|
580
588
|
function createOrRuntimeAdapter(
|
|
581
589
|
childTypes: ReadonlyArray<AnyStandardType>,
|
|
582
590
|
getChildSupports: () => ReadonlyArray<ResolvedCodecSupport>,
|
|
@@ -607,14 +615,12 @@ function createOrRuntimeAdapter(
|
|
|
607
615
|
}
|
|
608
616
|
|
|
609
617
|
const supports = getChildSupports()
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
}
|
|
618
|
+
// prefer branches whose (possibly nested) models are the closest to the snapshot ones
|
|
619
|
+
const matchingSupport = isObject(value)
|
|
620
|
+
? findClosestModelTypeMatch(supports, matchCodecSupport, value)
|
|
621
|
+
: findFirstMatch(supports, matchCodecSupport, value)
|
|
616
622
|
|
|
617
|
-
return supports[0]
|
|
623
|
+
return matchingSupport ?? supports[0]
|
|
618
624
|
}
|
|
619
625
|
|
|
620
626
|
return {
|