@fluidframework/container-runtime 2.41.0 → 2.43.0-343119

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 (136) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/channelCollection.d.ts +1 -1
  4. package/dist/channelCollection.d.ts.map +1 -1
  5. package/dist/channelCollection.js +4 -4
  6. package/dist/channelCollection.js.map +1 -1
  7. package/dist/compatUtils.d.ts +24 -1
  8. package/dist/compatUtils.d.ts.map +1 -1
  9. package/dist/compatUtils.js +109 -7
  10. package/dist/compatUtils.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +36 -15
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +186 -71
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStore.d.ts.map +1 -1
  16. package/dist/dataStore.js +5 -0
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +2 -0
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcDefinitions.d.ts +1 -1
  22. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  23. package/dist/gc/gcDefinitions.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/messageTypes.d.ts +5 -4
  28. package/dist/messageTypes.d.ts.map +1 -1
  29. package/dist/messageTypes.js.map +1 -1
  30. package/dist/metadata.d.ts +1 -1
  31. package/dist/metadata.d.ts.map +1 -1
  32. package/dist/metadata.js.map +1 -1
  33. package/dist/opLifecycle/definitions.d.ts +6 -5
  34. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  35. package/dist/opLifecycle/definitions.js.map +1 -1
  36. package/dist/opLifecycle/index.d.ts +1 -1
  37. package/dist/opLifecycle/index.d.ts.map +1 -1
  38. package/dist/opLifecycle/index.js.map +1 -1
  39. package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
  40. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  41. package/dist/opLifecycle/opGroupingManager.js +6 -4
  42. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  43. package/dist/opLifecycle/opSerialization.d.ts +2 -1
  44. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  45. package/dist/opLifecycle/opSerialization.js.map +1 -1
  46. package/dist/packageVersion.d.ts +1 -1
  47. package/dist/packageVersion.d.ts.map +1 -1
  48. package/dist/packageVersion.js +1 -1
  49. package/dist/packageVersion.js.map +1 -1
  50. package/dist/pendingStateManager.d.ts +18 -5
  51. package/dist/pendingStateManager.d.ts.map +1 -1
  52. package/dist/pendingStateManager.js +20 -13
  53. package/dist/pendingStateManager.js.map +1 -1
  54. package/dist/summary/documentSchema.d.ts +79 -16
  55. package/dist/summary/documentSchema.d.ts.map +1 -1
  56. package/dist/summary/documentSchema.js +119 -53
  57. package/dist/summary/documentSchema.js.map +1 -1
  58. package/dist/summary/index.d.ts +1 -1
  59. package/dist/summary/index.d.ts.map +1 -1
  60. package/dist/summary/index.js.map +1 -1
  61. package/lib/channelCollection.d.ts +1 -1
  62. package/lib/channelCollection.d.ts.map +1 -1
  63. package/lib/channelCollection.js +4 -4
  64. package/lib/channelCollection.js.map +1 -1
  65. package/lib/compatUtils.d.ts +24 -1
  66. package/lib/compatUtils.d.ts.map +1 -1
  67. package/lib/compatUtils.js +102 -3
  68. package/lib/compatUtils.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +36 -15
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +188 -73
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/dataStore.d.ts.map +1 -1
  74. package/lib/dataStore.js +5 -0
  75. package/lib/dataStore.js.map +1 -1
  76. package/lib/gc/garbageCollection.d.ts.map +1 -1
  77. package/lib/gc/garbageCollection.js +2 -0
  78. package/lib/gc/garbageCollection.js.map +1 -1
  79. package/lib/gc/gcDefinitions.d.ts +1 -1
  80. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  81. package/lib/gc/gcDefinitions.js.map +1 -1
  82. package/lib/index.d.ts +2 -2
  83. package/lib/index.d.ts.map +1 -1
  84. package/lib/index.js.map +1 -1
  85. package/lib/messageTypes.d.ts +5 -4
  86. package/lib/messageTypes.d.ts.map +1 -1
  87. package/lib/messageTypes.js.map +1 -1
  88. package/lib/metadata.d.ts +1 -1
  89. package/lib/metadata.d.ts.map +1 -1
  90. package/lib/metadata.js.map +1 -1
  91. package/lib/opLifecycle/definitions.d.ts +6 -5
  92. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  93. package/lib/opLifecycle/definitions.js.map +1 -1
  94. package/lib/opLifecycle/index.d.ts +1 -1
  95. package/lib/opLifecycle/index.d.ts.map +1 -1
  96. package/lib/opLifecycle/index.js.map +1 -1
  97. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  98. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  99. package/lib/opLifecycle/opGroupingManager.js +6 -4
  100. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  101. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  102. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  103. package/lib/opLifecycle/opSerialization.js.map +1 -1
  104. package/lib/packageVersion.d.ts +1 -1
  105. package/lib/packageVersion.d.ts.map +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/pendingStateManager.d.ts +18 -5
  109. package/lib/pendingStateManager.d.ts.map +1 -1
  110. package/lib/pendingStateManager.js +20 -13
  111. package/lib/pendingStateManager.js.map +1 -1
  112. package/lib/summary/documentSchema.d.ts +79 -16
  113. package/lib/summary/documentSchema.d.ts.map +1 -1
  114. package/lib/summary/documentSchema.js +119 -53
  115. package/lib/summary/documentSchema.js.map +1 -1
  116. package/lib/summary/index.d.ts +1 -1
  117. package/lib/summary/index.d.ts.map +1 -1
  118. package/lib/summary/index.js.map +1 -1
  119. package/package.json +18 -18
  120. package/src/channelCollection.ts +4 -4
  121. package/src/compatUtils.ts +147 -10
  122. package/src/containerRuntime.ts +242 -85
  123. package/src/dataStore.ts +7 -0
  124. package/src/gc/garbageCollection.ts +2 -0
  125. package/src/gc/gcDefinitions.ts +1 -1
  126. package/src/index.ts +4 -2
  127. package/src/messageTypes.ts +12 -5
  128. package/src/metadata.ts +1 -1
  129. package/src/opLifecycle/definitions.ts +7 -3
  130. package/src/opLifecycle/index.ts +1 -0
  131. package/src/opLifecycle/opGroupingManager.ts +17 -4
  132. package/src/opLifecycle/opSerialization.ts +6 -1
  133. package/src/packageVersion.ts +1 -1
  134. package/src/pendingStateManager.ts +49 -22
  135. package/src/summary/documentSchema.ts +228 -83
  136. package/src/summary/index.ts +3 -1
@@ -4,8 +4,11 @@
4
4
  */
5
5
 
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
+ import type { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
7
8
  import { DataProcessingError } from "@fluidframework/telemetry-utils/internal";
9
+ import { gt, lt, parse } from "semver-ts";
8
10
 
11
+ import type { SemanticVersion } from "../compatUtils.js";
9
12
  import { pkgVersion } from "../packageVersion.js";
10
13
 
11
14
  /**
@@ -59,15 +62,56 @@ export type IdCompressorMode = "on" | "delayed" | undefined;
59
62
  * @internal
60
63
  */
61
64
  export interface IDocumentSchema {
62
- // version that describes how data is stored in this structure.
63
- // If runtime sees a version it does not understand, it should immediately fail and not
64
- // attempt to interpret any further data.
65
+ // Note: Incoming schemas from other clients may have additional root-level properties (i.e. IDocumentSchema.app)
66
+ // that this client does not understand. The runtime will ignore these properties, unless they are within the
67
+ // "runtime" sub-tree, in which case it will fail if it is unable to understand any runtime properties.
68
+
69
+ /**
70
+ * Describes how data needed to understand the schema is stored in this structure.
71
+ * If runtime sees a version it does not understand, it should immediately fail and not
72
+ * attempt to interpret any further data.
73
+ */
65
74
  version: number;
66
75
 
67
- // Sequence number when this schema became active.
76
+ /**
77
+ * Sequence number when this schema became active.
78
+ */
68
79
  refSeq: number;
69
80
 
81
+ /**
82
+ * Runtime configurations that affect the document schema. Other clients must understand these
83
+ * properties to be able to open the document.
84
+ */
70
85
  runtime: Record<string, DocumentSchemaValueType>;
86
+
87
+ /**
88
+ * Info about this document that can be updated via Document Schema change op, but isn't required
89
+ * to be understood by all clients (unlike the rest of IDocumentSchema properties). Because of this,
90
+ * some older documents may not have this property, so it's an optional property.
91
+ */
92
+ info?: IDocumentSchemaInfo;
93
+ }
94
+
95
+ /**
96
+ * Informational properties of the document that are not subject to strict schema enforcement.
97
+ *
98
+ * @internal
99
+ */
100
+ export interface IDocumentSchemaInfo {
101
+ /**
102
+ * The minimum version of the FF runtime that should be used to load this document.
103
+ * Will likely be advanced over time as applications pick up later FF versions.
104
+ *
105
+ * We use this to issue telemetry warning events if a client tries to open a document
106
+ * with a runtime version lower than this.
107
+ *
108
+ * See {@link @fluidframework/container-runtime#LoadContainerRuntimeParams} for additional details on `minVersionForCollab`.
109
+ *
110
+ * @remarks
111
+ * We use `SemanticVersion` instead of `MinimumVersionForCollab` since we may open future documents that with a
112
+ * minVersionForCollab version that `MinimumVersionForCollab` does not support.
113
+ */
114
+ minVersionForCollab: SemanticVersion;
71
115
  }
72
116
 
73
117
  /**
@@ -75,10 +119,17 @@ export interface IDocumentSchema {
75
119
  * The meaning of refSeq field is different in such messages (compared to other usages of IDocumentSchemaCurrent)
76
120
  * ContainerMessageType.DocumentSchemaChange messages use CAS (Compare-and-swap) semantics, and convey
77
121
  * regSeq of last known schema change (known to a client proposing schema change).
78
- * @see ContainerRuntimeDocumentSchemaMessage
122
+ * @see InboundContainerRuntimeDocumentSchemaMessage
123
+ * @internal
124
+ */
125
+ export type IDocumentSchemaChangeMessageIncoming = IDocumentSchema;
126
+
127
+ /**
128
+ * Similar to {@link IDocumentSchemaChangeMessageIncoming}, but used for outgoing schema messages.
129
+ * @see OutboundContainerRuntimeDocumentSchemaMessage
79
130
  * @internal
80
131
  */
81
- export type IDocumentSchemaChangeMessage = IDocumentSchema;
132
+ export type IDocumentSchemaChangeMessageOutgoing = IDocumentSchemaCurrent;
82
133
 
83
134
  /**
84
135
  * Settings that this session would like to have, based on options and feature gates.
@@ -113,42 +164,61 @@ export interface IDocumentSchemaFeatures {
113
164
 
114
165
  /**
115
166
  * Current version known properties that define document schema
116
- * This must be bumped whenever the format of document schema or protocol for changing the current document schema changes.
117
- * Ex: adding a new configuration property (under IDocumentSchema.runtime) does not require changing this version.
118
- * Ex: Changing the 'document schema acceptance' mechanism from convert-and-swap to one requiring consensus does require changing this version.
167
+ * This must be bumped whenever the format of document schema or protocol for changing the current document schema changes
168
+ * in a way that all old/new clients are required to understand.
169
+ * Ex: Adding a new configuration property (under IDocumentSchema.runtime) does not require changing this version since there is logic
170
+ * in old clients for handling new/unknown properties.
171
+ * Ex: Adding a new property to IDocumentSchema.info does not require changing this version, since info properties are not required to be
172
+ * understood by all clients.
173
+ * Ex: Changing the 'document schema acceptance' mechanism from convert-and-swap to one requiring consensus does require changing this version
174
+ * since all clients need to understand the new protocol.
119
175
  * @internal
120
176
  */
121
177
  export const currentDocumentVersionSchema = 1;
122
178
 
123
179
  /**
124
180
  * Current document schema.
181
+ * This interface represents the schema that we currently understand and know the
182
+ * structure of (which properties will be present).
183
+ *
125
184
  * @internal
126
185
  */
127
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
128
- export type IDocumentSchemaCurrent = {
129
- version: 1;
130
- refSeq: number;
131
-
186
+ export interface IDocumentSchemaCurrent extends Required<IDocumentSchema> {
187
+ // This is the version of the schema that we currently understand.
188
+ version: typeof currentDocumentVersionSchema;
189
+ // This narrows the runtime property to only include the properties in IDocumentSchemaFeatures (all as optional)
132
190
  runtime: {
133
191
  [P in keyof IDocumentSchemaFeatures]?: IDocumentSchemaFeatures[P] extends boolean
134
192
  ? true
135
193
  : IDocumentSchemaFeatures[P];
136
194
  };
137
- };
195
+ }
196
+
197
+ /**
198
+ * Document schema that is incoming from another client but validated to be "current".
199
+ *
200
+ * This interface represents when we have validated that an incoming IDocumentSchema object
201
+ * is compatible with the current runtime (by calling `checkRuntimeCompatibility()`).
202
+ * However, the `info` property is optional because some older documents may not have this property, but
203
+ * `info` is not required to be understood by all clients to be compatible.
204
+ */
205
+ interface IDocumentSchemaCurrentIncoming extends Omit<IDocumentSchemaCurrent, "info"> {
206
+ info?: IDocumentSchemaInfo;
207
+ }
138
208
 
139
209
  interface IProperty<T = unknown> {
140
- and: (currentDocSchema: T, desiredDocSchema: T) => T;
141
- or: (currentDocSchema: T, desiredDocSchema: T) => T;
210
+ and: (persistedSchema: T, providedSchema: T) => T;
211
+ or: (persistedSchema: T, providedSchema: T) => T;
142
212
  validate(t: unknown): boolean;
143
213
  }
144
214
 
145
215
  class TrueOrUndefined implements IProperty<true | undefined> {
146
- public and(currentDocSchema?: true, desiredDocSchema?: true): true | undefined {
147
- return currentDocSchema === true && desiredDocSchema === true ? true : undefined;
216
+ public and(persistedSchema?: true, providedSchema?: true): true | undefined {
217
+ return persistedSchema === true && providedSchema === true ? true : undefined;
148
218
  }
149
219
 
150
- public or(currentDocSchema?: true, desiredDocSchema?: true): true | undefined {
151
- return currentDocSchema === true || desiredDocSchema === true ? true : undefined;
220
+ public or(persistedSchema?: true, providedSchema?: true): true | undefined {
221
+ return persistedSchema === true || providedSchema === true ? true : undefined;
152
222
  }
153
223
 
154
224
  public validate(t: unknown): t is true | undefined {
@@ -157,32 +227,32 @@ class TrueOrUndefined implements IProperty<true | undefined> {
157
227
  }
158
228
 
159
229
  class TrueOrUndefinedMax extends TrueOrUndefined {
160
- public and(currentDocSchema?: true, desiredDocSchema?: true): true | undefined {
161
- return this.or(currentDocSchema, desiredDocSchema);
230
+ public and(persistedSchema?: true, providedSchema?: true): true | undefined {
231
+ return this.or(persistedSchema, providedSchema);
162
232
  }
163
233
  }
164
234
 
165
235
  class MultiChoice implements IProperty<string | undefined> {
166
236
  constructor(private readonly choices: string[]) {}
167
237
 
168
- public and(currentDocSchema?: string, desiredDocSchema?: string): string | undefined {
169
- if (currentDocSchema === undefined || desiredDocSchema === undefined) {
238
+ public and(persistedSchema?: string, providedSchema?: string): string | undefined {
239
+ if (persistedSchema === undefined || providedSchema === undefined) {
170
240
  return undefined;
171
241
  }
172
242
  return this.choices[
173
- Math.min(this.choices.indexOf(currentDocSchema), this.choices.indexOf(desiredDocSchema))
243
+ Math.min(this.choices.indexOf(persistedSchema), this.choices.indexOf(providedSchema))
174
244
  ];
175
245
  }
176
246
 
177
- public or(currentDocSchema?: string, desiredDocSchema?: string): string | undefined {
178
- if (currentDocSchema === undefined) {
179
- return desiredDocSchema;
247
+ public or(persistedSchema?: string, providedSchema?: string): string | undefined {
248
+ if (persistedSchema === undefined) {
249
+ return providedSchema;
180
250
  }
181
- if (desiredDocSchema === undefined) {
182
- return currentDocSchema;
251
+ if (providedSchema === undefined) {
252
+ return persistedSchema;
183
253
  }
184
254
  return this.choices[
185
- Math.max(this.choices.indexOf(currentDocSchema), this.choices.indexOf(desiredDocSchema))
255
+ Math.max(this.choices.indexOf(persistedSchema), this.choices.indexOf(providedSchema))
186
256
  ];
187
257
  }
188
258
 
@@ -193,26 +263,26 @@ class MultiChoice implements IProperty<string | undefined> {
193
263
 
194
264
  class IdCompressorProperty extends MultiChoice {
195
265
  // document schema always wins!
196
- public and(currentDocSchema?: string, desiredDocSchema?: string): string | undefined {
197
- return currentDocSchema;
266
+ public and(persistedSchema?: string, providedSchema?: string): string | undefined {
267
+ return persistedSchema;
198
268
  }
199
269
  }
200
270
 
201
271
  class CheckVersions implements IProperty<string[] | undefined> {
202
272
  public or(
203
- currentDocSchema: string[] = [],
204
- desiredDocSchema: string[] = [],
273
+ persistedSchema: string[] = [],
274
+ providedSchema: string[] = [],
205
275
  ): string[] | undefined {
206
- const set = new Set<string>([...currentDocSchema, ...desiredDocSchema]);
276
+ const set = new Set<string>([...persistedSchema, ...providedSchema]);
207
277
  return arrayToProp([...set.values()]);
208
278
  }
209
279
 
210
280
  // Once version is there, it stays there forever.
211
281
  public and(
212
- currentDocSchema: string[] = [],
213
- desiredDocSchema: string[] = [],
282
+ persistedSchema: string[] = [],
283
+ providedSchema: string[] = [],
214
284
  ): string[] | undefined {
215
- return this.or(currentDocSchema, desiredDocSchema);
285
+ return this.or(persistedSchema, providedSchema);
216
286
  }
217
287
 
218
288
  public validate(t: unknown): boolean {
@@ -240,7 +310,7 @@ const documentSchemaSupportedConfigs = {
240
310
  function checkRuntimeCompatibility(
241
311
  documentSchema: IDocumentSchema | undefined,
242
312
  schemaName: string,
243
- ): void {
313
+ ): asserts documentSchema is IDocumentSchemaCurrentIncoming {
244
314
  // Back-compat - we can't do anything about legacy documents.
245
315
  // There is no way to validate them, so we are taking a guess that safe deployment processes used by a given app
246
316
  // do not run into compat problems.
@@ -298,59 +368,89 @@ function checkRuntimeCompatibility(
298
368
  }
299
369
 
300
370
  function and(
301
- currentDocSchema: IDocumentSchemaCurrent,
302
- desiredDocSchema: IDocumentSchemaCurrent,
371
+ persistedSchema: IDocumentSchemaCurrentIncoming,
372
+ providedSchema: IDocumentSchemaCurrent,
303
373
  ): IDocumentSchemaCurrent {
304
374
  const runtime = {};
305
375
  for (const key of new Set([
306
- ...Object.keys(currentDocSchema.runtime),
307
- ...Object.keys(desiredDocSchema.runtime),
376
+ ...Object.keys(persistedSchema.runtime),
377
+ ...Object.keys(providedSchema.runtime),
308
378
  ])) {
309
379
  runtime[key] = (documentSchemaSupportedConfigs[key] as IProperty).and(
310
- currentDocSchema.runtime[key],
311
- desiredDocSchema.runtime[key],
380
+ persistedSchema.runtime[key],
381
+ providedSchema.runtime[key],
312
382
  );
313
383
  }
384
+
385
+ // We keep the persisted minVersionForCollab if present, even if the provided minVersionForCollab
386
+ // is higher.
387
+ const minVersionForCollab =
388
+ persistedSchema.info?.minVersionForCollab ?? providedSchema.info.minVersionForCollab;
389
+
314
390
  return {
315
391
  version: currentDocumentVersionSchema,
316
- refSeq: currentDocSchema.refSeq,
392
+ refSeq: persistedSchema.refSeq,
393
+ info: { minVersionForCollab },
317
394
  runtime,
318
- } as unknown as IDocumentSchemaCurrent;
395
+ };
319
396
  }
320
397
 
321
398
  function or(
322
- currentDocSchema: IDocumentSchemaCurrent,
323
- desiredDocSchema: IDocumentSchemaCurrent,
399
+ persistedSchema: IDocumentSchemaCurrentIncoming,
400
+ providedSchema: IDocumentSchemaCurrent,
324
401
  ): IDocumentSchemaCurrent {
325
402
  const runtime = {};
326
403
  for (const key of new Set([
327
- ...Object.keys(currentDocSchema.runtime),
328
- ...Object.keys(desiredDocSchema.runtime),
404
+ ...Object.keys(persistedSchema.runtime),
405
+ ...Object.keys(providedSchema.runtime),
329
406
  ])) {
330
407
  runtime[key] = (documentSchemaSupportedConfigs[key] as IProperty).or(
331
- currentDocSchema.runtime[key],
332
- desiredDocSchema.runtime[key],
408
+ persistedSchema.runtime[key],
409
+ providedSchema.runtime[key],
333
410
  );
334
411
  }
412
+
413
+ // We take the greater of the persisted/provided minVersionForCollab
414
+ const minVersionForCollab =
415
+ persistedSchema.info === undefined
416
+ ? providedSchema.info.minVersionForCollab
417
+ : gt(persistedSchema.info.minVersionForCollab, providedSchema.info.minVersionForCollab)
418
+ ? persistedSchema.info.minVersionForCollab
419
+ : providedSchema.info.minVersionForCollab;
420
+
335
421
  return {
336
422
  version: currentDocumentVersionSchema,
337
- refSeq: currentDocSchema.refSeq,
423
+ refSeq: persistedSchema.refSeq,
424
+ info: { minVersionForCollab },
338
425
  runtime,
339
- } as unknown as IDocumentSchemaCurrent;
426
+ };
340
427
  }
341
428
 
429
+ /**
430
+ * Determines if two schemas are the "same".
431
+ * Schemas are considered **not** the same if a schema change op is required to make
432
+ * the properties of `persistedSchema` match to the properties of `providedSchema`.
433
+ */
342
434
  function same(
343
- currentDocSchema: IDocumentSchemaCurrent,
344
- desiredDocSchema: IDocumentSchemaCurrent,
435
+ persistedSchema: IDocumentSchemaCurrentIncoming,
436
+ providedSchema: IDocumentSchemaCurrent,
345
437
  ): boolean {
438
+ if (
439
+ persistedSchema.info === undefined ||
440
+ lt(persistedSchema.info.minVersionForCollab, providedSchema.info.minVersionForCollab)
441
+ ) {
442
+ // If the persisted schema's minVersionForCollab is undefined or less than the provided schema's
443
+ // minVersionForCollab, then we should send a schema change op to update the minVersionForCollab.
444
+ return false;
445
+ }
346
446
  for (const key of new Set([
347
- ...Object.keys(currentDocSchema.runtime),
348
- ...Object.keys(desiredDocSchema.runtime),
447
+ ...Object.keys(persistedSchema.runtime),
448
+ ...Object.keys(providedSchema.runtime),
349
449
  ])) {
350
450
  // If schemas differ only by type of behavior, then we should not send schema change ops!
351
451
  if (
352
452
  key !== "explicitSchemaControl" &&
353
- currentDocSchema.runtime[key] !== desiredDocSchema.runtime[key]
453
+ persistedSchema.runtime[key] !== providedSchema.runtime[key]
354
454
  ) {
355
455
  return false;
356
456
  }
@@ -436,10 +536,15 @@ function arrayToProp(arr: string[]): string[] | undefined {
436
536
  */
437
537
  export class DocumentsSchemaController {
438
538
  private explicitSchemaControl: boolean;
439
- private sendOp = true;
539
+
540
+ /**
541
+ * Have we generated a DocumentSchemaChange op and we're waiting for the ack?
542
+ * This is used to ensure that we do not generate multiple schema change ops - this client should only ever send one (if any).
543
+ */
544
+ private opPending = false;
440
545
 
441
546
  // schema coming from document metadata (snapshot we loaded from)
442
- private documentSchema: IDocumentSchemaCurrent;
547
+ private documentSchema: IDocumentSchema;
443
548
 
444
549
  // desired schema, based on feature gates / runtime options.
445
550
  // This includes requests to enable to disable functionality
@@ -461,6 +566,8 @@ export class DocumentsSchemaController {
461
566
  * @param documentMetadataSchema - current document's schema, if present.
462
567
  * @param features - features of the document schema that current session wants to see enabled.
463
568
  * @param onSchemaChange - callback that is called whenever schema is changed (not called on creation / load, only when processing document schema change ops)
569
+ * @param info - Informational properties of the document that are not subject to strict schema enforcement
570
+ * @param logger - telemetry logger from the runtime
464
571
  */
465
572
  constructor(
466
573
  existing: boolean,
@@ -468,6 +575,8 @@ export class DocumentsSchemaController {
468
575
  documentMetadataSchema: IDocumentSchema | undefined,
469
576
  features: IDocumentSchemaFeatures,
470
577
  private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
578
+ info: IDocumentSchemaInfo,
579
+ logger: ITelemetryLoggerExt,
471
580
  ) {
472
581
  // For simplicity, let's only support new schema features for explicit schema control mode
473
582
  assert(
@@ -475,10 +584,34 @@ export class DocumentsSchemaController {
475
584
  0x949 /* not supported */,
476
585
  );
477
586
 
587
+ // We check the document's metadata to see if there is a minVersionForCollab. If it's not an existing document or
588
+ // if the document is older, then it won't have one. If it does have a minVersionForCollab, we check if it's greater
589
+ // than this client's runtime version. If so, we log a telemetry event to warn the customer that the client is outdated.
590
+ // Note: We only send a warning because we will confirm via `checkRuntimeCompatibility` if this client **can** understand
591
+ // the existing document's schema. We still want to issue a warning regardless if this client can or cannot understand the
592
+ // schema since it may be a sign that the customer is not properly waiting for saturation before updating their
593
+ // `minVersionForCollab` value, which could cause disruptions to users in the future.
594
+ const existingMinVersionForCollab = documentMetadataSchema?.info?.minVersionForCollab;
595
+ if (
596
+ existingMinVersionForCollab !== undefined &&
597
+ gt(existingMinVersionForCollab, pkgVersion) &&
598
+ // We also want to avoid sending the telemetry warning for dev builds, since they currently are formatted as
599
+ // `0.0.0-#####-test`. This will cause the telemetry warning to constantly fire.
600
+ // TODO: This can be removed after ADO:41351
601
+ !isDevBuild(pkgVersion)
602
+ ) {
603
+ const warnMsg = `WARNING: The version of Fluid Framework used by this client (${pkgVersion}) is not supported by this document! Please upgrade to version ${existingMinVersionForCollab} or later to ensure compatibility.`;
604
+ logger.sendTelemetryEvent({
605
+ eventName: "MinVersionForCollabWarning",
606
+ message: warnMsg,
607
+ });
608
+ }
609
+
478
610
  // Desired schema by this session - almost all props are coming from arguments
479
611
  this.desiredSchema = {
480
612
  version: currentDocumentVersionSchema,
481
613
  refSeq: documentMetadataSchema?.refSeq ?? 0,
614
+ info,
482
615
  runtime: {
483
616
  explicitSchemaControl: boolToProp(features.explicitSchemaControl),
484
617
  compressionLz4: boolToProp(features.compressionLz4),
@@ -491,13 +624,14 @@ export class DocumentsSchemaController {
491
624
 
492
625
  // Schema coming from document metadata (snapshot we loaded from), or if no document exists
493
626
  // (this is a new document) then this is the same as desiredSchema (same as session schema in such case).
494
- // Latter is importnat sure that's what will go into summary.
627
+ // Latter is important sure that's what will go into summary.
495
628
  this.documentSchema = existing
496
- ? ((documentMetadataSchema as IDocumentSchemaCurrent) ??
629
+ ? (documentMetadataSchema ??
497
630
  ({
498
631
  version: currentDocumentVersionSchema,
499
632
  // see comment in summarizeDocumentSchema() on why it has to stay zero
500
633
  refSeq: 0,
634
+ info,
501
635
  // If it's existing document and it has no schema, then it was written by legacy client.
502
636
  // If it's a new document, then we define it's legacy-related behaviors.
503
637
  runtime: {
@@ -542,14 +676,15 @@ export class DocumentsSchemaController {
542
676
  checkRuntimeCompatibility(this.futureSchema, "future");
543
677
  }
544
678
 
545
- public summarizeDocumentSchema(refSeq: number): IDocumentSchemaCurrent | undefined {
679
+ public summarizeDocumentSchema(
680
+ refSeq: number,
681
+ ): IDocumentSchema | IDocumentSchemaCurrent | undefined {
546
682
  // For legacy behavior, we could write nothing (return undefined).
547
683
  // It does not buy us anything, as whatever written in summary does not actually impact clients operating in legacy mode.
548
684
  // But writing current used config (and assuming most of the clients settle on same config over time) will help with transition
549
685
  // out of legacy mode, as clients transitioning out of it would be able to use all the
550
686
  // features that are mentioned in schema right away, without a need to go through schema transition (and thus for a session or
551
687
  // two losing ability to use all the features)
552
-
553
688
  const schema = this.explicitSchemaControl ? this.documentSchema : this.desiredSchema;
554
689
 
555
690
  // It's important to keep refSeq at zero in legacy mode, such that transition out of it is simple and we do not have
@@ -567,20 +702,17 @@ export class DocumentsSchemaController {
567
702
  /**
568
703
  * Called by Container runtime whenever it is about to send some op.
569
704
  * It gives opportunity for controller to issue its own ops - we do not want to send ops if there are no local changes in document.
570
- * Please consider note above constructor about race conditions - current design is to send op only once in a session lifetime.
705
+ * Please consider note above constructor about race conditions - current design is to generate op only once in a session lifetime.
571
706
  * @returns Optional message to send.
572
707
  */
573
- public maybeSendSchemaMessage(): IDocumentSchemaChangeMessage | undefined {
574
- if (this.sendOp && this.futureSchema !== undefined) {
575
- this.sendOp = false;
708
+ public maybeGenerateSchemaMessage(): IDocumentSchemaChangeMessageOutgoing | undefined {
709
+ if (this.futureSchema !== undefined && !this.opPending) {
710
+ this.opPending = true;
576
711
  assert(
577
712
  this.explicitSchemaControl && this.futureSchema.runtime.explicitSchemaControl === true,
578
713
  0x94e /* not legacy */,
579
714
  );
580
- return {
581
- ...this.futureSchema,
582
- refSeq: this.documentSchema.refSeq,
583
- };
715
+ return this.futureSchema;
584
716
  }
585
717
  }
586
718
 
@@ -612,7 +744,7 @@ export class DocumentsSchemaController {
612
744
  * @returns - true if schema was accepted, otherwise false (rejected due to failed CAS)
613
745
  */
614
746
  public processDocumentSchemaMessages(
615
- contents: IDocumentSchemaChangeMessage[],
747
+ contents: IDocumentSchemaChangeMessageIncoming[],
616
748
  local: boolean,
617
749
  sequenceNumber: number,
618
750
  ): boolean {
@@ -639,10 +771,12 @@ export class DocumentsSchemaController {
639
771
 
640
772
  // Changes are in effect. Immediately check that this client understands these changes
641
773
  checkRuntimeCompatibility(content, "change");
642
-
643
- const schema: IDocumentSchema = { ...content, refSeq: sequenceNumber };
644
- this.documentSchema = schema as IDocumentSchemaCurrent;
645
- this.sessionSchema = and(this.documentSchema, this.desiredSchema);
774
+ const schema = {
775
+ ...content,
776
+ refSeq: sequenceNumber,
777
+ } satisfies IDocumentSchemaCurrentIncoming;
778
+ this.documentSchema = schema;
779
+ this.sessionSchema = and(schema, this.desiredSchema);
646
780
  assert(this.sessionSchema.refSeq === sequenceNumber, 0x97d /* seq# */);
647
781
 
648
782
  // legacy behavior is automatically off for the document once someone sends a schema op -
@@ -663,9 +797,20 @@ export class DocumentsSchemaController {
663
797
  return true;
664
798
  }
665
799
 
666
- public onDisconnect(): void {
667
- this.sendOp = true;
800
+ /**
801
+ * Indicates the pending op was not ack'd and we may try to send it again if needed.
802
+ */
803
+ public pendingOpNotAcked(): void {
804
+ this.opPending = false;
668
805
  }
669
806
  }
670
807
 
808
+ /**
809
+ * Determines if a given version is a dev-build (i.e. `0.0.0-#####-test`).
810
+ */
811
+ function isDevBuild(version: string): boolean {
812
+ const parsed = parse(version);
813
+ return parsed !== null && parsed.prerelease.includes("test");
814
+ }
815
+
671
816
  /* eslint-enable jsdoc/check-indentation */
@@ -103,10 +103,12 @@ export {
103
103
  IdCompressorMode,
104
104
  IDocumentSchemaCurrent,
105
105
  IDocumentSchema,
106
+ IDocumentSchemaInfo,
106
107
  currentDocumentVersionSchema,
107
108
  DocumentSchemaValueType,
108
109
  DocumentsSchemaController,
109
- IDocumentSchemaChangeMessage,
110
+ IDocumentSchemaChangeMessageIncoming,
111
+ IDocumentSchemaChangeMessageOutgoing,
110
112
  IDocumentSchemaFeatures,
111
113
  } from "./documentSchema.js";
112
114
  export {