@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
@@ -95,6 +95,7 @@ import { FetchSource, MessageType } from "@fluidframework/driver-definitions/int
95
95
  import { readAndParse } from "@fluidframework/driver-utils/internal";
96
96
  import type { IIdCompressor } from "@fluidframework/id-compressor";
97
97
  import type {
98
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
98
99
  IIdCompressorCore,
99
100
  IdCreationRange,
100
101
  SerializedIdCompressorWithNoSession,
@@ -107,7 +108,6 @@ import {
107
108
  } from "@fluidframework/id-compressor/internal";
108
109
  import {
109
110
  FlushMode,
110
- FlushModeExperimental,
111
111
  channelsTreeName,
112
112
  gcTreeKey,
113
113
  } from "@fluidframework/runtime-definitions/internal";
@@ -131,6 +131,7 @@ import type {
131
131
  IContainerRuntimeBaseInternal,
132
132
  MinimumVersionForCollab,
133
133
  ContainerExtensionExpectations,
134
+ ContainerRuntimeBaseAlpha,
134
135
  } from "@fluidframework/runtime-definitions/internal";
135
136
  import {
136
137
  addBlobToSummary,
@@ -220,7 +221,6 @@ import {
220
221
  type IGCRuntimeOptions,
221
222
  type IGCStats,
222
223
  type IGarbageCollector,
223
- gcGenerationOptionName,
224
224
  type GarbageCollectionMessage,
225
225
  type IGarbageCollectionRuntime,
226
226
  } from "./gc/index.js";
@@ -244,6 +244,7 @@ import {
244
244
  DuplicateBatchDetector,
245
245
  ensureContentsDeserialized,
246
246
  type IBatchCheckpoint,
247
+ largeBatchThreshold,
247
248
  OpCompressor,
248
249
  OpDecompressor,
249
250
  OpGroupingManager,
@@ -259,6 +260,7 @@ import {
259
260
  type IPendingLocalState,
260
261
  PendingStateManager,
261
262
  type PendingBatchResubmitMetadata,
263
+ type IPendingMessage,
262
264
  } from "./pendingStateManager.js";
263
265
  import { BatchRunCounter, RunCounter } from "./runCounter.js";
264
266
  import {
@@ -476,6 +478,25 @@ export interface ContainerRuntimeOptions {
476
478
  * When enabled (`true`), createBlob will return a handle before the blob upload completes.
477
479
  */
478
480
  readonly createBlobPayloadPending: true | undefined;
481
+
482
+ /**
483
+ * Controls automatic batch flushing during staging mode.
484
+ * Normal turn-based/async flush scheduling is suppressed while in staging mode
485
+ * until the accumulated batch reaches this many ops, at which point the batch
486
+ * is flushed. Incoming ops always break the current batch regardless of this setting.
487
+ *
488
+ * Set to Infinity to only break batches on system events (incoming ops).
489
+ *
490
+ * @defaultValue `largeBatchThreshold` (currently 1000)
491
+ */
492
+ readonly stagingModeAutoFlushThreshold: number;
493
+
494
+ /**
495
+ * When this property is set to true, the runtime will never send DocumentSchemaChange ops
496
+ * and will throw an error if any incoming DocumentSchemaChange ops are received.
497
+ * This effectively freezes the document schema at whatever state it was in when the document was created.
498
+ */
499
+ readonly disableSchemaUpgrade: boolean;
479
500
  }
480
501
 
481
502
  /**
@@ -608,6 +629,16 @@ const defaultMaxBatchSizeInBytes = 700 * 1024;
608
629
 
609
630
  const defaultChunkSizeInBytes = 204800;
610
631
 
632
+ /**
633
+ * Default maximum ops per staging-mode batch before automatic flush scheduling resumes.
634
+ *
635
+ * Chosen based on production telemetry: copy-paste operations routinely produce batches
636
+ * of 1000+ ops (435K instances over 30 days), and receivers on modern Fluid versions
637
+ * handle them without issues. Uses {@link largeBatchThreshold} to stay aligned with
638
+ * the existing "large batch" telemetry threshold ({@link OpGroupingManager}).
639
+ */
640
+ const defaultStagingModeAutoFlushThreshold = largeBatchThreshold;
641
+
611
642
  /**
612
643
  * The default time to wait for pending ops to be processed during summarization
613
644
  */
@@ -805,6 +836,24 @@ export async function loadContainerRuntime(
805
836
  return ContainerRuntime.loadRuntime(params);
806
837
  }
807
838
 
839
+ /**
840
+ * Alpha variant of {@link loadContainerRuntime} that returns the runtime in an
841
+ * extendable object, allowing additional properties to be added in the future.
842
+ *
843
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
844
+ * @returns An object containing the runtime.
845
+ *
846
+ * @legacy @alpha
847
+ */
848
+ export async function loadContainerRuntimeAlpha(params: LoadContainerRuntimeParams): Promise<{
849
+ runtime: IContainerRuntime & ContainerRuntimeBaseAlpha & IRuntime;
850
+ }> {
851
+ return ContainerRuntime.loadRuntime2({
852
+ ...params,
853
+ registry: new FluidDataStoreRegistry(params.registryEntries),
854
+ });
855
+ }
856
+
808
857
  const defaultMaxConsecutiveReconnects = 7;
809
858
 
810
859
  /**
@@ -880,14 +929,15 @@ export class ContainerRuntime
880
929
  return ContainerRuntime.loadRuntime2({
881
930
  ...params,
882
931
  registry: new FluidDataStoreRegistry(params.registryEntries),
883
- });
932
+ }).then((r) => r.runtime);
884
933
  }
885
934
 
886
935
  /**
887
- * Load the stores from a snapshot and returns the runtime.
936
+ * Load the stores from a snapshot and returns an object containing the runtime.
888
937
  * @remarks
889
938
  * Same as {@link ContainerRuntime.loadRuntime},
890
939
  * but with `registry` instead of `registryEntries` and more `runtimeOptions`.
940
+ * Returns `{ runtime }` to allow future extensions (e.g. staging mode controls).
891
941
  */
892
942
  public static async loadRuntime2(
893
943
  params: Omit<LoadContainerRuntimeParams, "registryEntries" | "runtimeOptions"> & {
@@ -906,7 +956,7 @@ export class ContainerRuntime
906
956
  */
907
957
  runtimeOptions?: IContainerRuntimeOptionsInternal;
908
958
  },
909
- ): Promise<ContainerRuntime> {
959
+ ): Promise<{ runtime: ContainerRuntime }> {
910
960
  const {
911
961
  context,
912
962
  registry,
@@ -962,6 +1012,8 @@ export class ContainerRuntime
962
1012
  loadSequenceNumberVerification: "close",
963
1013
  maxBatchSizeInBytes: defaultMaxBatchSizeInBytes,
964
1014
  chunkSizeInBytes: defaultChunkSizeInBytes,
1015
+ stagingModeAutoFlushThreshold: defaultStagingModeAutoFlushThreshold,
1016
+ disableSchemaUpgrade: false,
965
1017
  };
966
1018
 
967
1019
  const defaultConfigs = {
@@ -987,6 +1039,8 @@ export class ContainerRuntime
987
1039
  ? disabledCompressionConfig
988
1040
  : defaultConfigs.compressionOptions,
989
1041
  createBlobPayloadPending = defaultConfigs.createBlobPayloadPending,
1042
+ stagingModeAutoFlushThreshold = defaultConfigs.stagingModeAutoFlushThreshold,
1043
+ disableSchemaUpgrade = defaultConfigs.disableSchemaUpgrade,
990
1044
  }: IContainerRuntimeOptionsInternal = runtimeOptions;
991
1045
 
992
1046
  // If explicitSchemaControl is off, ensure that options which require explicitSchemaControl are not enabled.
@@ -1131,6 +1185,7 @@ export class ContainerRuntime
1131
1185
  idCompressorMode = desiredIdCompressorMode;
1132
1186
  }
1133
1187
 
1188
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1134
1189
  const createIdCompressorFn = (): IIdCompressor & IIdCompressorCore => {
1135
1190
  /**
1136
1191
  * Because the IdCompressor emits so much telemetry, this function is used to sample
@@ -1185,6 +1240,7 @@ export class ContainerRuntime
1185
1240
  },
1186
1241
  { minVersionForCollab },
1187
1242
  logger,
1243
+ disableSchemaUpgrade,
1188
1244
  );
1189
1245
 
1190
1246
  // If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
@@ -1215,6 +1271,8 @@ export class ContainerRuntime
1215
1271
  enableGroupedBatching,
1216
1272
  explicitSchemaControl,
1217
1273
  createBlobPayloadPending,
1274
+ stagingModeAutoFlushThreshold,
1275
+ disableSchemaUpgrade,
1218
1276
  };
1219
1277
 
1220
1278
  validateMinimumVersionForCollab(updatedMinVersionForCollab);
@@ -1250,7 +1308,7 @@ export class ContainerRuntime
1250
1308
  // or zero. This must be done before Container replays saved ops.
1251
1309
  await runtime.pendingStateManager.applyStashedOpsAt(runtimeSequenceNumber ?? 0);
1252
1310
 
1253
- return runtime;
1311
+ return { runtime };
1254
1312
  }
1255
1313
 
1256
1314
  public readonly options: Record<string | number, unknown>;
@@ -1320,6 +1378,7 @@ export class ContainerRuntime
1320
1378
  return this.documentsSchemaController.sessionSchema.runtime;
1321
1379
  }
1322
1380
 
1381
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1323
1382
  private _idCompressor: (IIdCompressor & IIdCompressorCore) | undefined;
1324
1383
 
1325
1384
  // We accumulate Id compressor Ops while Id compressor is not loaded yet (only for "delayed" mode)
@@ -1335,6 +1394,7 @@ export class ContainerRuntime
1335
1394
  /**
1336
1395
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.idCompressor}
1337
1396
  */
1397
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1338
1398
  public get idCompressor(): (IIdCompressor & IIdCompressorCore) | undefined {
1339
1399
  // Expose ID Compressor only if it's On from the start.
1340
1400
  // If container uses delayed mode, then we can only expose generateDocumentUniqueId() and nothing else.
@@ -1393,6 +1453,7 @@ export class ContainerRuntime
1393
1453
 
1394
1454
  private readonly batchRunner = new BatchRunCounter();
1395
1455
  private readonly _flushMode: FlushMode;
1456
+ private readonly stagingModeAutoFlushThreshold: number;
1396
1457
  /**
1397
1458
  * BatchId tracking is needed whenever there's a possibility of a "forked Container",
1398
1459
  * where the same local state is pending in two different running Containers, each of
@@ -1534,13 +1595,6 @@ export class ContainerRuntime
1534
1595
  return runtimeCompatDetailsForLoader;
1535
1596
  }
1536
1597
 
1537
- /**
1538
- * If true, will skip Outbox flushing before processing an incoming message (and on DeltaManager "op" event for loader back-compat),
1539
- * and instead the Outbox will check for a split batch on every submit.
1540
- * This is a kill-bit switch for this simplification of logic, in case it causes unexpected issues.
1541
- */
1542
- private readonly skipSafetyFlushDuringProcessStack: boolean;
1543
-
1544
1598
  private readonly extensions = new Map<ContainerExtensionId, ExtensionEntry>();
1545
1599
 
1546
1600
  /***/
@@ -1561,6 +1615,7 @@ export class ContainerRuntime
1561
1615
 
1562
1616
  blobManagerLoadInfo: IBlobManagerLoadInfo,
1563
1617
  private readonly _storage: IContainerStorageService,
1618
+ // eslint-disable-next-line import-x/no-deprecated -- Will be undeprecated in 2.100.0 when it becomes an internal API
1564
1619
  private readonly createIdCompressorFn: () => IIdCompressor & IIdCompressorCore,
1565
1620
 
1566
1621
  private readonly documentsSchemaController: DocumentsSchemaController,
@@ -1598,7 +1653,6 @@ export class ContainerRuntime
1598
1653
  audience,
1599
1654
  signalAudience,
1600
1655
  pendingLocalState,
1601
- supportedFeatures,
1602
1656
  snapshotWithContents,
1603
1657
  getConnectionState,
1604
1658
  } = context;
@@ -1752,15 +1806,6 @@ export class ContainerRuntime
1752
1806
  this.getConnectionState() === ConnectionState.CatchingUp
1753
1807
  : undefined;
1754
1808
 
1755
- this.mc.logger.sendTelemetryEvent({
1756
- eventName: "GCFeatureMatrix",
1757
- metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
1758
- inputs: JSON.stringify({
1759
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1760
- gcOptions_gcGeneration: runtimeOptions.gcOptions[gcGenerationOptionName],
1761
- }),
1762
- });
1763
-
1764
1809
  this.telemetryDocumentId = metadata?.telemetryDocumentId ?? uuid();
1765
1810
 
1766
1811
  const opGroupingManager = new OpGroupingManager(
@@ -1833,22 +1878,19 @@ export class ContainerRuntime
1833
1878
  this.maxConsecutiveReconnects =
1834
1879
  this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? defaultMaxConsecutiveReconnects;
1835
1880
 
1836
- // If the context has ILayerCompatDetails, it supports referenceSequenceNumbers since that features
1837
- // predates ILayerCompatDetails.
1838
- const referenceSequenceNumbersSupported =
1839
- maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails === undefined
1840
- ? supportedFeatures?.get("referenceSequenceNumbers") === true
1841
- : true;
1842
- if (
1843
- runtimeOptions.flushMode === (FlushModeExperimental.Async as unknown as FlushMode) &&
1844
- !referenceSequenceNumbersSupported
1845
- ) {
1846
- // The loader does not support reference sequence numbers, falling back on FlushMode.TurnBased
1847
- this.mc.logger.sendErrorEvent({ eventName: "FlushModeFallback" });
1848
- this._flushMode = FlushMode.TurnBased;
1849
- } else {
1850
- this._flushMode = runtimeOptions.flushMode;
1881
+ this._flushMode = runtimeOptions.flushMode;
1882
+ // TODO: Added in 2.90.0 - Remove this validation once we've released and confirmed no consumer passes an invalid flushMode value.
1883
+ if (this._flushMode !== FlushMode.Immediate && this._flushMode !== FlushMode.TurnBased) {
1884
+ const error = new UsageError(
1885
+ "Invalid flushMode runtime option. Expected FlushMode.Immediate or FlushMode.TurnBased.",
1886
+ );
1887
+ this.closeFn(error);
1888
+ throw error;
1851
1889
  }
1890
+ this.stagingModeAutoFlushThreshold =
1891
+ this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
1892
+ runtimeOptions.stagingModeAutoFlushThreshold ??
1893
+ defaultStagingModeAutoFlushThreshold;
1852
1894
  this.batchIdTrackingEnabled =
1853
1895
  this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
1854
1896
  this.mc.config.getBoolean("Fluid.ContainerRuntime.enableBatchIdTracking") ??
@@ -2007,10 +2049,6 @@ export class ContainerRuntime
2007
2049
 
2008
2050
  const legacySendBatchFn = makeLegacySendBatchFn(submitFn, this.innerDeltaManager);
2009
2051
 
2010
- this.skipSafetyFlushDuringProcessStack =
2011
- // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
2012
- this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
2013
-
2014
2052
  this.outbox = new Outbox({
2015
2053
  shouldSend: () => this.shouldSendOps(),
2016
2054
  pendingStateManager: this.pendingStateManager,
@@ -2021,8 +2059,6 @@ export class ContainerRuntime
2021
2059
  config: {
2022
2060
  compressionOptions,
2023
2061
  maxBatchSizeInBytes: runtimeOptions.maxBatchSizeInBytes,
2024
- // If we disable flush before process, we must be ready to flush partial batches
2025
- flushPartialBatches: this.skipSafetyFlushDuringProcessStack,
2026
2062
  },
2027
2063
  logger: this.mc.logger,
2028
2064
  groupingManager: opGroupingManager,
@@ -2079,14 +2115,12 @@ export class ContainerRuntime
2079
2115
  this.lastEmittedDirty = this.computeCurrentDirtyState();
2080
2116
  context.updateDirtyContainerState(this.lastEmittedDirty);
2081
2117
 
2082
- if (!this.skipSafetyFlushDuringProcessStack) {
2083
- // Reference Sequence Number may have just changed, and it must be consistent across a batch,
2084
- // so we should flush now to clear the way for the next ops.
2085
- // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
2086
- // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
2087
- // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
2088
- this.deltaManager.on("op", () => this.flush());
2089
- }
2118
+ // Reference Sequence Number may have just changed, and it must be consistent across a batch,
2119
+ // so we should flush now to clear the way for the next ops.
2120
+ // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
2121
+ // But we need this coverage for old loaders that don't call ContainerRuntime.process for non-runtime messages.
2122
+ // (We have to call flush _before_ processing a runtime op, but after is ok for non-runtime op)
2123
+ this.deltaManager.on("op", () => this.flush());
2090
2124
 
2091
2125
  // logging hardware telemetry
2092
2126
  this.baseLogger.send({
@@ -2102,7 +2136,9 @@ export class ContainerRuntime
2102
2136
  summaryNumber: loadSummaryNumber,
2103
2137
  summaryFormatVersion: metadata?.summaryFormatVersion,
2104
2138
  disableIsolatedChannels: metadata?.disableIsolatedChannels,
2139
+ // This is useful even for interactive clients since they track unreferenced nodes and log errors.
2105
2140
  gcVersion: metadata?.gcFeature,
2141
+ gcConfigs: this.garbageCollector.serializedConfigs,
2106
2142
  options: JSON.stringify(runtimeOptions),
2107
2143
  idCompressorModeMetadata: metadata?.documentSchema?.runtime?.idCompressorMode,
2108
2144
  idCompressorMode: this.sessionSchema.idCompressorMode,
@@ -2110,7 +2146,6 @@ export class ContainerRuntime
2110
2146
  featureGates: JSON.stringify({
2111
2147
  ...featureGatesForTelemetry,
2112
2148
  closeSummarizerDelayOverride,
2113
- disableFlushBeforeProcess: this.skipSafetyFlushDuringProcessStack,
2114
2149
  }),
2115
2150
  telemetryDocumentId: this.telemetryDocumentId,
2116
2151
  groupedBatchingEnabled: this.groupedBatchingEnabled,
@@ -2732,12 +2767,10 @@ export class ContainerRuntime
2732
2767
  this.emitDirtyDocumentEvent = false;
2733
2768
 
2734
2769
  try {
2735
- // Any ID Allocation ops that failed to submit after the pending state was queued need to have
2736
- // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
2737
- // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
2738
- // before staging mode so we can simply say staged: false.
2739
- this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
2740
- this.scheduleFlush();
2770
+ // Any ID Allocation ops that failed to submit need to have their ranges included
2771
+ // in the next allocation op. Reset the compressor's unfinalized range cursor so that the next
2772
+ // call to takeNextCreationRange (during replay) will include those unfinalized ranges.
2773
+ this._idCompressor?.resetUnfinalizedCreationRange();
2741
2774
 
2742
2775
  // replay the ops
2743
2776
  this.pendingStateManager.replayPendingStates();
@@ -3032,10 +3065,8 @@ export class ContainerRuntime
3032
3065
 
3033
3066
  this.verifyNotClosed();
3034
3067
 
3035
- if (!this.skipSafetyFlushDuringProcessStack) {
3036
- // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
3037
- this.flush();
3038
- }
3068
+ // Reference Sequence Number may be about to change, and it must be consistent across a batch, so flush now
3069
+ this.flush();
3039
3070
 
3040
3071
  this.ensureNoDataModelChanges(() => {
3041
3072
  this.processInboundMessageOrBatch(messageCopy, local);
@@ -3639,20 +3670,39 @@ export class ContainerRuntime
3639
3670
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
3640
3671
  this.flush();
3641
3672
 
3642
- const exitStagingMode = (discardOrCommit: () => void): void => {
3673
+ const exitStagingMode = (
3674
+ discardOrCommit: () => IPendingMessage["batchInfo"][],
3675
+ exitMethod: "commit" | "discard",
3676
+ ): void => {
3643
3677
  try {
3644
- // Final flush of any last staged changes
3645
- // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
3646
- this.outbox.flush();
3647
-
3648
- this.stageControls = undefined;
3649
-
3650
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
3651
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
3652
- this.submitIdAllocationOpIfNeeded({ staged: false });
3653
- discardOrCommit();
3654
-
3655
- this.channelCollection.notifyStagingMode(false);
3678
+ PerformanceEvent.timedExec(
3679
+ this.mc.logger,
3680
+ {
3681
+ eventName: `ExitStagingMode_${exitMethod}`,
3682
+ },
3683
+ (event) => {
3684
+ // Final flush of any last staged changes
3685
+ // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
3686
+ this.outbox.flush();
3687
+
3688
+ this.stageControls = undefined;
3689
+
3690
+ // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
3691
+ // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
3692
+ this.submitIdAllocationOpIfNeeded({ staged: false });
3693
+ const batchInfos = discardOrCommit();
3694
+ event.reportProgress({
3695
+ details: {
3696
+ autoFlushThreshold: this.stagingModeAutoFlushThreshold,
3697
+ batches: batchInfos.length,
3698
+ batchesAtOrOverThreshold: batchInfos.filter(
3699
+ (b) => b.length >= this.stagingModeAutoFlushThreshold,
3700
+ ).length,
3701
+ },
3702
+ });
3703
+ this.channelCollection.notifyStagingMode(false);
3704
+ },
3705
+ );
3656
3706
  } catch (error) {
3657
3707
  const normalizedError = normalizeError(error);
3658
3708
  this.closeFn(normalizedError);
@@ -3664,21 +3714,24 @@ export class ContainerRuntime
3664
3714
  discardChanges: () =>
3665
3715
  exitStagingMode(() => {
3666
3716
  // Pop all staged batches from the PSM and roll them back in LIFO order
3667
- this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
3668
- this.rollbackStagedChange(runtimeOp, localOpMetadata);
3669
- });
3717
+ const batchInfos = this.pendingStateManager.popStagedBatches(
3718
+ ({ runtimeOp, localOpMetadata }) => {
3719
+ this.rollbackStagedChange(runtimeOp, localOpMetadata);
3720
+ },
3721
+ );
3670
3722
  this.updateDocumentDirtyState();
3671
- }),
3723
+ return batchInfos;
3724
+ }, "discard"),
3672
3725
  commitChanges: (options) => {
3673
3726
  const { squash } = { ...defaultStagingCommitOptions, ...options };
3674
3727
  exitStagingMode(() => {
3675
3728
  // Replay all staged batches in typical FIFO order.
3676
3729
  // We'll be out of staging mode so they'll be sent to the service finally.
3677
- this.pendingStateManager.replayPendingStates({
3730
+ return this.pendingStateManager.replayPendingStates({
3678
3731
  committingStagedBatches: true,
3679
3732
  squash,
3680
3733
  });
3681
- });
3734
+ }, "commit");
3682
3735
  },
3683
3736
  };
3684
3737
 
@@ -3786,7 +3839,7 @@ export class ContainerRuntime
3786
3839
  }
3787
3840
 
3788
3841
  /**
3789
- * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
3842
+ * Returns true if the container is dirty: not attached, or has pending user messages (ignores "non-dirtyable" ones though)
3790
3843
  */
3791
3844
  private computeCurrentDirtyState(): boolean {
3792
3845
  return (
@@ -4809,6 +4862,20 @@ export class ContainerRuntime
4809
4862
  }
4810
4863
 
4811
4864
  private scheduleFlush(): void {
4865
+ // During staging mode, suppress automatic flush scheduling until the main batch
4866
+ // reaches or exceeds the threshold.
4867
+ // Incoming ops still break the batch via direct this.flush() calls elsewhere
4868
+ // (deltaManager "op" handler, process(), connection changes, getPendingLocalState,
4869
+ // exitStagingMode). Those all bypass scheduleFlush(), so they're unaffected by this check.
4870
+ // Additionally, outbox.maybeFlushPartialBatch() (called on every submit) detects
4871
+ // sequence number changes and throws if unexpected changes are detected.
4872
+ if (
4873
+ this.inStagingMode &&
4874
+ this.outbox.mainBatchMessageCount < this.stagingModeAutoFlushThreshold
4875
+ ) {
4876
+ return;
4877
+ }
4878
+
4812
4879
  if (this.flushScheduled) {
4813
4880
  return;
4814
4881
  }
@@ -4830,15 +4897,6 @@ export class ContainerRuntime
4830
4897
  break;
4831
4898
  }
4832
4899
 
4833
- // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
4834
- case FlushModeExperimental.Async as unknown as FlushMode: {
4835
- // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
4836
- // batch when all micro-tasks are complete.
4837
- // Compared to TurnBased, this flush mode will capture more ops into the same batch.
4838
- setTimeout(() => this.flush(), 0);
4839
- break;
4840
- }
4841
-
4842
4900
  default: {
4843
4901
  fail(0x587 /* Unreachable unless manually accumulating a batch */);
4844
4902
  }
@@ -5213,7 +5271,9 @@ export class ContainerRuntime
5213
5271
  eventName: "getPendingLocalState",
5214
5272
  },
5215
5273
  (event) => {
5216
- const pending = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
5274
+ const { pending } = this.pendingStateManager.getLocalState(
5275
+ props?.snapshotSequenceNumber,
5276
+ );
5217
5277
  const sessionExpiryTimerStarted =
5218
5278
  props?.sessionExpiryTimerStarted ?? this.garbageCollector.sessionExpiryTimerStarted;
5219
5279
 
@@ -100,6 +100,10 @@ export class GarbageCollector implements IGarbageCollector {
100
100
 
101
101
  private readonly configs: IGarbageCollectorConfigs;
102
102
 
103
+ public get serializedConfigs(): string {
104
+ return JSON.stringify(this.configs);
105
+ }
106
+
103
107
  public get shouldRunGC(): boolean {
104
108
  return this.configs.gcAllowed;
105
109
  }
@@ -334,15 +338,6 @@ export class GarbageCollector implements IGarbageCollector {
334
338
 
335
339
  return { gcData: { gcNodes }, usedRoutes };
336
340
  });
337
-
338
- // Log all the GC options and the state determined by the garbage collector.
339
- // This is useful even for interactive clients since they track unreferenced nodes and log errors.
340
- this.mc.logger.sendTelemetryEvent({
341
- eventName: "GarbageCollectorLoaded",
342
- gcConfigs: JSON.stringify(this.configs),
343
- gcOptions: JSON.stringify(createParams.gcOptions),
344
- ...createParams.createContainerMetadata,
345
- });
346
341
  }
347
342
 
348
343
  /**
@@ -373,6 +373,10 @@ export interface IGarbageCollectionRuntime {
373
373
  * Defines the contract for the garbage collector.
374
374
  */
375
375
  export interface IGarbageCollector {
376
+ /**
377
+ * The GC configurations serialized as a JSON string for telemetry.
378
+ */
379
+ readonly serializedConfigs: string;
376
380
  /**
377
381
  * Tells the time at which session expiry timer started in a previous container.
378
382
  * This is only set when loading from a stashed container and will be equal to the
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  type IContainerRuntimeOptions,
11
11
  type IContainerRuntimeOptionsInternal,
12
12
  loadContainerRuntime,
13
+ loadContainerRuntimeAlpha,
13
14
  type LoadContainerRuntimeParams,
14
15
  agentSchedulerId,
15
16
  ContainerRuntime,
@@ -162,7 +162,8 @@ export class BatchManager {
162
162
  throw new LoggingError("Ops generated during rollback", {
163
163
  count,
164
164
  ...tagData(TelemetryDataTag.UserData, {
165
- ops: serializeOp(this.pendingBatch.slice(startPoint).map((b) => b.runtimeOp)),
165
+ ops: serializeOp(this.pendingBatch.slice(startPoint).map((b) => b.runtimeOp))
166
+ .content,
166
167
  }),
167
168
  });
168
169
  }
@@ -45,6 +45,7 @@ export {
45
45
  } from "./remoteMessageProcessor.js";
46
46
  export {
47
47
  type EmptyGroupedBatch,
48
+ largeBatchThreshold,
48
49
  OpGroupingManager,
49
50
  type OpGroupingManagerConfig,
50
51
  isGroupedBatch,
@@ -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
  }