@fluidframework/container-runtime 2.42.0 → 2.43.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 +4 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/channelCollection.d.ts +1 -1
- package/dist/channelCollection.d.ts.map +1 -1
- package/dist/channelCollection.js +3 -1
- package/dist/channelCollection.js.map +1 -1
- package/dist/compatUtils.d.ts +2 -0
- package/dist/compatUtils.d.ts.map +1 -1
- package/dist/compatUtils.js.map +1 -1
- package/dist/containerRuntime.d.ts +3 -3
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +43 -26
- package/dist/containerRuntime.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/opLifecycle/batchManager.d.ts +1 -0
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +3 -2
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +1 -1
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js +2 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/outbox.js +1 -1
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +7 -6
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +20 -15
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/summary/documentSchema.d.ts +40 -1
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +57 -1
- package/dist/summary/documentSchema.js.map +1 -1
- package/dist/summary/index.d.ts +1 -1
- package/dist/summary/index.d.ts.map +1 -1
- package/dist/summary/index.js.map +1 -1
- package/lib/channelCollection.d.ts +1 -1
- package/lib/channelCollection.d.ts.map +1 -1
- package/lib/channelCollection.js +3 -1
- package/lib/channelCollection.js.map +1 -1
- package/lib/compatUtils.d.ts +2 -0
- package/lib/compatUtils.d.ts.map +1 -1
- package/lib/compatUtils.js.map +1 -1
- package/lib/containerRuntime.d.ts +3 -3
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +43 -26
- package/lib/containerRuntime.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/opLifecycle/batchManager.d.ts +1 -0
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +1 -1
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +1 -1
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js +1 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/outbox.js +1 -1
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +7 -6
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +20 -15
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/summary/documentSchema.d.ts +40 -1
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +57 -1
- package/lib/summary/documentSchema.js.map +1 -1
- package/lib/summary/index.d.ts +1 -1
- package/lib/summary/index.d.ts.map +1 -1
- package/lib/summary/index.js.map +1 -1
- package/package.json +20 -20
- package/src/channelCollection.ts +3 -1
- package/src/compatUtils.ts +2 -0
- package/src/containerRuntime.ts +54 -35
- package/src/index.ts +2 -1
- package/src/opLifecycle/batchManager.ts +1 -1
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/opGroupingManager.ts +1 -1
- package/src/opLifecycle/outbox.ts +1 -1
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +30 -22
- package/src/summary/documentSchema.ts +125 -5
- package/src/summary/index.ts +1 -0
|
@@ -48,9 +48,9 @@ export interface IPendingMessage {
|
|
|
48
48
|
content: string;
|
|
49
49
|
/**
|
|
50
50
|
* The original runtime op that was submitted to the ContainerRuntime
|
|
51
|
-
* Unless this pending message came from stashed content, in which case this
|
|
51
|
+
* Unless this pending message came from stashed content, in which case this is undefined at first and then deserialized from the contents string
|
|
52
52
|
*/
|
|
53
|
-
runtimeOp
|
|
53
|
+
runtimeOp: LocalContainerRuntimeMessage | EmptyGroupedBatch | undefined; // Undefined for initial messages before parsing
|
|
54
54
|
/**
|
|
55
55
|
* Local Op Metadata that was passed to the ContainerRuntime when the op was submitted.
|
|
56
56
|
* This contains state needed when processing the ack, or to resubmit or rollback the op.
|
|
@@ -234,10 +234,11 @@ function toSerializableForm(
|
|
|
234
234
|
|
|
235
235
|
interface ReplayPendingStateOptions {
|
|
236
236
|
/**
|
|
237
|
-
* If true, only replay staged batches
|
|
237
|
+
* If true, only replay staged batches, clearing the "staged" flag.
|
|
238
|
+
* This is used when we are exiting staging mode and want to rebase and submit the staged batches without resubmitting pre-staged messages.
|
|
238
239
|
* Default: false
|
|
239
240
|
*/
|
|
240
|
-
|
|
241
|
+
committingStagedBatches: boolean;
|
|
241
242
|
/**
|
|
242
243
|
* @param squash - If true, edits should be squashed when resubmitting.
|
|
243
244
|
* Default: false
|
|
@@ -246,7 +247,7 @@ interface ReplayPendingStateOptions {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
const defaultReplayPendingStatesOptions: ReplayPendingStateOptions = {
|
|
249
|
-
|
|
250
|
+
committingStagedBatches: false,
|
|
250
251
|
squash: false,
|
|
251
252
|
};
|
|
252
253
|
|
|
@@ -300,8 +301,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
300
301
|
for (let i = 0; i < this.pendingMessages.length; i++) {
|
|
301
302
|
const element = this.pendingMessages.get(i);
|
|
302
303
|
if (
|
|
303
|
-
element
|
|
304
|
-
|
|
304
|
+
element !== undefined &&
|
|
305
|
+
hasTypicalRuntimeOp(element) && // Empty batches don't count towards user changes
|
|
305
306
|
isContainerMessageDirtyable(element.runtimeOp)
|
|
306
307
|
) {
|
|
307
308
|
return true;
|
|
@@ -742,16 +743,18 @@ export class PendingStateManager implements IDisposable {
|
|
|
742
743
|
* states in its queue. This includes triggering resubmission of unacked ops.
|
|
743
744
|
* ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
|
|
744
745
|
*/
|
|
745
|
-
public replayPendingStates(
|
|
746
|
-
const
|
|
747
|
-
|
|
746
|
+
public replayPendingStates(options?: ReplayPendingStateOptions): void {
|
|
747
|
+
const { committingStagedBatches, squash } = {
|
|
748
|
+
...defaultReplayPendingStatesOptions,
|
|
749
|
+
...options,
|
|
750
|
+
};
|
|
748
751
|
assert(
|
|
749
|
-
this.stateHandler.connected() ||
|
|
752
|
+
this.stateHandler.connected() || committingStagedBatches === true,
|
|
750
753
|
0x172 /* "The connection state is not consistent with the runtime" */,
|
|
751
754
|
);
|
|
752
755
|
|
|
753
756
|
// Staged batches have not yet been submitted so check doesn't apply
|
|
754
|
-
if (!
|
|
757
|
+
if (!committingStagedBatches) {
|
|
755
758
|
// This assert suggests we are about to send same ops twice, which will result in data loss.
|
|
756
759
|
assert(
|
|
757
760
|
this.clientIdFromLastReplay !== this.stateHandler.clientId(),
|
|
@@ -778,8 +781,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
778
781
|
let pendingMessage = this.pendingMessages.shift()!;
|
|
779
782
|
remainingPendingMessagesCount--;
|
|
780
783
|
|
|
781
|
-
// Re-queue pre-staging messages
|
|
782
|
-
if (
|
|
784
|
+
// Re-queue pre-staging messages - we are only to replay staged batches
|
|
785
|
+
if (committingStagedBatches) {
|
|
783
786
|
if (!pendingMessage.batchInfo.staged) {
|
|
784
787
|
assert(!seenStagedBatch, 0xb86 /* Staged batch was followed by non-staged batch */);
|
|
785
788
|
this.pendingMessages.push(pendingMessage);
|
|
@@ -807,8 +810,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
807
810
|
}
|
|
808
811
|
|
|
809
812
|
assert(
|
|
810
|
-
|
|
811
|
-
0xb87 /*
|
|
813
|
+
hasTypicalRuntimeOp(pendingMessage),
|
|
814
|
+
0xb87 /* runtimeOp is only undefined for empty batches */,
|
|
812
815
|
);
|
|
813
816
|
|
|
814
817
|
/**
|
|
@@ -843,8 +846,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
843
846
|
// check is >= because batch end may be last pending message
|
|
844
847
|
while (remainingPendingMessagesCount >= 0) {
|
|
845
848
|
assert(
|
|
846
|
-
|
|
847
|
-
0xb88 /*
|
|
849
|
+
hasTypicalRuntimeOp(pendingMessage),
|
|
850
|
+
0xb88 /* runtimeOp is only undefined for empty batches */,
|
|
848
851
|
);
|
|
849
852
|
batch.push({
|
|
850
853
|
runtimeOp: pendingMessage.runtimeOp,
|
|
@@ -890,15 +893,17 @@ export class PendingStateManager implements IDisposable {
|
|
|
890
893
|
*/
|
|
891
894
|
public popStagedBatches(
|
|
892
895
|
callback: (
|
|
893
|
-
|
|
896
|
+
// callback will only be given staged messages with a valid runtime op (i.e. not empty batch and not an initial message with only serialized content)
|
|
897
|
+
stagedMessage: IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage },
|
|
894
898
|
) => void,
|
|
895
899
|
): void {
|
|
896
900
|
while (!this.pendingMessages.isEmpty()) {
|
|
897
901
|
const stagedMessage = this.pendingMessages.peekBack();
|
|
898
902
|
if (stagedMessage?.batchInfo.staged === true) {
|
|
899
|
-
|
|
903
|
+
this.pendingMessages.pop();
|
|
904
|
+
|
|
905
|
+
if (hasTypicalRuntimeOp(stagedMessage)) {
|
|
900
906
|
callback(stagedMessage);
|
|
901
|
-
this.pendingMessages.pop();
|
|
902
907
|
}
|
|
903
908
|
} else {
|
|
904
909
|
break; // no more staged messages
|
|
@@ -924,7 +929,10 @@ function patchbatchInfo(
|
|
|
924
929
|
}
|
|
925
930
|
}
|
|
926
931
|
|
|
927
|
-
|
|
932
|
+
/**
|
|
933
|
+
* This filters out messages that are not "typical" runtime ops, i.e. empty batches or initial messages (which only have serialized content).
|
|
934
|
+
*/
|
|
935
|
+
function hasTypicalRuntimeOp(
|
|
928
936
|
message: IPendingMessage,
|
|
929
937
|
): message is IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage } {
|
|
930
938
|
return message.runtimeOp !== undefined && message.runtimeOp.type !== "groupedBatch";
|
|
@@ -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
|
/**
|
|
@@ -80,10 +83,42 @@ export interface IDocumentSchema {
|
|
|
80
83
|
* properties to be able to open the document.
|
|
81
84
|
*/
|
|
82
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;
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
/**
|
|
86
118
|
* Content of the type=ContainerMessageType.DocumentSchemaChange ops.
|
|
119
|
+
* The meaning of refSeq field is different in such messages (compared to other usages of IDocumentSchemaCurrent)
|
|
120
|
+
* ContainerMessageType.DocumentSchemaChange messages use CAS (Compare-and-swap) semantics, and convey
|
|
121
|
+
* regSeq of last known schema change (known to a client proposing schema change).
|
|
87
122
|
* @see InboundContainerRuntimeDocumentSchemaMessage
|
|
88
123
|
* @internal
|
|
89
124
|
*/
|
|
@@ -133,6 +168,8 @@ export interface IDocumentSchemaFeatures {
|
|
|
133
168
|
* in a way that all old/new clients are required to understand.
|
|
134
169
|
* Ex: Adding a new configuration property (under IDocumentSchema.runtime) does not require changing this version since there is logic
|
|
135
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.
|
|
136
173
|
* Ex: Changing the 'document schema acceptance' mechanism from convert-and-swap to one requiring consensus does require changing this version
|
|
137
174
|
* since all clients need to understand the new protocol.
|
|
138
175
|
* @internal
|
|
@@ -141,11 +178,15 @@ export const currentDocumentVersionSchema = 1;
|
|
|
141
178
|
|
|
142
179
|
/**
|
|
143
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
|
+
*
|
|
144
184
|
* @internal
|
|
145
185
|
*/
|
|
146
186
|
export interface IDocumentSchemaCurrent extends Required<IDocumentSchema> {
|
|
147
187
|
// This is the version of the schema that we currently understand.
|
|
148
188
|
version: typeof currentDocumentVersionSchema;
|
|
189
|
+
// This narrows the runtime property to only include the properties in IDocumentSchemaFeatures (all as optional)
|
|
149
190
|
runtime: {
|
|
150
191
|
[P in keyof IDocumentSchemaFeatures]?: IDocumentSchemaFeatures[P] extends boolean
|
|
151
192
|
? true
|
|
@@ -153,6 +194,18 @@ export interface IDocumentSchemaCurrent extends Required<IDocumentSchema> {
|
|
|
153
194
|
};
|
|
154
195
|
}
|
|
155
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
|
+
}
|
|
208
|
+
|
|
156
209
|
interface IProperty<T = unknown> {
|
|
157
210
|
and: (persistedSchema: T, providedSchema: T) => T;
|
|
158
211
|
or: (persistedSchema: T, providedSchema: T) => T;
|
|
@@ -257,7 +310,7 @@ const documentSchemaSupportedConfigs = {
|
|
|
257
310
|
function checkRuntimeCompatibility(
|
|
258
311
|
documentSchema: IDocumentSchema | undefined,
|
|
259
312
|
schemaName: string,
|
|
260
|
-
): asserts documentSchema is
|
|
313
|
+
): asserts documentSchema is IDocumentSchemaCurrentIncoming {
|
|
261
314
|
// Back-compat - we can't do anything about legacy documents.
|
|
262
315
|
// There is no way to validate them, so we are taking a guess that safe deployment processes used by a given app
|
|
263
316
|
// do not run into compat problems.
|
|
@@ -315,7 +368,7 @@ function checkRuntimeCompatibility(
|
|
|
315
368
|
}
|
|
316
369
|
|
|
317
370
|
function and(
|
|
318
|
-
persistedSchema:
|
|
371
|
+
persistedSchema: IDocumentSchemaCurrentIncoming,
|
|
319
372
|
providedSchema: IDocumentSchemaCurrent,
|
|
320
373
|
): IDocumentSchemaCurrent {
|
|
321
374
|
const runtime = {};
|
|
@@ -328,15 +381,22 @@ function and(
|
|
|
328
381
|
providedSchema.runtime[key],
|
|
329
382
|
);
|
|
330
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
|
+
|
|
331
390
|
return {
|
|
332
391
|
version: currentDocumentVersionSchema,
|
|
333
392
|
refSeq: persistedSchema.refSeq,
|
|
393
|
+
info: { minVersionForCollab },
|
|
334
394
|
runtime,
|
|
335
395
|
};
|
|
336
396
|
}
|
|
337
397
|
|
|
338
398
|
function or(
|
|
339
|
-
persistedSchema:
|
|
399
|
+
persistedSchema: IDocumentSchemaCurrentIncoming,
|
|
340
400
|
providedSchema: IDocumentSchemaCurrent,
|
|
341
401
|
): IDocumentSchemaCurrent {
|
|
342
402
|
const runtime = {};
|
|
@@ -349,17 +409,40 @@ function or(
|
|
|
349
409
|
providedSchema.runtime[key],
|
|
350
410
|
);
|
|
351
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
|
+
|
|
352
421
|
return {
|
|
353
422
|
version: currentDocumentVersionSchema,
|
|
354
423
|
refSeq: persistedSchema.refSeq,
|
|
424
|
+
info: { minVersionForCollab },
|
|
355
425
|
runtime,
|
|
356
426
|
};
|
|
357
427
|
}
|
|
358
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
|
+
*/
|
|
359
434
|
function same(
|
|
360
|
-
persistedSchema:
|
|
435
|
+
persistedSchema: IDocumentSchemaCurrentIncoming,
|
|
361
436
|
providedSchema: IDocumentSchemaCurrent,
|
|
362
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
|
+
}
|
|
363
446
|
for (const key of new Set([
|
|
364
447
|
...Object.keys(persistedSchema.runtime),
|
|
365
448
|
...Object.keys(providedSchema.runtime),
|
|
@@ -483,6 +566,8 @@ export class DocumentsSchemaController {
|
|
|
483
566
|
* @param documentMetadataSchema - current document's schema, if present.
|
|
484
567
|
* @param features - features of the document schema that current session wants to see enabled.
|
|
485
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
|
|
486
571
|
*/
|
|
487
572
|
constructor(
|
|
488
573
|
existing: boolean,
|
|
@@ -490,6 +575,8 @@ export class DocumentsSchemaController {
|
|
|
490
575
|
documentMetadataSchema: IDocumentSchema | undefined,
|
|
491
576
|
features: IDocumentSchemaFeatures,
|
|
492
577
|
private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
|
|
578
|
+
info: IDocumentSchemaInfo,
|
|
579
|
+
logger: ITelemetryLoggerExt,
|
|
493
580
|
) {
|
|
494
581
|
// For simplicity, let's only support new schema features for explicit schema control mode
|
|
495
582
|
assert(
|
|
@@ -497,10 +584,34 @@ export class DocumentsSchemaController {
|
|
|
497
584
|
0x949 /* not supported */,
|
|
498
585
|
);
|
|
499
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
|
+
|
|
500
610
|
// Desired schema by this session - almost all props are coming from arguments
|
|
501
611
|
this.desiredSchema = {
|
|
502
612
|
version: currentDocumentVersionSchema,
|
|
503
613
|
refSeq: documentMetadataSchema?.refSeq ?? 0,
|
|
614
|
+
info,
|
|
504
615
|
runtime: {
|
|
505
616
|
explicitSchemaControl: boolToProp(features.explicitSchemaControl),
|
|
506
617
|
compressionLz4: boolToProp(features.compressionLz4),
|
|
@@ -520,6 +631,7 @@ export class DocumentsSchemaController {
|
|
|
520
631
|
version: currentDocumentVersionSchema,
|
|
521
632
|
// see comment in summarizeDocumentSchema() on why it has to stay zero
|
|
522
633
|
refSeq: 0,
|
|
634
|
+
info,
|
|
523
635
|
// If it's existing document and it has no schema, then it was written by legacy client.
|
|
524
636
|
// If it's a new document, then we define it's legacy-related behaviors.
|
|
525
637
|
runtime: {
|
|
@@ -662,7 +774,7 @@ export class DocumentsSchemaController {
|
|
|
662
774
|
const schema = {
|
|
663
775
|
...content,
|
|
664
776
|
refSeq: sequenceNumber,
|
|
665
|
-
} satisfies
|
|
777
|
+
} satisfies IDocumentSchemaCurrentIncoming;
|
|
666
778
|
this.documentSchema = schema;
|
|
667
779
|
this.sessionSchema = and(schema, this.desiredSchema);
|
|
668
780
|
assert(this.sessionSchema.refSeq === sequenceNumber, 0x97d /* seq# */);
|
|
@@ -693,4 +805,12 @@ export class DocumentsSchemaController {
|
|
|
693
805
|
}
|
|
694
806
|
}
|
|
695
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
|
+
|
|
696
816
|
/* eslint-enable jsdoc/check-indentation */
|