@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
@@ -4,7 +4,7 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.isContainerMessageDirtyable = exports.ContainerRuntime = exports.loadContainerRuntime = exports.getSingleUseLegacyLogCallback = exports.makeLegacySendBatchFn = exports.getDeviceSpec = exports.agentSchedulerId = exports.isUnpackedRuntimeMessage = exports.defaultPendingOpsRetryDelayMs = exports.defaultPendingOpsWaitTimeoutMs = exports.defaultRuntimeHeaderData = exports.InactiveResponseHeaderKey = exports.TombstoneResponseHeaderKey = exports.DeletedResponseHeaderKey = void 0;
7
+ exports.isContainerMessageDirtyable = exports.ContainerRuntime = exports.loadContainerRuntimeAlpha = exports.loadContainerRuntime = exports.getSingleUseLegacyLogCallback = exports.makeLegacySendBatchFn = exports.getDeviceSpec = exports.agentSchedulerId = exports.isUnpackedRuntimeMessage = exports.defaultPendingOpsRetryDelayMs = exports.defaultPendingOpsWaitTimeoutMs = exports.defaultRuntimeHeaderData = exports.InactiveResponseHeaderKey = exports.TombstoneResponseHeaderKey = exports.DeletedResponseHeaderKey = void 0;
8
8
  const client_utils_1 = require("@fluid-internal/client-utils");
9
9
  const container_definitions_1 = require("@fluidframework/container-definitions");
10
10
  const internal_1 = require("@fluidframework/container-definitions/internal");
@@ -105,6 +105,15 @@ const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconn
105
105
  // - we do not stringify final op, thus we do not know how much escaping will be added.
106
106
  const defaultMaxBatchSizeInBytes = 700 * 1024;
107
107
  const defaultChunkSizeInBytes = 204800;
108
+ /**
109
+ * Default maximum ops per staging-mode batch before automatic flush scheduling resumes.
110
+ *
111
+ * Chosen based on production telemetry: copy-paste operations routinely produce batches
112
+ * of 1000+ ops (435K instances over 30 days), and receivers on modern Fluid versions
113
+ * handle them without issues. Uses {@link largeBatchThreshold} to stay aligned with
114
+ * the existing "large batch" telemetry threshold ({@link OpGroupingManager}).
115
+ */
116
+ const defaultStagingModeAutoFlushThreshold = index_js_3.largeBatchThreshold;
108
117
  /**
109
118
  * The default time to wait for pending ops to be processed during summarization
110
119
  */
@@ -206,6 +215,22 @@ async function loadContainerRuntime(params) {
206
215
  return ContainerRuntime.loadRuntime(params);
207
216
  }
208
217
  exports.loadContainerRuntime = loadContainerRuntime;
218
+ /**
219
+ * Alpha variant of {@link loadContainerRuntime} that returns the runtime in an
220
+ * extendable object, allowing additional properties to be added in the future.
221
+ *
222
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
223
+ * @returns An object containing the runtime.
224
+ *
225
+ * @legacy @alpha
226
+ */
227
+ async function loadContainerRuntimeAlpha(params) {
228
+ return ContainerRuntime.loadRuntime2({
229
+ ...params,
230
+ registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry(params.registryEntries),
231
+ });
232
+ }
233
+ exports.loadContainerRuntimeAlpha = loadContainerRuntimeAlpha;
209
234
  const defaultMaxConsecutiveReconnects = 7;
210
235
  /**
211
236
  * These are the ONLY message types that are allowed to be submitted while in staging mode
@@ -245,13 +270,14 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
245
270
  return ContainerRuntime.loadRuntime2({
246
271
  ...params,
247
272
  registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry(params.registryEntries),
248
- });
273
+ }).then((r) => r.runtime);
249
274
  }
250
275
  /**
251
- * Load the stores from a snapshot and returns the runtime.
276
+ * Load the stores from a snapshot and returns an object containing the runtime.
252
277
  * @remarks
253
278
  * Same as {@link ContainerRuntime.loadRuntime},
254
279
  * but with `registry` instead of `registryEntries` and more `runtimeOptions`.
280
+ * Returns `{ runtime }` to allow future extensions (e.g. staging mode controls).
255
281
  */
256
282
  static async loadRuntime2(params) {
257
283
  const { context, registry, existing, requestHandler, provideEntryPoint, runtimeOptions = {}, containerScope = {}, containerRuntimeCtor = ContainerRuntime, minVersionForCollab = internal_7.defaultMinVersionForCollab, } = params;
@@ -288,6 +314,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
288
314
  loadSequenceNumberVerification: "close",
289
315
  maxBatchSizeInBytes: defaultMaxBatchSizeInBytes,
290
316
  chunkSizeInBytes: defaultChunkSizeInBytes,
317
+ stagingModeAutoFlushThreshold: defaultStagingModeAutoFlushThreshold,
318
+ disableSchemaUpgrade: false,
291
319
  };
292
320
  const defaultConfigs = {
293
321
  ...defaultsAffectingDocSchema,
@@ -301,7 +329,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
301
329
  // is enabled via runtimeOptions, we will throw an error later.
302
330
  compressionOptions = enableGroupedBatching === false
303
331
  ? compressionDefinitions_js_1.disabledCompressionConfig
304
- : defaultConfigs.compressionOptions, createBlobPayloadPending = defaultConfigs.createBlobPayloadPending, } = runtimeOptions;
332
+ : defaultConfigs.compressionOptions, createBlobPayloadPending = defaultConfigs.createBlobPayloadPending, stagingModeAutoFlushThreshold = defaultConfigs.stagingModeAutoFlushThreshold, disableSchemaUpgrade = defaultConfigs.disableSchemaUpgrade, } = runtimeOptions;
305
333
  // If explicitSchemaControl is off, ensure that options which require explicitSchemaControl are not enabled.
306
334
  if (!explicitSchemaControl) {
307
335
  const disallowedKeys = Object.keys(runtimeOptions).filter((key) => containerCompatibility_js_1.runtimeOptionKeysThatRequireExplicitSchemaControl.includes(key) && runtimeOptions[key] !== undefined);
@@ -408,6 +436,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
408
436
  else {
409
437
  idCompressorMode = desiredIdCompressorMode;
410
438
  }
439
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
411
440
  const createIdCompressorFn = () => {
412
441
  /**
413
442
  * Because the IdCompressor emits so much telemetry, this function is used to sample
@@ -444,7 +473,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
444
473
  disallowedVersions: [],
445
474
  }, (schema) => {
446
475
  runtime.onSchemaChange(schema);
447
- }, { minVersionForCollab }, logger);
476
+ }, { minVersionForCollab }, logger, disableSchemaUpgrade);
448
477
  // If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
449
478
  const existingMinVersionForCollab = documentSchemaController.sessionSchema.info.minVersionForCollab;
450
479
  const updatedMinVersionForCollab = existingMinVersionForCollab === undefined ||
@@ -468,6 +497,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
468
497
  enableGroupedBatching,
469
498
  explicitSchemaControl,
470
499
  createBlobPayloadPending,
500
+ stagingModeAutoFlushThreshold,
501
+ disableSchemaUpgrade,
471
502
  };
472
503
  (0, internal_7.validateMinimumVersionForCollab)(updatedMinVersionForCollab);
473
504
  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
@@ -478,7 +509,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
478
509
  // Apply stashed ops with a reference sequence number equal to the sequence number of the snapshot,
479
510
  // or zero. This must be done before Container replays saved ops.
480
511
  await runtime.pendingStateManager.applyStashedOpsAt(runtimeSequenceNumber ?? 0);
481
- return runtime;
512
+ return { runtime };
482
513
  }
483
514
  get clientId() {
484
515
  return this._getClientId();
@@ -517,6 +548,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
517
548
  /**
518
549
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.idCompressor}
519
550
  */
551
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
520
552
  get idCompressor() {
521
553
  // Expose ID Compressor only if it's On from the start.
522
554
  // If container uses delayed mode, then we can only expose generateDocumentUniqueId() and nothing else.
@@ -595,7 +627,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
595
627
  /***/
596
628
  constructor(context, registry, metadata, electedSummarizerData, chunks, dataStoreAliasMap, runtimeOptions, containerScope,
597
629
  // Create a custom ITelemetryBaseLogger to output telemetry events.
598
- baseLogger, existing, blobManagerLoadInfo, _storage, createIdCompressorFn, documentsSchemaController, featureGatesForTelemetry, provideEntryPoint, minVersionForCollab, requestHandler,
630
+ baseLogger, existing, blobManagerLoadInfo, _storage,
631
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
632
+ createIdCompressorFn, documentsSchemaController, featureGatesForTelemetry, provideEntryPoint, minVersionForCollab, requestHandler,
599
633
  // // eslint-disable-next-line unicorn/no-object-as-default-parameter
600
634
  summaryConfiguration = {
601
635
  // the defaults
@@ -652,17 +686,28 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
652
686
  // Make sure Outbox is empty before entering staging mode,
653
687
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
654
688
  this.flush();
655
- const exitStagingMode = (discardOrCommit) => {
689
+ const exitStagingMode = (discardOrCommit, exitMethod) => {
656
690
  try {
657
- // Final flush of any last staged changes
658
- // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
659
- this.outbox.flush();
660
- this.stageControls = undefined;
661
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
662
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
663
- this.submitIdAllocationOpIfNeeded({ staged: false });
664
- discardOrCommit();
665
- this.channelCollection.notifyStagingMode(false);
691
+ internal_8.PerformanceEvent.timedExec(this.mc.logger, {
692
+ eventName: `ExitStagingMode_${exitMethod}`,
693
+ }, (event) => {
694
+ // Final flush of any last staged changes
695
+ // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
696
+ this.outbox.flush();
697
+ this.stageControls = undefined;
698
+ // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
699
+ // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
700
+ this.submitIdAllocationOpIfNeeded({ staged: false });
701
+ const batchInfos = discardOrCommit();
702
+ event.reportProgress({
703
+ details: {
704
+ autoFlushThreshold: this.stagingModeAutoFlushThreshold,
705
+ batches: batchInfos.length,
706
+ batchesAtOrOverThreshold: batchInfos.filter((b) => b.length >= this.stagingModeAutoFlushThreshold).length,
707
+ },
708
+ });
709
+ this.channelCollection.notifyStagingMode(false);
710
+ });
666
711
  }
667
712
  catch (error) {
668
713
  const normalizedError = (0, internal_8.normalizeError)(error);
@@ -673,21 +718,22 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
673
718
  const stageControls = {
674
719
  discardChanges: () => exitStagingMode(() => {
675
720
  // Pop all staged batches from the PSM and roll them back in LIFO order
676
- this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
721
+ const batchInfos = this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
677
722
  this.rollbackStagedChange(runtimeOp, localOpMetadata);
678
723
  });
679
724
  this.updateDocumentDirtyState();
680
- }),
725
+ return batchInfos;
726
+ }, "discard"),
681
727
  commitChanges: (options) => {
682
728
  const { squash } = { ...defaultStagingCommitOptions, ...options };
683
729
  exitStagingMode(() => {
684
730
  // Replay all staged batches in typical FIFO order.
685
731
  // We'll be out of staging mode so they'll be sent to the service finally.
686
- this.pendingStateManager.replayPendingStates({
732
+ return this.pendingStateManager.replayPendingStates({
687
733
  committingStagedBatches: true,
688
734
  squash,
689
735
  });
690
- });
736
+ }, "commit");
691
737
  },
692
738
  };
693
739
  this.stageControls = stageControls;
@@ -748,7 +794,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
748
794
  }
749
795
  return eventEmitter;
750
796
  });
751
- const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, signalAudience, pendingLocalState, supportedFeatures, snapshotWithContents, getConnectionState, } = context;
797
+ const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, signalAudience, pendingLocalState, snapshotWithContents, getConnectionState, } = context;
752
798
  this.getConnectionState = getConnectionState;
753
799
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
754
800
  this.disposeFn = disposeFn ?? closeFn;
@@ -870,14 +916,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
870
916
  ? this.getConnectionState() === internal_1.ConnectionState.Connected ||
871
917
  this.getConnectionState() === internal_1.ConnectionState.CatchingUp
872
918
  : undefined;
873
- this.mc.logger.sendTelemetryEvent({
874
- eventName: "GCFeatureMatrix",
875
- metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
876
- inputs: JSON.stringify({
877
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
878
- gcOptions_gcGeneration: runtimeOptions.gcOptions[index_js_2.gcGenerationOptionName],
879
- }),
880
- });
881
919
  this.telemetryDocumentId = metadata?.telemetryDocumentId ?? (0, uuid_1.v4)();
882
920
  const opGroupingManager = new index_js_3.OpGroupingManager({
883
921
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -918,20 +956,17 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
918
956
  this.mc.config.getBoolean("Fluid.ContainerRuntime.Test.DisableSummaries") === true;
919
957
  this.maxConsecutiveReconnects =
920
958
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? defaultMaxConsecutiveReconnects;
921
- // If the context has ILayerCompatDetails, it supports referenceSequenceNumbers since that features
922
- // predates ILayerCompatDetails.
923
- const referenceSequenceNumbersSupported = maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails === undefined
924
- ? supportedFeatures?.get("referenceSequenceNumbers") === true
925
- : true;
926
- if (runtimeOptions.flushMode === internal_6.FlushModeExperimental.Async &&
927
- !referenceSequenceNumbersSupported) {
928
- // The loader does not support reference sequence numbers, falling back on FlushMode.TurnBased
929
- this.mc.logger.sendErrorEvent({ eventName: "FlushModeFallback" });
930
- this._flushMode = internal_6.FlushMode.TurnBased;
931
- }
932
- else {
933
- this._flushMode = runtimeOptions.flushMode;
959
+ this._flushMode = runtimeOptions.flushMode;
960
+ // TODO: Added in 2.90.0 - Remove this validation once we've released and confirmed no consumer passes an invalid flushMode value.
961
+ if (this._flushMode !== internal_6.FlushMode.Immediate && this._flushMode !== internal_6.FlushMode.TurnBased) {
962
+ const error = new internal_8.UsageError("Invalid flushMode runtime option. Expected FlushMode.Immediate or FlushMode.TurnBased.");
963
+ this.closeFn(error);
964
+ throw error;
934
965
  }
966
+ this.stagingModeAutoFlushThreshold =
967
+ this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
968
+ runtimeOptions.stagingModeAutoFlushThreshold ??
969
+ defaultStagingModeAutoFlushThreshold;
935
970
  this.batchIdTrackingEnabled =
936
971
  this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
937
972
  this.mc.config.getBoolean("Fluid.ContainerRuntime.enableBatchIdTracking") ??
@@ -1041,9 +1076,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1041
1076
  this.deltaScheduler = new deltaScheduler_js_1.DeltaScheduler(this.innerDeltaManager, this, (0, internal_8.createChildLogger)({ logger: this.baseLogger, namespace: "DeltaScheduler" }));
1042
1077
  this.inboundBatchAggregator = new inboundBatchAggregator_js_1.InboundBatchAggregator(this.innerDeltaManager, () => this.clientId, (0, internal_8.createChildLogger)({ logger: this.baseLogger, namespace: "InboundBatchAggregator" }));
1043
1078
  const legacySendBatchFn = (0, exports.makeLegacySendBatchFn)(submitFn, this.innerDeltaManager);
1044
- this.skipSafetyFlushDuringProcessStack =
1045
- // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
1046
- this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
1047
1079
  this.outbox = new index_js_3.Outbox({
1048
1080
  shouldSend: () => this.shouldSendOps(),
1049
1081
  pendingStateManager: this.pendingStateManager,
@@ -1054,8 +1086,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1054
1086
  config: {
1055
1087
  compressionOptions,
1056
1088
  maxBatchSizeInBytes: runtimeOptions.maxBatchSizeInBytes,
1057
- // If we disable flush before process, we must be ready to flush partial batches
1058
- flushPartialBatches: this.skipSafetyFlushDuringProcessStack,
1059
1089
  },
1060
1090
  logger: this.mc.logger,
1061
1091
  groupingManager: opGroupingManager,
@@ -1099,14 +1129,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1099
1129
  // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1100
1130
  this.lastEmittedDirty = this.computeCurrentDirtyState();
1101
1131
  context.updateDirtyContainerState(this.lastEmittedDirty);
1102
- if (!this.skipSafetyFlushDuringProcessStack) {
1103
- // Reference Sequence Number may have just changed, and it must be consistent across a batch,
1104
- // so we should flush now to clear the way for the next ops.
1105
- // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
1106
- // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
1107
- // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
1108
- this.deltaManager.on("op", () => this.flush());
1109
- }
1132
+ // Reference Sequence Number may have just changed, and it must be consistent across a batch,
1133
+ // so we should flush now to clear the way for the next ops.
1134
+ // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
1135
+ // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
1136
+ // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
1137
+ this.deltaManager.on("op", () => this.flush());
1110
1138
  // logging hardware telemetry
1111
1139
  this.baseLogger.send({
1112
1140
  category: "generic",
@@ -1120,7 +1148,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1120
1148
  summaryNumber: loadSummaryNumber,
1121
1149
  summaryFormatVersion: metadata?.summaryFormatVersion,
1122
1150
  disableIsolatedChannels: metadata?.disableIsolatedChannels,
1151
+ // This is useful even for interactive clients since they track unreferenced nodes and log errors.
1123
1152
  gcVersion: metadata?.gcFeature,
1153
+ gcConfigs: this.garbageCollector.serializedConfigs,
1124
1154
  options: JSON.stringify(runtimeOptions),
1125
1155
  idCompressorModeMetadata: metadata?.documentSchema?.runtime?.idCompressorMode,
1126
1156
  idCompressorMode: this.sessionSchema.idCompressorMode,
@@ -1128,7 +1158,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1128
1158
  featureGates: JSON.stringify({
1129
1159
  ...featureGatesForTelemetry,
1130
1160
  closeSummarizerDelayOverride,
1131
- disableFlushBeforeProcess: this.skipSafetyFlushDuringProcessStack,
1132
1161
  }),
1133
1162
  telemetryDocumentId: this.telemetryDocumentId,
1134
1163
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -1585,12 +1614,10 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1585
1614
  (0, internal_2.assert)(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
1586
1615
  this.emitDirtyDocumentEvent = false;
1587
1616
  try {
1588
- // Any ID Allocation ops that failed to submit after the pending state was queued need to have
1589
- // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
1590
- // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
1591
- // before staging mode so we can simply say staged: false.
1592
- this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
1593
- this.scheduleFlush();
1617
+ // Any ID Allocation ops that failed to submit need to have their ranges included
1618
+ // in the next allocation op. Reset the compressor's unfinalized range cursor so that the next
1619
+ // call to takeNextCreationRange (during replay) will include those unfinalized ranges.
1620
+ this._idCompressor?.resetUnfinalizedCreationRange();
1594
1621
  // replay the ops
1595
1622
  this.pendingStateManager.replayPendingStates();
1596
1623
  }
@@ -1811,10 +1838,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1811
1838
  // spread operator above ensure we make a shallow copy of message, as the processing flow will modify it.
1812
1839
  // There might be multiple container instances receiving the same message.
1813
1840
  this.verifyNotClosed();
1814
- if (!this.skipSafetyFlushDuringProcessStack) {
1815
- // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
1816
- this.flush();
1817
- }
1841
+ // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
1842
+ this.flush();
1818
1843
  this.ensureNoDataModelChanges(() => {
1819
1844
  this.processInboundMessageOrBatch(messageCopy, local);
1820
1845
  });
@@ -2305,7 +2330,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2305
2330
  return this.lastEmittedDirty;
2306
2331
  }
2307
2332
  /**
2308
- * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
2333
+ * Returns true if the container is dirty: not attached, or has pending user messages (ignores "non-dirtyable" ones though)
2309
2334
  */
2310
2335
  computeCurrentDirtyState() {
2311
2336
  return (this.attachState !== container_definitions_1.AttachState.Attached ||
@@ -3045,6 +3070,17 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3045
3070
  this.updateDocumentDirtyState();
3046
3071
  }
3047
3072
  scheduleFlush() {
3073
+ // During staging mode, suppress automatic flush scheduling until the main batch
3074
+ // reaches or exceeds the threshold.
3075
+ // Incoming ops still break the batch via direct this.flush() calls elsewhere
3076
+ // (deltaManager "op" handler, process(), connection changes, getPendingLocalState,
3077
+ // exitStagingMode). Those all bypass scheduleFlush(), so they're unaffected by this check.
3078
+ // Additionally, outbox.maybeFlushPartialBatch() (called on every submit) detects
3079
+ // sequence number changes and throws if unexpected changes are detected.
3080
+ if (this.inStagingMode &&
3081
+ this.outbox.mainBatchMessageCount < this.stagingModeAutoFlushThreshold) {
3082
+ return;
3083
+ }
3048
3084
  if (this.flushScheduled) {
3049
3085
  return;
3050
3086
  }
@@ -3064,14 +3100,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3064
3100
  Promise.resolve().then(() => this.flush());
3065
3101
  break;
3066
3102
  }
3067
- // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
3068
- case internal_6.FlushModeExperimental.Async: {
3069
- // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
3070
- // batch when all micro-tasks are complete.
3071
- // Compared to TurnBased, this flush mode will capture more ops into the same batch.
3072
- setTimeout(() => this.flush(), 0);
3073
- break;
3074
- }
3075
3103
  default: {
3076
3104
  (0, internal_2.fail)(0x587 /* Unreachable unless manually accumulating a batch */);
3077
3105
  }
@@ -3347,7 +3375,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3347
3375
  return internal_8.PerformanceEvent.timedExec(this.mc.logger, {
3348
3376
  eventName: "getPendingLocalState",
3349
3377
  }, (event) => {
3350
- const pending = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
3378
+ const { pending } = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
3351
3379
  const sessionExpiryTimerStarted = props?.sessionExpiryTimerStarted ?? this.garbageCollector.sessionExpiryTimerStarted;
3352
3380
  const pendingIdCompressorState = this._idCompressor?.serialize(true);
3353
3381
  const pendingAttachmentBlobs = this.blobManager.getPendingBlobs();