@fluidframework/container-runtime 2.90.0 → 2.92.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 (135) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/api-report/container-runtime.legacy.beta.api.md +2 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/containerCompatibility.d.ts +1 -1
  5. package/dist/containerCompatibility.d.ts.map +1 -1
  6. package/dist/containerCompatibility.js.map +1 -1
  7. package/dist/containerRuntime.d.ts +37 -10
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +105 -77
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/gc/garbageCollection.d.ts +1 -0
  12. package/dist/gc/garbageCollection.d.ts.map +1 -1
  13. package/dist/gc/garbageCollection.js +3 -8
  14. package/dist/gc/garbageCollection.js.map +1 -1
  15. package/dist/gc/gcDefinitions.d.ts +4 -0
  16. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  17. package/dist/gc/gcDefinitions.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/legacy.d.ts +1 -1
  23. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  24. package/dist/opLifecycle/batchManager.js +2 -1
  25. package/dist/opLifecycle/batchManager.js.map +1 -1
  26. package/dist/opLifecycle/index.d.ts +1 -1
  27. package/dist/opLifecycle/index.d.ts.map +1 -1
  28. package/dist/opLifecycle/index.js +2 -1
  29. package/dist/opLifecycle/index.js.map +1 -1
  30. package/dist/opLifecycle/opGroupingManager.d.ts +6 -0
  31. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  32. package/dist/opLifecycle/opGroupingManager.js +11 -2
  33. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  34. package/dist/opLifecycle/opSerialization.d.ts +3 -1
  35. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  36. package/dist/opLifecycle/opSerialization.js +11 -9
  37. package/dist/opLifecycle/opSerialization.js.map +1 -1
  38. package/dist/opLifecycle/outbox.d.ts +0 -6
  39. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  40. package/dist/opLifecycle/outbox.js +2 -9
  41. package/dist/opLifecycle/outbox.js.map +1 -1
  42. package/dist/packageVersion.d.ts +1 -1
  43. package/dist/packageVersion.js +1 -1
  44. package/dist/packageVersion.js.map +1 -1
  45. package/dist/pendingStateManager.d.ts +7 -3
  46. package/dist/pendingStateManager.d.ts.map +1 -1
  47. package/dist/pendingStateManager.js +19 -7
  48. package/dist/pendingStateManager.js.map +1 -1
  49. package/dist/public.d.ts +1 -1
  50. package/dist/runtimeLayerCompatState.d.ts +1 -1
  51. package/dist/summary/documentSchema.d.ts +9 -3
  52. package/dist/summary/documentSchema.d.ts.map +1 -1
  53. package/dist/summary/documentSchema.js +19 -3
  54. package/dist/summary/documentSchema.js.map +1 -1
  55. package/dist/summary/orderedClientElection.js +2 -2
  56. package/dist/summary/orderedClientElection.js.map +1 -1
  57. package/dist/summary/summaryManager.d.ts +9 -0
  58. package/dist/summary/summaryManager.d.ts.map +1 -1
  59. package/dist/summary/summaryManager.js +29 -0
  60. package/dist/summary/summaryManager.js.map +1 -1
  61. package/internal.d.ts +1 -1
  62. package/legacy.d.ts +1 -1
  63. package/lib/containerCompatibility.d.ts +1 -1
  64. package/lib/containerCompatibility.d.ts.map +1 -1
  65. package/lib/containerCompatibility.js.map +1 -1
  66. package/lib/containerRuntime.d.ts +37 -10
  67. package/lib/containerRuntime.d.ts.map +1 -1
  68. package/lib/containerRuntime.js +106 -79
  69. package/lib/containerRuntime.js.map +1 -1
  70. package/lib/gc/garbageCollection.d.ts +1 -0
  71. package/lib/gc/garbageCollection.d.ts.map +1 -1
  72. package/lib/gc/garbageCollection.js +3 -8
  73. package/lib/gc/garbageCollection.js.map +1 -1
  74. package/lib/gc/gcDefinitions.d.ts +4 -0
  75. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  76. package/lib/gc/gcDefinitions.js.map +1 -1
  77. package/lib/index.d.ts +1 -1
  78. package/lib/index.d.ts.map +1 -1
  79. package/lib/index.js +1 -1
  80. package/lib/index.js.map +1 -1
  81. package/lib/legacy.d.ts +1 -1
  82. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  83. package/lib/opLifecycle/batchManager.js +2 -1
  84. package/lib/opLifecycle/batchManager.js.map +1 -1
  85. package/lib/opLifecycle/index.d.ts +1 -1
  86. package/lib/opLifecycle/index.d.ts.map +1 -1
  87. package/lib/opLifecycle/index.js +1 -1
  88. package/lib/opLifecycle/index.js.map +1 -1
  89. package/lib/opLifecycle/opGroupingManager.d.ts +6 -0
  90. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  91. package/lib/opLifecycle/opGroupingManager.js +10 -1
  92. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  93. package/lib/opLifecycle/opSerialization.d.ts +3 -1
  94. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  95. package/lib/opLifecycle/opSerialization.js +11 -9
  96. package/lib/opLifecycle/opSerialization.js.map +1 -1
  97. package/lib/opLifecycle/outbox.d.ts +0 -6
  98. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  99. package/lib/opLifecycle/outbox.js +2 -9
  100. package/lib/opLifecycle/outbox.js.map +1 -1
  101. package/lib/packageVersion.d.ts +1 -1
  102. package/lib/packageVersion.js +1 -1
  103. package/lib/packageVersion.js.map +1 -1
  104. package/lib/pendingStateManager.d.ts +7 -3
  105. package/lib/pendingStateManager.d.ts.map +1 -1
  106. package/lib/pendingStateManager.js +19 -7
  107. package/lib/pendingStateManager.js.map +1 -1
  108. package/lib/public.d.ts +1 -1
  109. package/lib/runtimeLayerCompatState.d.ts +1 -1
  110. package/lib/summary/documentSchema.d.ts +9 -3
  111. package/lib/summary/documentSchema.d.ts.map +1 -1
  112. package/lib/summary/documentSchema.js +19 -3
  113. package/lib/summary/documentSchema.js.map +1 -1
  114. package/lib/summary/orderedClientElection.js +2 -2
  115. package/lib/summary/orderedClientElection.js.map +1 -1
  116. package/lib/summary/summaryManager.d.ts +9 -0
  117. package/lib/summary/summaryManager.d.ts.map +1 -1
  118. package/lib/summary/summaryManager.js +29 -0
  119. package/lib/summary/summaryManager.js.map +1 -1
  120. package/package.json +28 -24
  121. package/src/containerCompatibility.ts +2 -0
  122. package/src/containerRuntime.ts +153 -93
  123. package/src/gc/garbageCollection.ts +4 -9
  124. package/src/gc/gcDefinitions.ts +4 -0
  125. package/src/index.ts +1 -0
  126. package/src/opLifecycle/batchManager.ts +2 -1
  127. package/src/opLifecycle/index.ts +1 -0
  128. package/src/opLifecycle/opGroupingManager.ts +11 -1
  129. package/src/opLifecycle/opSerialization.ts +14 -12
  130. package/src/opLifecycle/outbox.ts +2 -17
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +27 -11
  133. package/src/summary/documentSchema.ts +25 -2
  134. package/src/summary/orderedClientElection.ts +2 -2
  135. package/src/summary/summaryManager.ts +32 -0
@@ -10,7 +10,7 @@ import { SummaryType } from "@fluidframework/driver-definitions";
10
10
  import { FetchSource, MessageType } from "@fluidframework/driver-definitions/internal";
11
11
  import { readAndParse } from "@fluidframework/driver-utils/internal";
12
12
  import { createIdCompressor, createSessionId, deserializeIdCompressor, } from "@fluidframework/id-compressor/internal";
13
- import { FlushMode, FlushModeExperimental, channelsTreeName, gcTreeKey, } from "@fluidframework/runtime-definitions/internal";
13
+ import { FlushMode, channelsTreeName, gcTreeKey, } from "@fluidframework/runtime-definitions/internal";
14
14
  import { addBlobToSummary, addSummarizeResultToSummary, calculateStats, create404Response, defaultMinVersionForCollab, exceptionToResponse, GCDataBuilder, isValidMinVersionForCollab, RequestParser, RuntimeHeaders, validateMinimumVersionForCollab, seqFromTree, TelemetryContext, } from "@fluidframework/runtime-utils/internal";
15
15
  import { DataCorruptionError, DataProcessingError, extractSafePropertiesFromMessage, GenericError, LoggingError, PerformanceEvent,
16
16
  // eslint-disable-next-line import-x/no-deprecated
@@ -28,10 +28,10 @@ import { channelToDataStore } from "./dataStore.js";
28
28
  import { FluidDataStoreRegistry } from "./dataStoreRegistry.js";
29
29
  import { BaseDeltaManagerProxy, DeltaManagerPendingOpsProxy, DeltaManagerSummarizerProxy, } from "./deltaManagerProxies.js";
30
30
  import { DeltaScheduler } from "./deltaScheduler.js";
31
- import { GCNodeType, GarbageCollector, gcGenerationOptionName, } from "./gc/index.js";
31
+ import { GCNodeType, GarbageCollector, } from "./gc/index.js";
32
32
  import { InboundBatchAggregator } from "./inboundBatchAggregator.js";
33
33
  import { ContainerMessageType, } from "./messageTypes.js";
34
- import { DuplicateBatchDetector, ensureContentsDeserialized, OpCompressor, OpDecompressor, OpGroupingManager, OpSplitter, Outbox, RemoteMessageProcessor, } from "./opLifecycle/index.js";
34
+ import { DuplicateBatchDetector, ensureContentsDeserialized, largeBatchThreshold, OpCompressor, OpDecompressor, OpGroupingManager, OpSplitter, Outbox, RemoteMessageProcessor, } from "./opLifecycle/index.js";
35
35
  import { pkgVersion } from "./packageVersion.js";
36
36
  import { PendingStateManager, } from "./pendingStateManager.js";
37
37
  import { BatchRunCounter, RunCounter } from "./runCounter.js";
@@ -104,6 +104,15 @@ const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconn
104
104
  // - we do not stringify final op, thus we do not know how much escaping will be added.
105
105
  const defaultMaxBatchSizeInBytes = 700 * 1024;
106
106
  const defaultChunkSizeInBytes = 204800;
107
+ /**
108
+ * Default maximum ops per staging-mode batch before automatic flush scheduling resumes.
109
+ *
110
+ * Chosen based on production telemetry: copy-paste operations routinely produce batches
111
+ * of 1000+ ops (435K instances over 30 days), and receivers on modern Fluid versions
112
+ * handle them without issues. Uses {@link largeBatchThreshold} to stay aligned with
113
+ * the existing "large batch" telemetry threshold ({@link OpGroupingManager}).
114
+ */
115
+ const defaultStagingModeAutoFlushThreshold = largeBatchThreshold;
107
116
  /**
108
117
  * The default time to wait for pending ops to be processed during summarization
109
118
  */
@@ -200,6 +209,21 @@ export let getSingleUseLegacyLogCallback = (logger, type) => {
200
209
  export async function loadContainerRuntime(params) {
201
210
  return ContainerRuntime.loadRuntime(params);
202
211
  }
212
+ /**
213
+ * Alpha variant of {@link loadContainerRuntime} that returns the runtime in an
214
+ * extendable object, allowing additional properties to be added in the future.
215
+ *
216
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
217
+ * @returns An object containing the runtime.
218
+ *
219
+ * @legacy @alpha
220
+ */
221
+ export async function loadContainerRuntimeAlpha(params) {
222
+ return ContainerRuntime.loadRuntime2({
223
+ ...params,
224
+ registry: new FluidDataStoreRegistry(params.registryEntries),
225
+ });
226
+ }
203
227
  const defaultMaxConsecutiveReconnects = 7;
204
228
  /**
205
229
  * These are the ONLY message types that are allowed to be submitted while in staging mode
@@ -239,13 +263,14 @@ export class ContainerRuntime extends TypedEventEmitter {
239
263
  return ContainerRuntime.loadRuntime2({
240
264
  ...params,
241
265
  registry: new FluidDataStoreRegistry(params.registryEntries),
242
- });
266
+ }).then((r) => r.runtime);
243
267
  }
244
268
  /**
245
- * Load the stores from a snapshot and returns the runtime.
269
+ * Load the stores from a snapshot and returns an object containing the runtime.
246
270
  * @remarks
247
271
  * Same as {@link ContainerRuntime.loadRuntime},
248
272
  * but with `registry` instead of `registryEntries` and more `runtimeOptions`.
273
+ * Returns `{ runtime }` to allow future extensions (e.g. staging mode controls).
249
274
  */
250
275
  static async loadRuntime2(params) {
251
276
  const { context, registry, existing, requestHandler, provideEntryPoint, runtimeOptions = {}, containerScope = {}, containerRuntimeCtor = ContainerRuntime, minVersionForCollab = defaultMinVersionForCollab, } = params;
@@ -282,6 +307,8 @@ export class ContainerRuntime extends TypedEventEmitter {
282
307
  loadSequenceNumberVerification: "close",
283
308
  maxBatchSizeInBytes: defaultMaxBatchSizeInBytes,
284
309
  chunkSizeInBytes: defaultChunkSizeInBytes,
310
+ stagingModeAutoFlushThreshold: defaultStagingModeAutoFlushThreshold,
311
+ disableSchemaUpgrade: false,
285
312
  };
286
313
  const defaultConfigs = {
287
314
  ...defaultsAffectingDocSchema,
@@ -295,7 +322,7 @@ export class ContainerRuntime extends TypedEventEmitter {
295
322
  // is enabled via runtimeOptions, we will throw an error later.
296
323
  compressionOptions = enableGroupedBatching === false
297
324
  ? disabledCompressionConfig
298
- : defaultConfigs.compressionOptions, createBlobPayloadPending = defaultConfigs.createBlobPayloadPending, } = runtimeOptions;
325
+ : defaultConfigs.compressionOptions, createBlobPayloadPending = defaultConfigs.createBlobPayloadPending, stagingModeAutoFlushThreshold = defaultConfigs.stagingModeAutoFlushThreshold, disableSchemaUpgrade = defaultConfigs.disableSchemaUpgrade, } = runtimeOptions;
299
326
  // If explicitSchemaControl is off, ensure that options which require explicitSchemaControl are not enabled.
300
327
  if (!explicitSchemaControl) {
301
328
  const disallowedKeys = Object.keys(runtimeOptions).filter((key) => runtimeOptionKeysThatRequireExplicitSchemaControl.includes(key) && runtimeOptions[key] !== undefined);
@@ -402,6 +429,7 @@ export class ContainerRuntime extends TypedEventEmitter {
402
429
  else {
403
430
  idCompressorMode = desiredIdCompressorMode;
404
431
  }
432
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
405
433
  const createIdCompressorFn = () => {
406
434
  /**
407
435
  * Because the IdCompressor emits so much telemetry, this function is used to sample
@@ -438,7 +466,7 @@ export class ContainerRuntime extends TypedEventEmitter {
438
466
  disallowedVersions: [],
439
467
  }, (schema) => {
440
468
  runtime.onSchemaChange(schema);
441
- }, { minVersionForCollab }, logger);
469
+ }, { minVersionForCollab }, logger, disableSchemaUpgrade);
442
470
  // If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
443
471
  const existingMinVersionForCollab = documentSchemaController.sessionSchema.info.minVersionForCollab;
444
472
  const updatedMinVersionForCollab = existingMinVersionForCollab === undefined ||
@@ -462,6 +490,8 @@ export class ContainerRuntime extends TypedEventEmitter {
462
490
  enableGroupedBatching,
463
491
  explicitSchemaControl,
464
492
  createBlobPayloadPending,
493
+ stagingModeAutoFlushThreshold,
494
+ disableSchemaUpgrade,
465
495
  };
466
496
  validateMinimumVersionForCollab(updatedMinVersionForCollab);
467
497
  const runtime = new containerRuntimeCtor(context, registry, metadata, electedSummarizerData, chunks ?? [], aliases ?? [], internalRuntimeOptions, containerScope, logger, existing, blobManagerLoadInfo, context.storage, createIdCompressorFn, documentSchemaController, featureGatesForTelemetry, provideEntryPoint, updatedMinVersionForCollab, requestHandler, undefined, // summaryConfiguration
@@ -472,7 +502,7 @@ export class ContainerRuntime extends TypedEventEmitter {
472
502
  // Apply stashed ops with a reference sequence number equal to the sequence number of the snapshot,
473
503
  // or zero. This must be done before Container replays saved ops.
474
504
  await runtime.pendingStateManager.applyStashedOpsAt(runtimeSequenceNumber ?? 0);
475
- return runtime;
505
+ return { runtime };
476
506
  }
477
507
  get clientId() {
478
508
  return this._getClientId();
@@ -511,6 +541,7 @@ export class ContainerRuntime extends TypedEventEmitter {
511
541
  /**
512
542
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.idCompressor}
513
543
  */
544
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
514
545
  get idCompressor() {
515
546
  // Expose ID Compressor only if it's On from the start.
516
547
  // If container uses delayed mode, then we can only expose generateDocumentUniqueId() and nothing else.
@@ -589,7 +620,9 @@ export class ContainerRuntime extends TypedEventEmitter {
589
620
  /***/
590
621
  constructor(context, registry, metadata, electedSummarizerData, chunks, dataStoreAliasMap, runtimeOptions, containerScope,
591
622
  // Create a custom ITelemetryBaseLogger to output telemetry events.
592
- baseLogger, existing, blobManagerLoadInfo, _storage, createIdCompressorFn, documentsSchemaController, featureGatesForTelemetry, provideEntryPoint, minVersionForCollab, requestHandler,
623
+ baseLogger, existing, blobManagerLoadInfo, _storage,
624
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
625
+ createIdCompressorFn, documentsSchemaController, featureGatesForTelemetry, provideEntryPoint, minVersionForCollab, requestHandler,
593
626
  // // eslint-disable-next-line unicorn/no-object-as-default-parameter
594
627
  summaryConfiguration = {
595
628
  // the defaults
@@ -646,17 +679,28 @@ export class ContainerRuntime extends TypedEventEmitter {
646
679
  // Make sure Outbox is empty before entering staging mode,
647
680
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
648
681
  this.flush();
649
- const exitStagingMode = (discardOrCommit) => {
682
+ const exitStagingMode = (discardOrCommit, exitMethod) => {
650
683
  try {
651
- // Final flush of any last staged changes
652
- // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
653
- this.outbox.flush();
654
- this.stageControls = undefined;
655
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
656
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
657
- this.submitIdAllocationOpIfNeeded({ staged: false });
658
- discardOrCommit();
659
- this.channelCollection.notifyStagingMode(false);
684
+ PerformanceEvent.timedExec(this.mc.logger, {
685
+ eventName: `ExitStagingMode_${exitMethod}`,
686
+ }, (event) => {
687
+ // Final flush of any last staged changes
688
+ // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
689
+ this.outbox.flush();
690
+ this.stageControls = undefined;
691
+ // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
692
+ // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
693
+ this.submitIdAllocationOpIfNeeded({ staged: false });
694
+ const batchInfos = discardOrCommit();
695
+ event.reportProgress({
696
+ details: {
697
+ autoFlushThreshold: this.stagingModeAutoFlushThreshold,
698
+ batches: batchInfos.length,
699
+ batchesAtOrOverThreshold: batchInfos.filter((b) => b.length >= this.stagingModeAutoFlushThreshold).length,
700
+ },
701
+ });
702
+ this.channelCollection.notifyStagingMode(false);
703
+ });
660
704
  }
661
705
  catch (error) {
662
706
  const normalizedError = normalizeError(error);
@@ -667,21 +711,22 @@ export class ContainerRuntime extends TypedEventEmitter {
667
711
  const stageControls = {
668
712
  discardChanges: () => exitStagingMode(() => {
669
713
  // Pop all staged batches from the PSM and roll them back in LIFO order
670
- this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
714
+ const batchInfos = this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
671
715
  this.rollbackStagedChange(runtimeOp, localOpMetadata);
672
716
  });
673
717
  this.updateDocumentDirtyState();
674
- }),
718
+ return batchInfos;
719
+ }, "discard"),
675
720
  commitChanges: (options) => {
676
721
  const { squash } = { ...defaultStagingCommitOptions, ...options };
677
722
  exitStagingMode(() => {
678
723
  // Replay all staged batches in typical FIFO order.
679
724
  // We'll be out of staging mode so they'll be sent to the service finally.
680
- this.pendingStateManager.replayPendingStates({
725
+ return this.pendingStateManager.replayPendingStates({
681
726
  committingStagedBatches: true,
682
727
  squash,
683
728
  });
684
- });
729
+ }, "commit");
685
730
  },
686
731
  };
687
732
  this.stageControls = stageControls;
@@ -742,7 +787,7 @@ export class ContainerRuntime extends TypedEventEmitter {
742
787
  }
743
788
  return eventEmitter;
744
789
  });
745
- const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, signalAudience, pendingLocalState, supportedFeatures, snapshotWithContents, getConnectionState, } = context;
790
+ const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, signalAudience, pendingLocalState, snapshotWithContents, getConnectionState, } = context;
746
791
  this.getConnectionState = getConnectionState;
747
792
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
748
793
  this.disposeFn = disposeFn ?? closeFn;
@@ -864,14 +909,6 @@ export class ContainerRuntime extends TypedEventEmitter {
864
909
  ? this.getConnectionState() === ConnectionState.Connected ||
865
910
  this.getConnectionState() === ConnectionState.CatchingUp
866
911
  : undefined;
867
- this.mc.logger.sendTelemetryEvent({
868
- eventName: "GCFeatureMatrix",
869
- metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
870
- inputs: JSON.stringify({
871
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
872
- gcOptions_gcGeneration: runtimeOptions.gcOptions[gcGenerationOptionName],
873
- }),
874
- });
875
912
  this.telemetryDocumentId = metadata?.telemetryDocumentId ?? uuid();
876
913
  const opGroupingManager = new OpGroupingManager({
877
914
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -912,20 +949,17 @@ export class ContainerRuntime extends TypedEventEmitter {
912
949
  this.mc.config.getBoolean("Fluid.ContainerRuntime.Test.DisableSummaries") === true;
913
950
  this.maxConsecutiveReconnects =
914
951
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? defaultMaxConsecutiveReconnects;
915
- // If the context has ILayerCompatDetails, it supports referenceSequenceNumbers since that features
916
- // predates ILayerCompatDetails.
917
- const referenceSequenceNumbersSupported = maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails === undefined
918
- ? supportedFeatures?.get("referenceSequenceNumbers") === true
919
- : true;
920
- if (runtimeOptions.flushMode === FlushModeExperimental.Async &&
921
- !referenceSequenceNumbersSupported) {
922
- // The loader does not support reference sequence numbers, falling back on FlushMode.TurnBased
923
- this.mc.logger.sendErrorEvent({ eventName: "FlushModeFallback" });
924
- this._flushMode = FlushMode.TurnBased;
925
- }
926
- else {
927
- this._flushMode = runtimeOptions.flushMode;
952
+ this._flushMode = runtimeOptions.flushMode;
953
+ // TODO: Added in 2.90.0 - Remove this validation once we've released and confirmed no consumer passes an invalid flushMode value.
954
+ if (this._flushMode !== FlushMode.Immediate && this._flushMode !== FlushMode.TurnBased) {
955
+ const error = new UsageError("Invalid flushMode runtime option. Expected FlushMode.Immediate or FlushMode.TurnBased.");
956
+ this.closeFn(error);
957
+ throw error;
928
958
  }
959
+ this.stagingModeAutoFlushThreshold =
960
+ this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
961
+ runtimeOptions.stagingModeAutoFlushThreshold ??
962
+ defaultStagingModeAutoFlushThreshold;
929
963
  this.batchIdTrackingEnabled =
930
964
  this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
931
965
  this.mc.config.getBoolean("Fluid.ContainerRuntime.enableBatchIdTracking") ??
@@ -1035,9 +1069,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1035
1069
  this.deltaScheduler = new DeltaScheduler(this.innerDeltaManager, this, createChildLogger({ logger: this.baseLogger, namespace: "DeltaScheduler" }));
1036
1070
  this.inboundBatchAggregator = new InboundBatchAggregator(this.innerDeltaManager, () => this.clientId, createChildLogger({ logger: this.baseLogger, namespace: "InboundBatchAggregator" }));
1037
1071
  const legacySendBatchFn = makeLegacySendBatchFn(submitFn, this.innerDeltaManager);
1038
- this.skipSafetyFlushDuringProcessStack =
1039
- // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
1040
- this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
1041
1072
  this.outbox = new Outbox({
1042
1073
  shouldSend: () => this.shouldSendOps(),
1043
1074
  pendingStateManager: this.pendingStateManager,
@@ -1048,8 +1079,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1048
1079
  config: {
1049
1080
  compressionOptions,
1050
1081
  maxBatchSizeInBytes: runtimeOptions.maxBatchSizeInBytes,
1051
- // If we disable flush before process, we must be ready to flush partial batches
1052
- flushPartialBatches: this.skipSafetyFlushDuringProcessStack,
1053
1082
  },
1054
1083
  logger: this.mc.logger,
1055
1084
  groupingManager: opGroupingManager,
@@ -1093,14 +1122,12 @@ export class ContainerRuntime extends TypedEventEmitter {
1093
1122
  // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1094
1123
  this.lastEmittedDirty = this.computeCurrentDirtyState();
1095
1124
  context.updateDirtyContainerState(this.lastEmittedDirty);
1096
- if (!this.skipSafetyFlushDuringProcessStack) {
1097
- // Reference Sequence Number may have just changed, and it must be consistent across a batch,
1098
- // so we should flush now to clear the way for the next ops.
1099
- // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
1100
- // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
1101
- // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
1102
- this.deltaManager.on("op", () => this.flush());
1103
- }
1125
+ // Reference Sequence Number may have just changed, and it must be consistent across a batch,
1126
+ // so we should flush now to clear the way for the next ops.
1127
+ // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
1128
+ // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
1129
+ // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
1130
+ this.deltaManager.on("op", () => this.flush());
1104
1131
  // logging hardware telemetry
1105
1132
  this.baseLogger.send({
1106
1133
  category: "generic",
@@ -1114,7 +1141,9 @@ export class ContainerRuntime extends TypedEventEmitter {
1114
1141
  summaryNumber: loadSummaryNumber,
1115
1142
  summaryFormatVersion: metadata?.summaryFormatVersion,
1116
1143
  disableIsolatedChannels: metadata?.disableIsolatedChannels,
1144
+ // This is useful even for interactive clients since they track unreferenced nodes and log errors.
1117
1145
  gcVersion: metadata?.gcFeature,
1146
+ gcConfigs: this.garbageCollector.serializedConfigs,
1118
1147
  options: JSON.stringify(runtimeOptions),
1119
1148
  idCompressorModeMetadata: metadata?.documentSchema?.runtime?.idCompressorMode,
1120
1149
  idCompressorMode: this.sessionSchema.idCompressorMode,
@@ -1122,7 +1151,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1122
1151
  featureGates: JSON.stringify({
1123
1152
  ...featureGatesForTelemetry,
1124
1153
  closeSummarizerDelayOverride,
1125
- disableFlushBeforeProcess: this.skipSafetyFlushDuringProcessStack,
1126
1154
  }),
1127
1155
  telemetryDocumentId: this.telemetryDocumentId,
1128
1156
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -1579,12 +1607,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1579
1607
  assert(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
1580
1608
  this.emitDirtyDocumentEvent = false;
1581
1609
  try {
1582
- // Any ID Allocation ops that failed to submit after the pending state was queued need to have
1583
- // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
1584
- // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
1585
- // before staging mode so we can simply say staged: false.
1586
- this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
1587
- this.scheduleFlush();
1610
+ // Any ID Allocation ops that failed to submit need to have their ranges included
1611
+ // in the next allocation op. Reset the compressor's unfinalized range cursor so that the next
1612
+ // call to takeNextCreationRange (during replay) will include those unfinalized ranges.
1613
+ this._idCompressor?.resetUnfinalizedCreationRange();
1588
1614
  // replay the ops
1589
1615
  this.pendingStateManager.replayPendingStates();
1590
1616
  }
@@ -1805,10 +1831,8 @@ export class ContainerRuntime extends TypedEventEmitter {
1805
1831
  // spread operator above ensure we make a shallow copy of message, as the processing flow will modify it.
1806
1832
  // There might be multiple container instances receiving the same message.
1807
1833
  this.verifyNotClosed();
1808
- if (!this.skipSafetyFlushDuringProcessStack) {
1809
- // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
1810
- this.flush();
1811
- }
1834
+ // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
1835
+ this.flush();
1812
1836
  this.ensureNoDataModelChanges(() => {
1813
1837
  this.processInboundMessageOrBatch(messageCopy, local);
1814
1838
  });
@@ -2299,7 +2323,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2299
2323
  return this.lastEmittedDirty;
2300
2324
  }
2301
2325
  /**
2302
- * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
2326
+ * Returns true if the container is dirty: not attached, or has pending user messages (ignores "non-dirtyable" ones though)
2303
2327
  */
2304
2328
  computeCurrentDirtyState() {
2305
2329
  return (this.attachState !== AttachState.Attached ||
@@ -3039,6 +3063,17 @@ export class ContainerRuntime extends TypedEventEmitter {
3039
3063
  this.updateDocumentDirtyState();
3040
3064
  }
3041
3065
  scheduleFlush() {
3066
+ // During staging mode, suppress automatic flush scheduling until the main batch
3067
+ // reaches or exceeds the threshold.
3068
+ // Incoming ops still break the batch via direct this.flush() calls elsewhere
3069
+ // (deltaManager "op" handler, process(), connection changes, getPendingLocalState,
3070
+ // exitStagingMode). Those all bypass scheduleFlush(), so they're unaffected by this check.
3071
+ // Additionally, outbox.maybeFlushPartialBatch() (called on every submit) detects
3072
+ // sequence number changes and throws if unexpected changes are detected.
3073
+ if (this.inStagingMode &&
3074
+ this.outbox.mainBatchMessageCount < this.stagingModeAutoFlushThreshold) {
3075
+ return;
3076
+ }
3042
3077
  if (this.flushScheduled) {
3043
3078
  return;
3044
3079
  }
@@ -3058,14 +3093,6 @@ export class ContainerRuntime extends TypedEventEmitter {
3058
3093
  Promise.resolve().then(() => this.flush());
3059
3094
  break;
3060
3095
  }
3061
- // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
3062
- case FlushModeExperimental.Async: {
3063
- // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
3064
- // batch when all micro-tasks are complete.
3065
- // Compared to TurnBased, this flush mode will capture more ops into the same batch.
3066
- setTimeout(() => this.flush(), 0);
3067
- break;
3068
- }
3069
3096
  default: {
3070
3097
  fail(0x587 /* Unreachable unless manually accumulating a batch */);
3071
3098
  }
@@ -3341,7 +3368,7 @@ export class ContainerRuntime extends TypedEventEmitter {
3341
3368
  return PerformanceEvent.timedExec(this.mc.logger, {
3342
3369
  eventName: "getPendingLocalState",
3343
3370
  }, (event) => {
3344
- const pending = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
3371
+ const { pending } = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
3345
3372
  const sessionExpiryTimerStarted = props?.sessionExpiryTimerStarted ?? this.garbageCollector.sessionExpiryTimerStarted;
3346
3373
  const pendingIdCompressorState = this._idCompressor?.serialize(true);
3347
3374
  const pendingAttachmentBlobs = this.blobManager.getPendingBlobs();