@fluidframework/container-runtime 2.91.0 → 2.93.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/api-report/container-runtime.legacy.beta.api.md +2 -0
  4. package/container-runtime.test-files.tar +0 -0
  5. package/dist/containerCompatibility.d.ts +1 -1
  6. package/dist/containerCompatibility.d.ts.map +1 -1
  7. package/dist/containerCompatibility.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +38 -11
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +118 -86
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/gc/garbageCollection.d.ts +1 -0
  13. package/dist/gc/garbageCollection.d.ts.map +1 -1
  14. package/dist/gc/garbageCollection.js +3 -8
  15. package/dist/gc/garbageCollection.js.map +1 -1
  16. package/dist/gc/gcDefinitions.d.ts +4 -0
  17. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  18. package/dist/gc/gcDefinitions.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/legacy.d.ts +1 -1
  24. package/dist/opLifecycle/batchManager.d.ts +3 -9
  25. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  26. package/dist/opLifecycle/batchManager.js +5 -3
  27. package/dist/opLifecycle/batchManager.js.map +1 -1
  28. package/dist/opLifecycle/index.d.ts +1 -1
  29. package/dist/opLifecycle/index.d.ts.map +1 -1
  30. package/dist/opLifecycle/index.js +2 -1
  31. package/dist/opLifecycle/index.js.map +1 -1
  32. package/dist/opLifecycle/opGroupingManager.d.ts +6 -0
  33. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  34. package/dist/opLifecycle/opGroupingManager.js +11 -2
  35. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  36. package/dist/opLifecycle/opSerialization.d.ts +3 -1
  37. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  38. package/dist/opLifecycle/opSerialization.js +11 -9
  39. package/dist/opLifecycle/opSerialization.js.map +1 -1
  40. package/dist/opLifecycle/outbox.d.ts +8 -11
  41. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  42. package/dist/opLifecycle/outbox.js +42 -66
  43. package/dist/opLifecycle/outbox.js.map +1 -1
  44. package/dist/packageVersion.d.ts +1 -1
  45. package/dist/packageVersion.js +1 -1
  46. package/dist/packageVersion.js.map +1 -1
  47. package/dist/pendingStateManager.d.ts +8 -9
  48. package/dist/pendingStateManager.d.ts.map +1 -1
  49. package/dist/pendingStateManager.js +24 -22
  50. package/dist/pendingStateManager.js.map +1 -1
  51. package/dist/public.d.ts +1 -1
  52. package/dist/runtimeLayerCompatState.d.ts +2 -2
  53. package/dist/summary/documentSchema.d.ts +9 -3
  54. package/dist/summary/documentSchema.d.ts.map +1 -1
  55. package/dist/summary/documentSchema.js +19 -3
  56. package/dist/summary/documentSchema.js.map +1 -1
  57. package/dist/summary/orderedClientElection.js +2 -2
  58. package/dist/summary/orderedClientElection.js.map +1 -1
  59. package/dist/summary/summaryManager.d.ts +1 -0
  60. package/dist/summary/summaryManager.d.ts.map +1 -1
  61. package/dist/summary/summaryManager.js +9 -0
  62. package/dist/summary/summaryManager.js.map +1 -1
  63. package/eslint.config.mts +1 -1
  64. package/internal.d.ts +1 -1
  65. package/legacy.d.ts +1 -1
  66. package/lib/containerCompatibility.d.ts +1 -1
  67. package/lib/containerCompatibility.d.ts.map +1 -1
  68. package/lib/containerCompatibility.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +38 -11
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +118 -87
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/gc/garbageCollection.d.ts +1 -0
  74. package/lib/gc/garbageCollection.d.ts.map +1 -1
  75. package/lib/gc/garbageCollection.js +3 -8
  76. package/lib/gc/garbageCollection.js.map +1 -1
  77. package/lib/gc/gcDefinitions.d.ts +4 -0
  78. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  79. package/lib/gc/gcDefinitions.js.map +1 -1
  80. package/lib/index.d.ts +1 -1
  81. package/lib/index.d.ts.map +1 -1
  82. package/lib/index.js +1 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/legacy.d.ts +1 -1
  85. package/lib/opLifecycle/batchManager.d.ts +3 -9
  86. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  87. package/lib/opLifecycle/batchManager.js +5 -3
  88. package/lib/opLifecycle/batchManager.js.map +1 -1
  89. package/lib/opLifecycle/index.d.ts +1 -1
  90. package/lib/opLifecycle/index.d.ts.map +1 -1
  91. package/lib/opLifecycle/index.js +1 -1
  92. package/lib/opLifecycle/index.js.map +1 -1
  93. package/lib/opLifecycle/opGroupingManager.d.ts +6 -0
  94. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  95. package/lib/opLifecycle/opGroupingManager.js +10 -1
  96. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  97. package/lib/opLifecycle/opSerialization.d.ts +3 -1
  98. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  99. package/lib/opLifecycle/opSerialization.js +11 -9
  100. package/lib/opLifecycle/opSerialization.js.map +1 -1
  101. package/lib/opLifecycle/outbox.d.ts +8 -11
  102. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  103. package/lib/opLifecycle/outbox.js +43 -67
  104. package/lib/opLifecycle/outbox.js.map +1 -1
  105. package/lib/packageVersion.d.ts +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/pendingStateManager.d.ts +8 -9
  109. package/lib/pendingStateManager.d.ts.map +1 -1
  110. package/lib/pendingStateManager.js +24 -22
  111. package/lib/pendingStateManager.js.map +1 -1
  112. package/lib/public.d.ts +1 -1
  113. package/lib/runtimeLayerCompatState.d.ts +2 -2
  114. package/lib/summary/documentSchema.d.ts +9 -3
  115. package/lib/summary/documentSchema.d.ts.map +1 -1
  116. package/lib/summary/documentSchema.js +19 -3
  117. package/lib/summary/documentSchema.js.map +1 -1
  118. package/lib/summary/orderedClientElection.js +2 -2
  119. package/lib/summary/orderedClientElection.js.map +1 -1
  120. package/lib/summary/summaryManager.d.ts +1 -0
  121. package/lib/summary/summaryManager.d.ts.map +1 -1
  122. package/lib/summary/summaryManager.js +9 -0
  123. package/lib/summary/summaryManager.js.map +1 -1
  124. package/lib/tsdoc-metadata.json +1 -1
  125. package/package.json +27 -28
  126. package/src/containerCompatibility.ts +2 -0
  127. package/src/containerRuntime.ts +163 -106
  128. package/src/gc/garbageCollection.ts +4 -9
  129. package/src/gc/gcDefinitions.ts +4 -0
  130. package/src/index.ts +1 -0
  131. package/src/opLifecycle/batchManager.ts +6 -13
  132. package/src/opLifecycle/index.ts +1 -0
  133. package/src/opLifecycle/opGroupingManager.ts +11 -1
  134. package/src/opLifecycle/opSerialization.ts +14 -12
  135. package/src/opLifecycle/outbox.ts +53 -86
  136. package/src/packageVersion.ts +1 -1
  137. package/src/pendingStateManager.ts +31 -33
  138. package/src/summary/documentSchema.ts +25 -2
  139. package/src/summary/orderedClientElection.ts +2 -2
  140. package/src/summary/summaryManager.ts +11 -0
@@ -17,6 +17,13 @@ import type {
17
17
  OutboundSingletonBatch,
18
18
  } from "./definitions.js";
19
19
 
20
+ /**
21
+ * The number of ops in a batch above which the batch is considered "large"
22
+ * for telemetry purposes. Used by both {@link OpGroupingManager} (GroupLargeBatch event)
23
+ * and as the default staging-mode auto-flush threshold.
24
+ */
25
+ export const largeBatchThreshold = 1000;
26
+
20
27
  /**
21
28
  * Grouping makes assumptions about the shape of message contents. This interface codifies those assumptions, but does not validate them.
22
29
  */
@@ -123,7 +130,10 @@ export class OpGroupingManager {
123
130
  return batch as OutboundSingletonBatch;
124
131
  }
125
132
 
126
- if (batch.messages.length >= 1000) {
133
+ // Use > (not >=) so that batches flushed exactly at the staging-mode
134
+ // auto-flush threshold (which defaults to largeBatchThreshold) don't
135
+ // trigger this event. Only genuinely oversized batches are logged.
136
+ if (batch.messages.length > largeBatchThreshold) {
127
137
  this.logger.sendTelemetryEvent({
128
138
  eventName: "GroupLargeBatch",
129
139
  length: batch.messages.length,
@@ -40,16 +40,18 @@ export function serializeOp(
40
40
  | EmptyGroupedBatch
41
41
  | LocalContainerRuntimeMessage
42
42
  | LocalContainerRuntimeMessage[],
43
- ): string {
44
- return JSON.stringify(
45
- toSerialize,
46
- // replacer:
47
- (key, value: unknown) => {
48
- // If 'value' is an IFluidHandle return its encoded form.
49
- if (isFluidHandle(value)) {
50
- return encodeHandleForSerialization(toFluidHandleInternal(value));
51
- }
52
- return value;
53
- },
54
- );
43
+ ): { content: string } {
44
+ return {
45
+ content: JSON.stringify(
46
+ toSerialize,
47
+ // replacer:
48
+ (key, value: unknown) => {
49
+ // If 'value' is an IFluidHandle return its encoded form.
50
+ if (isFluidHandle(value)) {
51
+ return encodeHandleForSerialization(toFluidHandleInternal(value));
52
+ }
53
+ return value;
54
+ },
55
+ ),
56
+ };
55
57
  }
@@ -28,6 +28,7 @@ import {
28
28
  type BatchSequenceNumbers,
29
29
  sequenceNumbersMatch,
30
30
  type BatchId,
31
+ addBatchMetadata,
31
32
  } from "./batchManager.js";
32
33
  import type {
33
34
  LocalBatchMessage,
@@ -48,12 +49,6 @@ export interface IOutboxConfig {
48
49
  * The maximum size of a batch that we can send over the wire.
49
50
  */
50
51
  readonly maxBatchSizeInBytes: number;
51
- /**
52
- * If true, maybeFlushPartialBatch will flush the batch if the reference sequence number changed
53
- * since the batch started. Otherwise, it will throw in this case (apart from reentrancy which is handled elsewhere).
54
- * Once the new throw-based flow is proved in a production environment, this option will be removed.
55
- */
56
- readonly flushPartialBatches: boolean;
57
52
  }
58
53
 
59
54
  export interface IOutboxParameters {
@@ -71,6 +66,13 @@ export interface IOutboxParameters {
71
66
  readonly getCurrentSequenceNumbers: () => BatchSequenceNumbers;
72
67
  readonly reSubmit: (message: PendingMessageResubmitData, squash: boolean) => void;
73
68
  readonly opReentrancy: () => boolean;
69
+ /**
70
+ * JIT callback to generate an ID allocation op at flush time.
71
+ * Called after rebase (if any), so the returned message has the correct refSeq.
72
+ *
73
+ * @returns A LocalBatchMessage for the ID allocation op, or undefined if no IDs need allocating.
74
+ */
75
+ readonly generateIdAllocationOp: () => LocalBatchMessage | undefined;
74
76
  }
75
77
 
76
78
  /**
@@ -142,7 +144,7 @@ export function localBatchToOutboundBatch({
142
144
  // Shallow copy each message as we switch types
143
145
  const outboundMessages = localBatch.messages.map<OutboundBatchMessage>(
144
146
  ({ runtimeOp, ...message }) => ({
145
- contents: serializeOp(runtimeOp),
147
+ contents: serializeOp(runtimeOp).content,
146
148
  ...message,
147
149
  }),
148
150
  );
@@ -195,7 +197,6 @@ export class Outbox {
195
197
  private readonly logger: ITelemetryLoggerExt;
196
198
  private readonly mainBatch: BatchManager;
197
199
  private readonly blobAttachBatch: BatchManager;
198
- private readonly idAllocationBatch: BatchManager;
199
200
  private batchRebasesToReport = 5;
200
201
  private rebasing = false;
201
202
 
@@ -211,16 +212,12 @@ export class Outbox {
211
212
  constructor(private readonly params: IOutboxParameters) {
212
213
  this.logger = createChildLogger({ logger: params.logger, namespace: "Outbox" });
213
214
 
214
- this.mainBatch = new BatchManager({ canRebase: true });
215
- this.blobAttachBatch = new BatchManager({ canRebase: true });
216
- this.idAllocationBatch = new BatchManager({
217
- canRebase: false,
218
- ignoreBatchId: true,
219
- });
215
+ this.mainBatch = new BatchManager({ disableGroupedBatching: false });
216
+ this.blobAttachBatch = new BatchManager({ disableGroupedBatching: true });
220
217
  }
221
218
 
222
219
  public get messageCount(): number {
223
- return this.mainBatch.length + this.blobAttachBatch.length + this.idAllocationBatch.length;
220
+ return this.mainBatch.length + this.blobAttachBatch.length;
224
221
  }
225
222
 
226
223
  public get mainBatchMessageCount(): number {
@@ -231,19 +228,12 @@ export class Outbox {
231
228
  return this.blobAttachBatch.length;
232
229
  }
233
230
 
234
- public get idAllocationBatchMessageCount(): number {
235
- return this.idAllocationBatch.length;
236
- }
237
-
238
231
  public get isEmpty(): boolean {
239
232
  return this.messageCount === 0;
240
233
  }
241
234
 
242
235
  public containsUserChanges(): boolean {
243
- return (
244
- this.mainBatch.containsUserChanges() || this.blobAttachBatch.containsUserChanges()
245
- // ID Allocation ops are not user changes
246
- );
236
+ return this.mainBatch.containsUserChanges() || this.blobAttachBatch.containsUserChanges();
247
237
  }
248
238
 
249
239
  /**
@@ -257,13 +247,11 @@ export class Outbox {
257
247
  * last message processed by the ContainerRuntime. In the absence of op reentrancy, this
258
248
  * pair will remain stable during a single JS turn during which the batch is being built up.
259
249
  */
260
- private maybeFlushPartialBatch(): void {
250
+ private outboxSequenceNumberCoherencyCheck(): void {
261
251
  const mainBatchSeqNums = this.mainBatch.sequenceNumbers;
262
252
  const blobAttachSeqNums = this.blobAttachBatch.sequenceNumbers;
263
- const idAllocSeqNums = this.idAllocationBatch.sequenceNumbers;
264
253
  assert(
265
- sequenceNumbersMatch(mainBatchSeqNums, blobAttachSeqNums) &&
266
- sequenceNumbersMatch(mainBatchSeqNums, idAllocSeqNums),
254
+ sequenceNumbersMatch(mainBatchSeqNums, blobAttachSeqNums),
267
255
  0x58d /* Reference sequence numbers from both batches must be in sync */,
268
256
  );
269
257
 
@@ -271,8 +259,7 @@ export class Outbox {
271
259
 
272
260
  if (
273
261
  sequenceNumbersMatch(mainBatchSeqNums, currentSequenceNumbers) &&
274
- sequenceNumbersMatch(blobAttachSeqNums, currentSequenceNumbers) &&
275
- sequenceNumbersMatch(idAllocSeqNums, currentSequenceNumbers)
262
+ sequenceNumbersMatch(blobAttachSeqNums, currentSequenceNumbers)
276
263
  ) {
277
264
  // The reference sequence numbers are stable, there is nothing to do
278
265
  return;
@@ -295,10 +282,7 @@ export class Outbox {
295
282
  this.logger.sendTelemetryEvent(
296
283
  {
297
284
  // Only log error if this is truly unexpected
298
- category:
299
- expectedDueToReentrancy || this.params.config.flushPartialBatches
300
- ? "generic"
301
- : "error",
285
+ category: expectedDueToReentrancy ? "generic" : "error",
302
286
  eventName: "ReferenceSequenceNumberMismatch",
303
287
  details: {
304
288
  expectedDueToReentrancy,
@@ -314,12 +298,6 @@ export class Outbox {
314
298
  );
315
299
  }
316
300
 
317
- // If we're configured to flush partial batches, do that now and return (don't throw)
318
- if (this.params.config.flushPartialBatches) {
319
- this.flushAll();
320
- return;
321
- }
322
-
323
301
  // If we are in a reentrant context, we know this can happen without causing any harm.
324
302
  if (expectedDueToReentrancy) {
325
303
  return;
@@ -329,23 +307,17 @@ export class Outbox {
329
307
  }
330
308
 
331
309
  public submit(message: LocalBatchMessage): void {
332
- this.maybeFlushPartialBatch();
310
+ this.outboxSequenceNumberCoherencyCheck();
333
311
 
334
312
  this.addMessageToBatchManager(this.mainBatch, message);
335
313
  }
336
314
 
337
315
  public submitBlobAttach(message: LocalBatchMessage): void {
338
- this.maybeFlushPartialBatch();
316
+ this.outboxSequenceNumberCoherencyCheck();
339
317
 
340
318
  this.addMessageToBatchManager(this.blobAttachBatch, message);
341
319
  }
342
320
 
343
- public submitIdAllocation(message: LocalBatchMessage): void {
344
- this.maybeFlushPartialBatch();
345
-
346
- this.addMessageToBatchManager(this.idAllocationBatch, message);
347
- }
348
-
349
321
  private addMessageToBatchManager(
350
322
  batchManager: BatchManager,
351
323
  message: LocalBatchMessage,
@@ -367,11 +339,12 @@ export class Outbox {
367
339
  public flush(resubmitInfo?: BatchResubmitInfo): void {
368
340
  // We have nothing to flush if all batchManagers are empty, and we we're not needing to resubmit an empty batch placeholder
369
341
  if (
370
- this.idAllocationBatch.empty &&
371
342
  this.blobAttachBatch.empty &&
372
343
  this.mainBatch.empty &&
373
344
  resubmitInfo?.batchId === undefined
374
345
  ) {
346
+ // Note that it's possible that there are unfinalized ranges in the ID Compressor,
347
+ // but there's no urgency to flush those if they're not referenced in any messages.
375
348
  return;
376
349
  }
377
350
 
@@ -383,8 +356,7 @@ export class Outbox {
383
356
  }
384
357
 
385
358
  private flushAll(resubmitInfo?: BatchResubmitInfo): void {
386
- const allBatchesEmpty =
387
- this.idAllocationBatch.empty && this.blobAttachBatch.empty && this.mainBatch.empty;
359
+ const allBatchesEmpty = this.blobAttachBatch.empty && this.mainBatch.empty;
388
360
  if (allBatchesEmpty) {
389
361
  // If we're resubmitting with a batchId and all batches are empty, we need to flush an empty batch.
390
362
  // Note that we currently resubmit one batch at a time, so on resubmit, 1 of the 2 batches will *always* be empty.
@@ -397,22 +369,8 @@ export class Outbox {
397
369
  return;
398
370
  }
399
371
 
400
- // Don't use resubmittingBatchId for idAllocationBatch.
401
- // ID Allocation messages are not directly resubmitted so don't pass the resubmitInfo
402
- this.flushInternal({
403
- batchManager: this.idAllocationBatch,
404
- // Note: For now, we will never stage ID Allocation messages.
405
- // They won't contain personal info and no harm in extra allocations in case of discarding the staged changes
406
- });
407
- this.flushInternal({
408
- batchManager: this.blobAttachBatch,
409
- disableGroupedBatching: true,
410
- resubmitInfo,
411
- });
412
- this.flushInternal({
413
- batchManager: this.mainBatch,
414
- resubmitInfo,
415
- });
372
+ this.flushInternal(this.blobAttachBatch, resubmitInfo);
373
+ this.flushInternal(this.mainBatch, resubmitInfo);
416
374
  }
417
375
 
418
376
  private flushEmptyBatch(
@@ -444,17 +402,15 @@ export class Outbox {
444
402
  return;
445
403
  }
446
404
 
447
- private flushInternal(params: {
448
- batchManager: BatchManager;
449
- disableGroupedBatching?: boolean;
450
- resubmitInfo?: BatchResubmitInfo; // undefined if not resubmitting
451
- }): void {
452
- const { batchManager, disableGroupedBatching = false, resubmitInfo } = params;
405
+ private flushInternal(
406
+ batchManager: BatchManager,
407
+ resubmitInfo?: BatchResubmitInfo, // undefined if not resubmitting
408
+ ): void {
453
409
  if (batchManager.empty) {
454
410
  return;
455
411
  }
456
412
 
457
- const rawBatch = batchManager.popBatch(resubmitInfo?.batchId);
413
+ let rawBatch = batchManager.popBatch();
458
414
 
459
415
  // On resubmit we use the original batch's staged state, so these should match as well.
460
416
  const staged = rawBatch.staged === true;
@@ -464,13 +420,15 @@ export class Outbox {
464
420
  );
465
421
 
466
422
  const groupingEnabled =
467
- !disableGroupedBatching && this.params.groupingManager.groupedBatchingEnabled();
468
- if (
469
- batchManager.options.canRebase &&
470
- rawBatch.hasReentrantOps === true &&
471
- groupingEnabled
472
- ) {
423
+ !batchManager.options.disableGroupedBatching &&
424
+ this.params.groupingManager.groupedBatchingEnabled();
425
+ if (rawBatch.hasReentrantOps === true) {
426
+ assert(
427
+ resubmitInfo === undefined,
428
+ 0xcf2 /* Re-submitting a batch with reentrant ops is not supported */,
429
+ );
473
430
  assert(!this.rebasing, 0x6fa /* A rebased batch should never have reentrant ops */);
431
+ // Rebase the current batch (resubmit the ops one-by-one) and then reinvoke flushInternal.
474
432
  // If a batch contains reentrant ops (ops created as a result from processing another op)
475
433
  // it needs to be rebased so that we can ensure consistent reference sequence numbers
476
434
  // and eventual consistency at the DDS level.
@@ -481,11 +439,22 @@ export class Outbox {
481
439
  return;
482
440
  }
483
441
 
484
- let clientSequenceNumber: number | undefined;
485
442
  // Did we disconnect? (i.e. is shouldSend false?)
486
443
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
487
444
  // Because flush() is a task that executes async (on clean stack), we can get here in disconnected state.
488
- if (this.params.shouldSend() && !staged) {
445
+ const shouldSendNow = this.params.shouldSend() && !staged;
446
+ let clientSequenceNumber: number | undefined;
447
+ if (shouldSendNow) {
448
+ // Generate ID Allocation op just-in-time, after rebase (if any), and before addBatchMetadata,
449
+ // so that the prepended idAllocMsg is correctly marked as the first op in the batch.
450
+ // This ensures the refSeq is correct (matching the rest of the batch) and that
451
+ // ID ranges aren't lost during rebase (since reSubmit drops IdAllocation ops).
452
+ // Only generate for non-staged batches — ID alloc ops are always non-staged.
453
+ const idAllocMsg = this.params.generateIdAllocationOp();
454
+ if (idAllocMsg !== undefined) {
455
+ rawBatch = { ...rawBatch, messages: [idAllocMsg, ...rawBatch.messages] };
456
+ }
457
+ addBatchMetadata(rawBatch, resubmitInfo?.batchId);
489
458
  const virtualizedBatch = this.virtualizeBatch(rawBatch, groupingEnabled);
490
459
 
491
460
  clientSequenceNumber = this.sendBatch(virtualizedBatch);
@@ -493,13 +462,14 @@ export class Outbox {
493
462
  clientSequenceNumber === undefined || clientSequenceNumber >= 0,
494
463
  0x9d2 /* unexpected negative clientSequenceNumber (empty batch should yield undefined) */,
495
464
  );
465
+ } else {
466
+ addBatchMetadata(rawBatch, resubmitInfo?.batchId);
496
467
  }
497
468
 
498
469
  this.params.pendingStateManager.onFlushBatch(
499
470
  rawBatch.messages,
500
471
  clientSequenceNumber,
501
472
  staged,
502
- batchManager.options.ignoreBatchId,
503
473
  );
504
474
  }
505
475
 
@@ -511,7 +481,6 @@ export class Outbox {
511
481
  */
512
482
  private rebase(rawBatch: LocalBatch, batchManager: BatchManager): void {
513
483
  assert(!this.rebasing, 0x6fb /* Reentrancy */);
514
- assert(batchManager.options.canRebase, 0x9a7 /* BatchManager does not support rebase */);
515
484
 
516
485
  this.rebasing = true;
517
486
  const squash = false;
@@ -538,7 +507,7 @@ export class Outbox {
538
507
  this.batchRebasesToReport--;
539
508
  }
540
509
 
541
- this.flushInternal({ batchManager });
510
+ this.flushInternal(batchManager);
542
511
  this.rebasing = false;
543
512
  }
544
513
 
@@ -679,7 +648,6 @@ export class Outbox {
679
648
  */
680
649
  public getBatchCheckpoints(): {
681
650
  mainBatch: IBatchCheckpoint;
682
- idAllocationBatch: IBatchCheckpoint;
683
651
  blobAttachBatch: IBatchCheckpoint;
684
652
  } {
685
653
  // This variable is declared with a specific type so that we have a standard import of the IBatchCheckpoint type.
@@ -687,7 +655,6 @@ export class Outbox {
687
655
  const mainBatch: IBatchCheckpoint = this.mainBatch.checkpoint();
688
656
  return {
689
657
  mainBatch,
690
- idAllocationBatch: this.idAllocationBatch.checkpoint(),
691
658
  blobAttachBatch: this.blobAttachBatch.checkpoint(),
692
659
  };
693
660
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.91.0";
9
+ export const pkgVersion = "2.93.0";
@@ -84,10 +84,6 @@ export interface IPendingMessage {
84
84
  * length of the batch (how many runtime messages here)
85
85
  */
86
86
  length: number;
87
- /**
88
- * If true, don't compare batchID of incoming batches to this. e.g. ID Allocation Batch IDs should be ignored
89
- */
90
- ignoreBatchId?: boolean;
91
87
  /**
92
88
  * If true, this batch is staged and should not actually be submitted on replayPendingStates.
93
89
  */
@@ -335,7 +331,9 @@ export class PendingStateManager implements IDisposable {
335
331
  return this.pendingMessagesCount !== 0;
336
332
  }
337
333
 
338
- public getLocalState(snapshotSequenceNumber?: number): IPendingLocalState {
334
+ public getLocalState(snapshotSequenceNumber?: number): {
335
+ pending: IPendingLocalState;
336
+ } {
339
337
  assert(
340
338
  this.initialMessages.isEmpty(),
341
339
  0x2e9 /* "Must call getLocalState() after applying initial states" */,
@@ -359,10 +357,12 @@ export class PendingStateManager implements IDisposable {
359
357
  }
360
358
  }
361
359
  return {
362
- pendingStates: [
363
- ...newSavedOps,
364
- ...this.pendingMessages.toArray().map((message) => toSerializableForm(message)),
365
- ],
360
+ pending: {
361
+ pendingStates: [
362
+ ...newSavedOps,
363
+ ...this.pendingMessages.toArray().map((message) => toSerializableForm(message)),
364
+ ],
365
+ },
366
366
  };
367
367
  }
368
368
 
@@ -403,13 +403,11 @@ export class PendingStateManager implements IDisposable {
403
403
  * @param clientSequenceNumber - The CSN of the first message in the batch,
404
404
  * or undefined if the batch was not yet sent (e.g. by the time we flushed we lost the connection)
405
405
  * @param staged - Indicates whether batch is staged (not to be submitted while runtime is in Staging Mode)
406
- * @param ignoreBatchId - Whether to ignore the batchId in the batchStartInfo
407
406
  */
408
407
  public onFlushBatch(
409
408
  batch: LocalBatchMessage[] | [LocalEmptyBatchPlaceholder],
410
409
  clientSequenceNumber: number | undefined,
411
410
  staged: boolean,
412
- ignoreBatchId?: boolean,
413
411
  ): void {
414
412
  // clientId and batchStartCsn are used for generating the batchId so we can detect container forks
415
413
  // where this batch was submitted by two different clients rehydrating from the same local state.
@@ -427,7 +425,7 @@ export class PendingStateManager implements IDisposable {
427
425
  clientId !== undefined,
428
426
  0xa33 /* clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case */,
429
427
  );
430
-
428
+ const batchInfo = { clientId, batchStartCsn, length: batch.length, staged };
431
429
  for (const message of batch) {
432
430
  const {
433
431
  runtimeOp,
@@ -438,12 +436,11 @@ export class PendingStateManager implements IDisposable {
438
436
  const pendingMessage: IPendingMessage = {
439
437
  type: "message",
440
438
  referenceSequenceNumber,
441
- content: serializeOp(runtimeOp),
439
+ content: serializeOp(runtimeOp).content,
442
440
  runtimeOp,
443
441
  localOpMetadata,
444
442
  opMetadata,
445
- // Note: We only will read this off the first message, but put it on all for simplicity
446
- batchInfo: { clientId, batchStartCsn, length: batch.length, ignoreBatchId, staged },
443
+ batchInfo,
447
444
  };
448
445
  this.pendingMessages.push(pendingMessage);
449
446
  }
@@ -509,23 +506,14 @@ export class PendingStateManager implements IDisposable {
509
506
  * @returns whether the batch IDs match
510
507
  */
511
508
  private remoteBatchMatchesPendingBatch(remoteBatchStart: BatchStartInfo): boolean {
512
- // Find the first pending message that uses Batch ID, to compare to the incoming remote batch.
513
- // If there is no such message, then the incoming remote batch doesn't have a match here and we can return.
514
- const firstIndexUsingBatchId = Array.from({
515
- length: this.pendingMessages.length,
516
- }).findIndex((_, i) => this.pendingMessages.get(i)?.batchInfo.ignoreBatchId !== true);
517
- const pendingMessageUsingBatchId =
518
- firstIndexUsingBatchId === -1
519
- ? undefined
520
- : this.pendingMessages.get(firstIndexUsingBatchId);
521
-
522
- if (pendingMessageUsingBatchId === undefined) {
509
+ const pendingMessage = this.pendingMessages.peekFront();
510
+ if (pendingMessage === undefined) {
523
511
  return false;
524
512
  }
525
513
 
526
514
  // We must compare the effective batch IDs, since one of these ops
527
515
  // may have been the original, not resubmitted, so wouldn't have its batch ID stamped yet.
528
- const pendingBatchId = getEffectiveBatchId(pendingMessageUsingBatchId);
516
+ const pendingBatchId = getEffectiveBatchId(pendingMessage);
529
517
  const inboundBatchId = getEffectiveBatchId(remoteBatchStart);
530
518
 
531
519
  return pendingBatchId === inboundBatchId;
@@ -747,8 +735,12 @@ export class PendingStateManager implements IDisposable {
747
735
  * Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
748
736
  * states in its queue. This includes triggering resubmission of unacked ops.
749
737
  * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
738
+ *
739
+ * @returns The unique batch infos for all batches that were replayed.
750
740
  */
751
- public replayPendingStates(options?: ReplayPendingStateOptions): void {
741
+ public replayPendingStates(
742
+ options?: ReplayPendingStateOptions,
743
+ ): IPendingMessage["batchInfo"][] {
752
744
  const { committingStagedBatches, squash } = {
753
745
  ...defaultReplayPendingStatesOptions,
754
746
  ...options,
@@ -775,6 +767,7 @@ export class PendingStateManager implements IDisposable {
775
767
 
776
768
  const initialPendingMessagesCount = this.pendingMessages.length;
777
769
  let remainingPendingMessagesCount = this.pendingMessages.length;
770
+ const replayedBatchSet = new Set<IPendingMessage["batchInfo"]>();
778
771
 
779
772
  let seenStagedBatch = false;
780
773
 
@@ -802,16 +795,14 @@ export class PendingStateManager implements IDisposable {
802
795
  assert(batchMetadataFlag !== false, 0x41b /* We cannot process batches in chunks */);
803
796
 
804
797
  // The next message starts a batch (possibly single-message), and we'll need its batchId.
805
- const batchId =
806
- pendingMessage.batchInfo.ignoreBatchId === true
807
- ? undefined
808
- : getEffectiveBatchId(pendingMessage);
798
+ const batchId = getEffectiveBatchId(pendingMessage);
809
799
 
810
800
  const staged = pendingMessage.batchInfo.staged;
811
801
 
812
802
  if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
813
803
  // Resubmit no messages, with the batchId. Will result in another empty batch marker.
814
804
  this.stateHandler.reSubmitBatch([], { batchId, staged, squash });
805
+ replayedBatchSet.add(pendingMessage.batchInfo);
815
806
  continue;
816
807
  }
817
808
 
@@ -838,6 +829,7 @@ export class PendingStateManager implements IDisposable {
838
829
  ],
839
830
  { batchId, staged, squash },
840
831
  );
832
+ replayedBatchSet.add(pendingMessage.batchInfo);
841
833
  continue;
842
834
  }
843
835
  // else: batchMetadataFlag === true (It's a typical multi-message batch)
@@ -877,6 +869,7 @@ export class PendingStateManager implements IDisposable {
877
869
  }
878
870
 
879
871
  this.stateHandler.reSubmitBatch(batch, { batchId, staged, squash });
872
+ replayedBatchSet.add(pendingMessage.batchInfo);
880
873
  }
881
874
 
882
875
  if (!committingStagedBatches) {
@@ -894,6 +887,8 @@ export class PendingStateManager implements IDisposable {
894
887
  clientId: this.stateHandler.clientId(),
895
888
  });
896
889
  }
890
+
891
+ return [...replayedBatchSet];
897
892
  }
898
893
 
899
894
  /**
@@ -904,11 +899,13 @@ export class PendingStateManager implements IDisposable {
904
899
  // 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)
905
900
  stagedMessage: IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage },
906
901
  ) => void,
907
- ): void {
902
+ ): IPendingMessage["batchInfo"][] {
903
+ const batchSet = new Set<IPendingMessage["batchInfo"]>();
908
904
  while (!this.pendingMessages.isEmpty()) {
909
905
  const stagedMessage = this.pendingMessages.peekBack();
910
906
  if (stagedMessage?.batchInfo.staged === true) {
911
907
  this.pendingMessages.pop();
908
+ batchSet.add(stagedMessage.batchInfo);
912
909
 
913
910
  if (hasTypicalRuntimeOp(stagedMessage)) {
914
911
  callback(stagedMessage);
@@ -921,6 +918,7 @@ export class PendingStateManager implements IDisposable {
921
918
  this.pendingMessages.toArray().every((m) => m.batchInfo.staged !== true),
922
919
  0xb89 /* Shouldn't be any more staged messages */,
923
920
  );
921
+ return [...batchSet];
924
922
  }
925
923
  }
926
924
 
@@ -512,7 +512,7 @@ function arrayToProp(arr: string[]): string[] | undefined {
512
512
  *
513
513
  * Users of this class need to use DocumentsSchemaController.sessionSchema to determine what features can be used.
514
514
  *
515
- * There are two modes this class can operate:
515
+ * There are three modes this class can operate:
516
516
  * 1) Legacy mode. In such mode it does not issue any ops to change document schema. Any changes happen implicitly,
517
517
  * right away, and new features are available right away
518
518
  * 2) Non-legacy mode. In such mode any changes to schema require an op roundtrip. This class will manage such transitions.
@@ -523,6 +523,9 @@ function arrayToProp(arr: string[]): string[] | undefined {
523
523
  * then eventually all documents that are modified will have that feature reflected in their schema. It could require
524
524
  * multiple reloads / new sessions to get there (depends on if code reacts to schema changes right away, or only consults
525
525
  * schema on document load).
526
+ * 3) Schema upgrade disabled mode (disableSchemaUpgrade = true). In this mode the controller will never send DocumentSchemaChange ops
527
+ * and will throw an error if any incoming schema change ops are received. The document schema is effectively frozen at the schema
528
+ * loaded for this session (snapshot) and will not accept further schema-change ops.
526
529
  *
527
530
  * How schemas are changed (in non-legacy mode):
528
531
  * If a client needs to change a schema, it will attempt to do so as part of normal ops sending process.
@@ -569,6 +572,7 @@ export class DocumentsSchemaController {
569
572
  * @param onSchemaChange - callback that is called whenever schema is changed (not called on creation / load, only when processing document schema change ops)
570
573
  * @param info - Informational properties of the document that are not subject to strict schema enforcement
571
574
  * @param logger - telemetry logger from the runtime
575
+ * @param disableSchemaUpgrade - when true, the controller will never send or accept DocumentSchemaChange ops
572
576
  */
573
577
  constructor(
574
578
  existing: boolean,
@@ -578,6 +582,7 @@ export class DocumentsSchemaController {
578
582
  private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
579
583
  info: IDocumentSchemaInfo,
580
584
  logger: ITelemetryLoggerExt,
585
+ private readonly disableSchemaUpgrade: boolean,
581
586
  ) {
582
587
  // For simplicity, let's only support new schema features for explicit schema control mode
583
588
  assert(
@@ -704,9 +709,12 @@ export class DocumentsSchemaController {
704
709
  * Called by Container runtime whenever it is about to send some op.
705
710
  * 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.
706
711
  * Please consider note above constructor about race conditions - current design is to generate op only once in a session lifetime.
707
- * @returns Optional message to send.
712
+ * @returns Optional message to send. Always returns undefined when disableSchemaUpgrade is true.
708
713
  */
709
714
  public maybeGenerateSchemaMessage(): IDocumentSchemaChangeMessageOutgoing | undefined {
715
+ if (this.disableSchemaUpgrade) {
716
+ return undefined;
717
+ }
710
718
  if (this.futureSchema !== undefined && !this.opPending) {
711
719
  this.opPending = true;
712
720
  assert(
@@ -739,6 +747,7 @@ export class DocumentsSchemaController {
739
747
  /**
740
748
  * Process document schema change messages
741
749
  * Called by ContainerRuntime whenever it sees document schema messages.
750
+ * When disableSchemaUpgrade is true, an error is thrown if any incoming schema change ops are received.
742
751
  * @param contents - contents of the messages
743
752
  * @param local - whether op is local
744
753
  * @param sequenceNumber - sequence number of the op
@@ -749,6 +758,20 @@ export class DocumentsSchemaController {
749
758
  local: boolean,
750
759
  sequenceNumber: number,
751
760
  ): boolean {
761
+ if (this.disableSchemaUpgrade) {
762
+ assert(
763
+ !local,
764
+ 0xceb /* local schema change messages should never be generated when disableSchemaUpgrade is enabled */,
765
+ );
766
+ // Clients with disableSchemaUpgrade enabled should never generate schema change messages, but they
767
+ // may receive them from misconfigured clients. In such case, throw on any incoming schema change ops
768
+ // to prevent unexpected schema upgrades.
769
+ throw DataProcessingError.create(
770
+ "DocSchema: Received schema change op while disableSchemaUpgrade is enabled",
771
+ "processDocumentSchemaMessages",
772
+ undefined,
773
+ );
774
+ }
752
775
  for (const content of contents) {
753
776
  this.validateSeqNumber(content.refSeq, this.documentSchema.refSeq, "content.refSeq");
754
777
  this.validateSeqNumber(this.documentSchema.refSeq, sequenceNumber, "refSeq");
@@ -515,7 +515,7 @@ export class OrderedClientElection
515
515
  "InteractiveClientElected",
516
516
  client,
517
517
  sequenceNumber,
518
- true /* forceSend */,
518
+ false /* forceSend */,
519
519
  reason,
520
520
  );
521
521
  // Changing the elected parent as well.
@@ -544,7 +544,7 @@ export class OrderedClientElection
544
544
  "ParentElected",
545
545
  client,
546
546
  sequenceNumber,
547
- true /* forceSend */,
547
+ false /* forceSend */,
548
548
  reason,
549
549
  );
550
550
  this._electedParent = client;