-
Notifications
You must be signed in to change notification settings - Fork 586
Expand file tree
/
Copy pathcompatibilityBase.ts
More file actions
376 lines (354 loc) · 15.7 KB
/
Copy pathcompatibilityBase.ts
File metadata and controls
376 lines (354 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { assert, fail } from "@fluidframework/core-utils/internal";
import type { OldestSupportedClientVersion } from "@fluidframework/runtime-definitions/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { featureVersion } from "@fluidframework/driver-definitions/internal";
import { compare, gt, gte, lte, parse } from "semver-ts";
import { pkgVersion } from "./packageVersion.js";
/**
* Oldest deployed Fluid Framework client version supported for cross-client compatibility.
*
* @internal
*/
export const lowestMinVersionForCollab =
"2.0.0" as const satisfies OldestSupportedClientVersion;
/**
* Default oldest supported client for APIs that still permit the setting to be omitted.
*
* @remarks
* This aliases {@link lowestMinVersionForCollab} in Client 3.0. Remove this fallback when
* customer-facing APIs require `oldestSupportedClient` in Client 3.10.
* See {@link https://github.com/microsoft/FluidFramework/issues/27180}.
*
* @internal
*/
export const defaultMinVersionForCollab = lowestMinVersionForCollab;
/**
* String in a valid semver format specifying the bottom of a minor version.
*
* @remarks
* Configuration maps use major/minor checkpoints. Exact patch and prerelease versions are values
* supplied to the selection logic, not configuration-map keys.
*
* @internal
*/
export type MinimumMinorSemanticVersion = `${bigint}.${bigint}.0`;
/**
* String in a valid semver format of a specific version at least specifying minor.
* Unlike {@link @fluidframework/runtime-definitions#OldestSupportedClientVersion}, this type does
* not encode the active compatibility floor or major-specific patch restrictions.
*
* @internal
*/
export type SemanticVersion =
| `${bigint}.${bigint}.${bigint}`
| `${bigint}.${bigint}.${bigint}-${string}`;
/**
* Converts a record into a configuration map that associates each key with an instance of its value type that is based on a {@link MinimumMinorSemanticVersion}.
* @remarks
* For a given input {@link @fluidframework/runtime-definitions#OldestSupportedClientVersion},
* the corresponding configuration values can be found by using the entry in the inner objects with the highest {@link MinimumMinorSemanticVersion}
* that does not exceed the given {@link @fluidframework/runtime-definitions#OldestSupportedClientVersion}.
*
* Use {@link getConfigsForMinVersionForCollab} to retrieve the configuration for a given a {@link @fluidframework/runtime-definitions#OldestSupportedClientVersion}.
*
* See the remarks on {@link MinimumMinorSemanticVersion} for some limitation on how ConfigMaps must handle versioning.
* @internal
*/
export type ConfigMap<T extends Record<string, unknown>> = {
readonly [K in keyof T]-?: ConfigMapEntry<T[K]>;
};
/**
* Entry in {@link ConfigMap} associating {@link MinimumMinorSemanticVersion} with configuration values that became supported in that version.
* @remarks
* All entries must at least provide an entry for {@link lowestMinVersionForCollab}.
* @internal
*/
export interface ConfigMapEntry<T> {
// This index signature (See https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures) requires all properties on this type to to have keys that are a MinimumMinorSemanticVersion and values of type T.
// Note that the "version" part of this syntax is really just documentation and has no impact on the type checking (other than some identifier being required to the syntax here to differentiate it from the computed property syntax).
[version: MinimumMinorSemanticVersion]: T;
// Require an entry for lowestMinVersionForCollab:
// this ensures that every supported deployed-client version has a specified value in the ConfigMap.
// Note that this is NOT an index signature.
// This is a regular property with a computed name (See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names).
[lowestMinVersionForCollab]: T;
}
/**
* Generic type for runtimeOptionsAffectingDocSchemaConfigValidationMap
*
* @internal
*/
export type ConfigValidationMap<T extends Record<string, unknown>> = {
readonly [K in keyof T]-?: (configValue: T[K]) => SemanticVersion | undefined;
};
/**
* Returns a default configuration given minVersionForCollab and configuration version map.
*
* @privateRemarks
* The extra `Record` type for the `configMap` is just used to allow the body of this function to be more type-safe due to limitations of generic types in TypeScript.
* It should have no impact on the user of this function.
* @internal
*/
export function getConfigsForMinVersionForCollab<T extends Record<SemanticVersion, unknown>>(
minVersionForCollab: OldestSupportedClientVersion,
configMap: ConfigMap<T> & Record<keyof T, unknown>,
): T {
validateMinimumVersionForCollab(minVersionForCollab);
const defaultConfigs: Partial<T> = {};
// Iterate over configMap to get default values for each option.
for (const [key, config] of Object.entries(configMap)) {
defaultConfigs[key] = getConfigForMinVersionForCollab(
minVersionForCollab,
config as ConfigMapEntry<unknown>,
);
}
// We have populated every key, so casting away the Partial is now safe:
return defaultConfigs as T;
}
/**
* Returns a default configuration given minVersionForCollab and {@link ConfigMapEntry}.
*
* @internal
*/
export function getConfigForMinVersionForCollab<T>(
minVersionForCollab: OldestSupportedClientVersion,
config: ConfigMapEntry<T>,
): T {
return getConfigForMinVersionForCollabIterable(
minVersionForCollab,
Object.entries(config) as [MinimumMinorSemanticVersion, T][],
);
}
/**
* Returns a default configuration given minVersionForCollab and the contents of a {@link ConfigMapEntry} in an Iterable.
* @remarks
* See also {@link getConfigForMinVersionForCollab} for consuming a ConfigMapEntry directly.
*
* `ConfigMapEntry` is a nice type safe format for developers to directly author this data,
* but it is messy and less type safe to work with it in this format programmatically.
* Thus this function exists to help meet the needs of programmatic use cases,
* like cases which transform a ConfigMapEntry before selecting a value from it.
* @internal
*/
export function getConfigForMinVersionForCollabIterable<T>(
minVersionForCollab: OldestSupportedClientVersion,
entries: Iterable<readonly [MinimumMinorSemanticVersion | OldestSupportedClientVersion, T]>, // [[typeof lowestMinVersionForCollab, T], ...[OldestSupportedClientVersion, T][]],
): T {
// Validate and strongly type the versions from the configMap.
const versions: [OldestSupportedClientVersion, T][] = Array.from(
entries,
([version, value]) => {
validateMinimumVersionForCollab(version);
return [version, value];
},
);
return (selectVersionRoundedDown(minVersionForCollab, versions) ??
fail(0xcb8 /* No config map entry for version */))[1];
}
/**
* Finds the entry for the highest version that is less than or equal to the provided minVersionForCollab.
* @remarks
* If none is found, returns undefined.
*
* When used with Fluid client versions, use the stricter {@link getConfigForMinVersionForCollabIterable} instead.
*
* @internal
*/
export function selectVersionRoundedDown<T>(
minVersionForCollab: string,
entries: Iterable<readonly [string, T]>,
compareVersions: (a: string, b: string) => number = compare,
): readonly [string, T] | undefined {
// Sort a copy of the iterable in descending order
const versions: (readonly [string, T])[] = [...entries];
versions.sort((a, b) => compareVersions(b[0], a[0]));
// For each config, we iterate over the keys and check if minVersionForCollab is greater than or equal to the version.
// If so, we set it as the default value for the option.
for (const pair of versions) {
const [version, _] = pair;
if (compareVersions(minVersionForCollab, version) >= 0) {
return pair;
}
}
return undefined;
}
/**
* Returns detailed information about the validity of a minVersionForCollab.
* @param minVersionForCollab - The minVersionForCollab to validate.
* @returns An object containing the validity information.
*
* @internal
*/
export function checkValidMinVersionForCollabVerbose(minVersionForCollab: SemanticVersion): {
isValidSemver: boolean;
isGteLowestMinVersion: boolean;
isLtePkgVersion: boolean;
isValidOldestSupportedClientVersion: boolean;
} {
const parsed = parse(minVersionForCollab);
const isValidSemver = parsed !== null && parsed.build.length === 0;
const isGteLowestMinVersion =
isValidSemver && gte(minVersionForCollab, lowestMinVersionForCollab);
const isLtePkgVersion = isValidSemver && lte(minVersionForCollab, cleanedPackageVersion);
const isValidOldestSupportedClientVersion =
isGteLowestMinVersion &&
isLtePkgVersion &&
parsed !== null &&
parsed.prerelease.length === 0 &&
(parsed.major < 3 || parsed.patch === 0);
return {
isValidSemver,
isGteLowestMinVersion,
isLtePkgVersion,
isValidOldestSupportedClientVersion,
};
}
/**
* Checks if the minVersionForCollab is valid.
* A valid minVersionForCollab is a OldestSupportedClientVersion that is at least `lowestMinVersionForCollab` and less than or equal to the current package version.
*
* @internal
*/
export function isValidMinVersionForCollab(
minVersionForCollab: SemanticVersion,
): minVersionForCollab is OldestSupportedClientVersion {
return checkValidMinVersionForCollabVerbose(minVersionForCollab)
.isValidOldestSupportedClientVersion;
}
/**
* `pkgVersion` version without pre-release and with zeroed patch.
* @remarks
* This is the version the current codebase will have when officially released.
* It allows CI and prerelease builds to test the release's features using that version as their
* `OldestSupportedClientVersion`.
*
* Code that needs to derive this value from a package version should use
* {@link @fluidframework/driver-definitions#featureVersion}.
*
* @privateRemarks
* This value is validated against {@link validateMinimumVersionForCollab} by a test.
*
* @internal
*/
export const cleanedPackageVersion = featureVersion(
pkgVersion,
) satisfies OldestSupportedClientVersion;
/**
* Narrows the type of the provided {@link SemanticVersion} to a {@link @fluidframework/runtime-definitions#OldestSupportedClientVersion}, throwing a UsageError if it is not valid.
* @remarks
* This is more strict than the type constraints imposed by `OldestSupportedClientVersion`.
* Currently there is no type which is used to separate semantically valid and typescript allowed OldestSupportedClientVersion values:
* thus users that care about strict validation may want to call this on un-validated `OldestSupportedClientVersion` values.
* @param semanticVersion - The version to check.
* @throws UsageError if the version is not a valid OldestSupportedClientVersion.
*
* @internal
*/
export function validateMinimumVersionForCollab(
semanticVersion: string,
): asserts semanticVersion is OldestSupportedClientVersion {
const minVersionForCollab = semanticVersion as OldestSupportedClientVersion;
const {
isValidSemver,
isGteLowestMinVersion,
isLtePkgVersion,
isValidOldestSupportedClientVersion,
} = checkValidMinVersionForCollabVerbose(minVersionForCollab);
if (!isValidOldestSupportedClientVersion) {
throw new UsageError(
`Version ${minVersionForCollab} is not a valid OldestSupportedClientVersion. ` +
`It must be in a valid semver format, at least ${lowestMinVersionForCollab}, ` +
`less than or equal to the current package version ${cleanedPackageVersion}, ` +
`have no prerelease component, and use patch version 0 for major version 3 and later. ` +
`Use "featureVersion" to normalize a package version to the correct format. ` +
`Details: { isValidSemver: ${isValidSemver}, isGteLowestMinVersion: ${isGteLowestMinVersion}, isLtePkgVersion: ${isLtePkgVersion}, isValidOldestSupportedClientVersion: ${isValidOldestSupportedClientVersion} }`,
);
}
}
/**
* Validates the given `overrides`.
*
* Checks that for keys which are in both the `validationMap` and the `overrides`,
* that the `validationMap` function for that key either returns undefined or a version less than or equal to `minVersionForCollab`.
* @privateRemarks
* This design seems odd, and might want to be revisited.
* Currently it only permits opting out of features, not into them (unless validationMap returns undefined),
* and the handling of undefined versions seems questionable.
* Also ignoring of extra keys in overrides might be bad since it seems like overrides is supposed to be validated.
* @internal
*/
export function validateConfigMapOverrides<T extends Record<string, unknown>>(
minVersionForCollab: SemanticVersion,
overrides: Partial<T>,
validationMap: ConfigValidationMap<T>,
): void {
// Iterate through each runtime option passed in by the user
// Type assertion is safe as entries come from runtimeOptions object
for (const [passedRuntimeOption, passedRuntimeOptionValue] of Object.entries(overrides) as [
keyof T & string,
T[keyof T & string],
][]) {
// Skip if passedRuntimeOption is not in validation map
if (!(passedRuntimeOption in validationMap)) {
continue;
}
const requiredVersion = validationMap[passedRuntimeOption](passedRuntimeOptionValue);
if (requiredVersion !== undefined && gt(requiredVersion, minVersionForCollab)) {
throw new UsageError(
`Runtime option ${passedRuntimeOption}:${JSON.stringify(passedRuntimeOptionValue)} requires ` +
`runtime version ${requiredVersion}. Please update minVersionForCollab ` +
`(currently ${minVersionForCollab}) to ${requiredVersion} or later to proceed.`,
);
}
}
}
/**
* Helper function to map ContainerRuntimeOptionsInternal config values to
* minVersionForCollab in, e.g., {@link @fluidframework/container-runtime#runtimeOptionsAffectingDocSchemaConfigValidationMap}.
*
* @internal
*/
export function configValueToMinVersionForCollab<
T extends string | number | boolean | undefined | object,
Arr extends readonly [T, SemanticVersion][],
>(configToMinVer: Arr): (configValue: T) => SemanticVersion | undefined {
const configValueToRequiredVersionMap = new Map(configToMinVer);
return (configValue: T) => {
// If the configValue is not an object then we can get the version required directly from the map.
if (typeof configValue !== "object") {
return configValueToRequiredVersionMap.get(configValue);
}
// When the input `configValue` is an object, this logic determines the minimum runtime version it requires.
// It iterates through each entry in `configValueToRequiredVersionMap`. If `possibleConfigValue` shares at
// least one key-value pair with the input `configValue`, its associated `versionRequired` is collected into
// `matchingVersions`. After checking all entries, the highest among the collected versions is returned.
// This represents the overall minimum version required to support the features implied by the input `configValue`.
const matchingVersions: SemanticVersion[] = [];
for (const [
possibleConfigValue,
versionRequired,
] of configValueToRequiredVersionMap.entries()) {
assert(
typeof possibleConfigValue == "object",
0xbb9 /* possibleConfigValue should be an object */,
);
// Check if `possibleConfigValue` and the input `configValue` share at least one
// common key-value pair. If they do, the `versionRequired` for this `possibleConfigValue`
// is added to `matchingVersions`.
if (Object.entries(possibleConfigValue).some(([k, v]) => configValue[k] === v)) {
matchingVersions.push(versionRequired);
}
}
if (matchingVersions.length > 0) {
// Return the latest minVersionForCollab among all matches.
return matchingVersions.sort((a, b) => compare(b, a))[0];
}
// If no matches then we return undefined. This means that the config value passed in
// does not require a specific minVersionForCollab to be valid.
return undefined;
};
}