@fluidframework/runtime-utils 2.72.0 → 2.73.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.
@@ -3,16 +3,16 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { assert } from "@fluidframework/core-utils/internal";
6
+ import { assert, fail } from "@fluidframework/core-utils/internal";
7
7
  import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal";
8
8
  import { UsageError } from "@fluidframework/telemetry-utils/internal";
9
- import { compare, gt, gte, lte, valid } from "semver-ts";
9
+ import { compare, gt, gte, lte, valid, parse } from "semver-ts";
10
10
 
11
11
  import { pkgVersion } from "./packageVersion.js";
12
12
 
13
13
  /**
14
- * Our policy is to support N/N-1 compatibility by default, where N is the most
15
- * recent public major release of the runtime.
14
+ * Our policy is to support major versions N and N-1, where N is most
15
+ * recent public major release of the Fluid Framework Client.
16
16
  * Therefore, if the customer does not provide a minVersionForCollab, we will
17
17
  * default to use N-1.
18
18
  *
@@ -68,54 +68,98 @@ export type SemanticVersion =
68
68
  | `${bigint}.${bigint}.${bigint}-${string}`;
69
69
 
70
70
  /**
71
- * Generic type for runtimeOptionsAffectingDocSchemaConfigMap
71
+ * 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}.
72
+ * @remarks
73
+ * For a given input {@link @fluidframework/runtime-definitions#MinimumVersionForCollab},
74
+ * the corresponding configuration values can be found by using the entry in the inner objects with the highest {@link MinimumMinorSemanticVersion}
75
+ * that does not exceed the given {@link @fluidframework/runtime-definitions#MinimumVersionForCollab}.
72
76
  *
77
+ * Use {@link getConfigsForMinVersionForCollab} to retrieve the configuration for a given a {@link @fluidframework/runtime-definitions#MinimumVersionForCollab}.
78
+ *
79
+ * See the remarks on {@link MinimumMinorSemanticVersion} for some limitation on how ConfigMaps must handle versioning.
73
80
  * @internal
74
81
  */
75
82
  export type ConfigMap<T extends Record<string, unknown>> = {
76
- [K in keyof T]-?: Record<MinimumMinorSemanticVersion, T[K]>;
83
+ readonly [K in keyof T]-?: ConfigMapEntry<T[K]>;
77
84
  };
78
85
 
86
+ /**
87
+ * Entry in {@link ConfigMap} associating {@link MinimumMinorSemanticVersion} with configuration values that became supported in that version.
88
+ * @remarks
89
+ * All entries must at least provide an entry for {@link lowestMinVersionForCollab}.
90
+ * @internal
91
+ */
92
+ export interface ConfigMapEntry<T> {
93
+ // 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.
94
+ // 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).
95
+ [version: MinimumMinorSemanticVersion]: T;
96
+ // Require an entry for the defaultMinVersionForCollab:
97
+ // this ensures that all versions of lowestMinVersionForCollab or later have a specified value in the ConfigMap.
98
+ // Note that this is NOT an index signature.
99
+ // 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).
100
+ [lowestMinVersionForCollab]: T;
101
+ }
102
+
79
103
  /**
80
104
  * Generic type for runtimeOptionsAffectingDocSchemaConfigValidationMap
81
105
  *
82
106
  * @internal
83
107
  */
84
108
  export type ConfigValidationMap<T extends Record<string, unknown>> = {
85
- [K in keyof T]-?: (configValue: T[K]) => SemanticVersion | undefined;
109
+ readonly [K in keyof T]-?: (configValue: T[K]) => SemanticVersion | undefined;
86
110
  };
87
111
 
88
112
  /**
89
113
  * Returns a default configuration given minVersionForCollab and configuration version map.
90
114
  *
115
+ * @privateRemarks
116
+ * 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.
117
+ * It should have no impact on the user of this function.
91
118
  * @internal
92
119
  */
93
120
  export function getConfigsForMinVersionForCollab<T extends Record<SemanticVersion, unknown>>(
94
- minVersionForCollab: SemanticVersion,
95
- configMap: ConfigMap<T>,
96
- ): Partial<T> {
121
+ minVersionForCollab: MinimumVersionForCollab,
122
+ configMap: ConfigMap<T> & Record<keyof T, unknown>,
123
+ ): T {
124
+ validateMinimumVersionForCollab(minVersionForCollab);
97
125
  const defaultConfigs: Partial<T> = {};
98
126
  // Iterate over configMap to get default values for each option.
99
- for (const key of Object.keys(configMap)) {
100
- // Type assertion is safe as key comes from Object.keys(configMap)
101
- const config = configMap[key as keyof T];
102
- // Sort the versions in ascending order so we can short circuit the loop.
103
- const versions = Object.keys(config).sort(compare);
104
- // For each config, we iterate over the keys and check if minVersionForCollab is greater than or equal to the version.
105
- // If so, we set it as the default value for the option. At the end of the loop we should have the most recent default
106
- // value that is compatible with the version specified as the minVersionForCollab.
107
- for (const version of versions) {
108
- if (gte(minVersionForCollab, version)) {
109
- // Type assertion is safe as version is a key from the config object
110
- defaultConfigs[key] = config[version as MinimumMinorSemanticVersion];
111
- } else {
112
- // If the minVersionForCollab is less than the version, we break out of the loop since we don't need to check
113
- // any later versions.
114
- break;
115
- }
127
+ for (const [key, config] of Object.entries(configMap)) {
128
+ defaultConfigs[key] = getConfigForMinVersionForCollab(
129
+ minVersionForCollab,
130
+ config as ConfigMapEntry<unknown>,
131
+ );
132
+ }
133
+ // We have populated every key, so casting away the Partial is now safe:
134
+ return defaultConfigs as T;
135
+ }
136
+
137
+ /**
138
+ * Returns a default configuration given minVersionForCollab and {@link ConfigMapEntry}.
139
+ *
140
+ * @internal
141
+ */
142
+ export function getConfigForMinVersionForCollab<T>(
143
+ minVersionForCollab: MinimumVersionForCollab,
144
+ config: ConfigMapEntry<T>,
145
+ ): T {
146
+ const entries: [string, unknown][] = Object.entries(config); // Assigning this to a typed variable to convert the "any" into unknown.
147
+ // Validate and strongly type the versions from the configMap.
148
+ const versions: [MinimumVersionForCollab, unknown][] = entries.map(([version, value]) => {
149
+ validateMinimumVersionForCollab(version);
150
+ return [version, value];
151
+ });
152
+ // Sort the versions in descending order to find the largest compatible entry.
153
+ // TODO: Enforcing a sorted order might be a good idea. For now tolerates any order.
154
+ versions.sort((a, b) => compare(b[0], a[0]));
155
+ // For each config, we iterate over the keys and check if minVersionForCollab is greater than or equal to the version.
156
+ // If so, we set it as the default value for the option.
157
+ for (const [version, value] of versions) {
158
+ if (gte(minVersionForCollab, version)) {
159
+ return value as T;
116
160
  }
117
161
  }
118
- return defaultConfigs;
162
+ fail("No config map entry for version");
119
163
  }
120
164
 
121
165
  /**
@@ -125,9 +169,7 @@ export function getConfigsForMinVersionForCollab<T extends Record<SemanticVersio
125
169
  *
126
170
  * @internal
127
171
  */
128
- export function checkValidMinVersionForCollabVerbose(
129
- minVersionForCollab: MinimumVersionForCollab,
130
- ): {
172
+ export function checkValidMinVersionForCollabVerbose(minVersionForCollab: SemanticVersion): {
131
173
  isValidSemver: boolean;
132
174
  isGteLowestMinVersion: boolean;
133
175
  isLtePkgVersion: boolean;
@@ -139,7 +181,7 @@ export function checkValidMinVersionForCollabVerbose(
139
181
  // We have to check if the value is a valid semver before calling gte/lte, otherwise they will throw when parsing the version.
140
182
  isGteLowestMinVersion:
141
183
  isValidSemver && gte(minVersionForCollab, lowestMinVersionForCollab),
142
- isLtePkgVersion: isValidSemver && lte(minVersionForCollab, pkgVersion),
184
+ isLtePkgVersion: isValidSemver && lte(minVersionForCollab, cleanedPackageVersion),
143
185
  };
144
186
  }
145
187
 
@@ -150,24 +192,57 @@ export function checkValidMinVersionForCollabVerbose(
150
192
  * @internal
151
193
  */
152
194
  export function isValidMinVersionForCollab(
153
- minVersionForCollab: MinimumVersionForCollab,
154
- ): boolean {
195
+ minVersionForCollab: SemanticVersion,
196
+ ): minVersionForCollab is MinimumVersionForCollab {
155
197
  const { isValidSemver, isGteLowestMinVersion, isLtePkgVersion } =
156
198
  checkValidMinVersionForCollabVerbose(minVersionForCollab);
157
199
  return isValidSemver && isGteLowestMinVersion && isLtePkgVersion;
158
200
  }
159
201
 
202
+ const parsedPackageVersion = parse(pkgVersion) ?? fail("Invalid package version");
203
+
160
204
  /**
161
- * Converts a SemanticVersion to a MinimumVersionForCollab.
162
- * @param semanticVersion - The version to convert.
163
- * @returns The version as a MinimumVersionForCollab.
205
+ * `pkgVersion` version without pre-release.
206
+ * @remarks
207
+ * This is the version that the code in the current version of the codebase will have when officially released.
208
+ * Generally, compatibility of prerelease builds is not guaranteed (especially for how they interact with future releases).
209
+ * So while technically a prerelease build is less (older) than the released version which follows it and thus supports less features,
210
+ * it makes sense for them to claim to support the same features as the following release so they can be used to test how the release would actually behave.
211
+ *
212
+ * To accomplish this, the version the next release will have is provided here as `cleanedPackageVersion` while `pkgVersion` may be a prerelease in some cases,
213
+ * like when running tests on CI, or in an actual prerelease published package.
214
+ * This is then used in {@link validateMinimumVersionForCollab} to allow the version shown on main to be usable as a `minVersionForCollab`, even in CI and prerelease packages.
215
+ *
216
+ * This is of particular note in two cases:
217
+ * 1. When landing a new feature, and setting the minVersionForCollab which enables it to be the version that the next release will have.
218
+ * Having that version be valid on main, pass tests locally, then fail on CI and when using published prerelease packages would be confusing, and probably undesired.
219
+ * 2. Setting the minVersionForCollab to the current version for scenarios that do no involve collab with other package versions seems like it should be valid.
220
+ * This is useful for testing new features, and also non collaborative scenarios where the latest features are desired.
221
+ *
222
+ * To accommodate some uses of the second case, it might be useful to package export this in the future.
223
+ *
224
+ * @privateRemarks
225
+ * Since this is used by validateMinimumVersionForCollab, the type case to MinimumVersionForCollab can not use it directly.
226
+ * Thus this is just `as` cast here, and a test confirms it is valid according to validateMinimumVersionForCollab.
227
+ *
228
+ */
229
+ export const cleanedPackageVersion =
230
+ `${parsedPackageVersion.major}.${parsedPackageVersion.minor}.${parsedPackageVersion.patch}` as MinimumVersionForCollab;
231
+
232
+ /**
233
+ * Narrows the type of the provided {@link SemanticVersion} to a {@link @fluidframework/runtime-definitions#MinimumVersionForCollab}, throwing a UsageError if it is not valid.
234
+ * @remarks
235
+ * This is more strict than the type constraints imposed by `MinimumVersionForCollab`.
236
+ * Currently there is no type which is used to separate semantically valid and typescript allowed MinimumVersionForCollab values:
237
+ * thus users that care about strict validation may want to call this on un-validated `MinimumVersionForCollab` values.
238
+ * @param semanticVersion - The version to check.
164
239
  * @throws UsageError if the version is not a valid MinimumVersionForCollab.
165
240
  *
166
241
  * @internal
167
242
  */
168
- export function semanticVersionToMinimumVersionForCollab(
169
- semanticVersion: SemanticVersion,
170
- ): MinimumVersionForCollab {
243
+ export function validateMinimumVersionForCollab(
244
+ semanticVersion: string,
245
+ ): asserts semanticVersion is MinimumVersionForCollab {
171
246
  const minVersionForCollab = semanticVersion as MinimumVersionForCollab;
172
247
  const { isValidSemver, isGteLowestMinVersion, isLtePkgVersion } =
173
248
  checkValidMinVersionForCollabVerbose(minVersionForCollab);
@@ -175,34 +250,43 @@ export function semanticVersionToMinimumVersionForCollab(
175
250
  throw new UsageError(
176
251
  `Version ${minVersionForCollab} is not a valid MinimumVersionForCollab. ` +
177
252
  `It must be in a valid semver format, at least ${lowestMinVersionForCollab}, ` +
178
- `and less than or equal to the current package version ${pkgVersion}. ` +
253
+ `and less than or equal to the current package version ${cleanedPackageVersion}. ` +
179
254
  `Details: { isValidSemver: ${isValidSemver}, isGteLowestMinVersion: ${isGteLowestMinVersion}, isLtePkgVersion: ${isLtePkgVersion} }`,
180
255
  );
181
256
  }
182
-
183
- return minVersionForCollab;
184
257
  }
185
258
 
186
259
  /**
187
- * Generic function to validate runtime options against the minVersionForCollab.
260
+ * Validates the given `overrides`.
188
261
  *
262
+ * No-op when minVersionForCollab is set to defaultMinVersionForCollab.
263
+ *
264
+ * Otherwise this checks that for keys which are in both the `validationMap` and the `overrides`,
265
+ * that the `validationMap` function for that key either returns undefined or a version less than or equal to `minVersionForCollab`.
266
+ * @privateRemarks
267
+ * This design seems odd, and might want to be revisited.
268
+ * Currently it only permits opting out of features, not into them (unless validationMap returns undefined),
269
+ * and the handling of defaultMinVersionForCollab and undefined versions seems questionable.
270
+ * Also ignoring of extra keys in overrides might be bad since it seems like overrides is supposed to be validated.
189
271
  * @internal
190
272
  */
191
- export function getValidationForRuntimeOptions<T extends Record<string, unknown>>(
273
+ export function validateConfigMapOverrides<T extends Record<string, unknown>>(
192
274
  minVersionForCollab: SemanticVersion,
193
- runtimeOptions: Partial<T>,
275
+ overrides: Partial<T>,
194
276
  validationMap: ConfigValidationMap<T>,
195
277
  ): void {
196
278
  if (minVersionForCollab === defaultMinVersionForCollab) {
197
279
  // If the minVersionForCollab is set to the default value, then we will not validate the runtime options
198
280
  // This is to avoid disruption to users who have not yet set the minVersionForCollab value explicitly.
281
+ // TODO: This also skips validation for users which explicitly request defaultMinVersionForCollab which seems like a bug.
199
282
  return;
200
283
  }
201
284
  // Iterate through each runtime option passed in by the user
202
285
  // Type assertion is safe as entries come from runtimeOptions object
203
- for (const [passedRuntimeOption, passedRuntimeOptionValue] of Object.entries(
204
- runtimeOptions,
205
- ) as [keyof T & string, T[keyof T & string]][]) {
286
+ for (const [passedRuntimeOption, passedRuntimeOptionValue] of Object.entries(overrides) as [
287
+ keyof T & string,
288
+ T[keyof T & string],
289
+ ][]) {
206
290
  // Skip if passedRuntimeOption is not in validation map
207
291
  if (!(passedRuntimeOption in validationMap)) {
208
292
  continue;
package/src/index.ts CHANGED
@@ -64,13 +64,16 @@ export {
64
64
  export {
65
65
  configValueToMinVersionForCollab,
66
66
  defaultMinVersionForCollab,
67
- getValidationForRuntimeOptions,
67
+ validateConfigMapOverrides,
68
+ getConfigForMinVersionForCollab,
68
69
  getConfigsForMinVersionForCollab,
69
70
  isValidMinVersionForCollab,
70
- semanticVersionToMinimumVersionForCollab,
71
+ validateMinimumVersionForCollab,
72
+ lowestMinVersionForCollab,
71
73
  } from "./compatibilityBase.js";
72
74
  export type {
73
75
  ConfigMap,
76
+ ConfigMapEntry,
74
77
  ConfigValidationMap,
75
78
  MinimumMinorSemanticVersion,
76
79
  SemanticVersion,
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/runtime-utils";
9
- export const pkgVersion = "2.72.0";
9
+ export const pkgVersion = "2.73.0";