@fluidframework/container-runtime 2.101.1 → 2.103.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 (229) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/batchTracker.d.ts +1 -1
  4. package/dist/batchTracker.d.ts.map +1 -1
  5. package/dist/batchTracker.js +1 -1
  6. package/dist/batchTracker.js.map +1 -1
  7. package/dist/blobManager/blobManagerSnapSum.d.ts +2 -2
  8. package/dist/blobManager/blobManagerSnapSum.d.ts.map +1 -1
  9. package/dist/blobManager/blobManagerSnapSum.js.map +1 -1
  10. package/dist/connectionTelemetry.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +16 -5
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +160 -18
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStore.d.ts +2 -2
  16. package/dist/dataStore.d.ts.map +1 -1
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/dataStoreContext.d.ts.map +1 -1
  19. package/dist/dataStoreContext.js +1 -4
  20. package/dist/dataStoreContext.js.map +1 -1
  21. package/dist/dataStoreContexts.d.ts.map +1 -1
  22. package/dist/dataStoreContexts.js.map +1 -1
  23. package/dist/deltaScheduler.d.ts +2 -2
  24. package/dist/deltaScheduler.d.ts.map +1 -1
  25. package/dist/deltaScheduler.js.map +1 -1
  26. package/dist/gc/garbageCollection.d.ts +2 -2
  27. package/dist/gc/garbageCollection.d.ts.map +1 -1
  28. package/dist/gc/garbageCollection.js +12 -5
  29. package/dist/gc/garbageCollection.js.map +1 -1
  30. package/dist/gc/gcDefinitions.d.ts +3 -3
  31. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  32. package/dist/gc/gcDefinitions.js.map +1 -1
  33. package/dist/gc/gcTelemetry.d.ts +3 -3
  34. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  35. package/dist/gc/gcTelemetry.js.map +1 -1
  36. package/dist/inboundBatchAggregator.d.ts +2 -2
  37. package/dist/inboundBatchAggregator.d.ts.map +1 -1
  38. package/dist/inboundBatchAggregator.js.map +1 -1
  39. package/dist/opLifecycle/duplicateBatchDetector.d.ts +39 -3
  40. package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
  41. package/dist/opLifecycle/duplicateBatchDetector.js +57 -15
  42. package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -1
  43. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  44. package/dist/opLifecycle/opCompressor.js.map +1 -1
  45. package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
  46. package/dist/opLifecycle/opDecompressor.js.map +1 -1
  47. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  48. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  49. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  50. package/dist/opLifecycle/opSplitter.js.map +1 -1
  51. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  52. package/dist/opLifecycle/outbox.js.map +1 -1
  53. package/dist/packageVersion.d.ts +1 -1
  54. package/dist/packageVersion.js +1 -1
  55. package/dist/packageVersion.js.map +1 -1
  56. package/dist/pendingStateManager.d.ts +48 -1
  57. package/dist/pendingStateManager.d.ts.map +1 -1
  58. package/dist/pendingStateManager.js +54 -1
  59. package/dist/pendingStateManager.js.map +1 -1
  60. package/dist/runtimeLayerCompatState.d.ts +2 -2
  61. package/dist/signalTelemetryProcessing.d.ts +2 -2
  62. package/dist/signalTelemetryProcessing.d.ts.map +1 -1
  63. package/dist/signalTelemetryProcessing.js.map +1 -1
  64. package/dist/summary/documentSchema.d.ts +2 -2
  65. package/dist/summary/documentSchema.d.ts.map +1 -1
  66. package/dist/summary/documentSchema.js +35 -3
  67. package/dist/summary/documentSchema.js.map +1 -1
  68. package/dist/summary/orderedClientElection.d.ts +2 -2
  69. package/dist/summary/orderedClientElection.d.ts.map +1 -1
  70. package/dist/summary/orderedClientElection.js.map +1 -1
  71. package/dist/summary/summarizerClientElection.d.ts +2 -2
  72. package/dist/summary/summarizerClientElection.d.ts.map +1 -1
  73. package/dist/summary/summarizerClientElection.js.map +1 -1
  74. package/dist/summary/summarizerNode/summarizerNode.d.ts +3 -3
  75. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  76. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  77. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts +2 -2
  78. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  79. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  80. package/dist/summary/summarizerTypes.d.ts +3 -3
  81. package/dist/summary/summarizerTypes.d.ts.map +1 -1
  82. package/dist/summary/summarizerTypes.js.map +1 -1
  83. package/dist/summary/summaryCollection.d.ts.map +1 -1
  84. package/dist/summary/summaryCollection.js.map +1 -1
  85. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +2 -2
  86. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
  87. package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
  88. package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts +2 -2
  89. package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
  90. package/dist/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
  91. package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts +2 -2
  92. package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts.map +1 -1
  93. package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.js.map +1 -1
  94. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +2 -2
  95. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
  96. package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
  97. package/dist/summary/summaryManager.d.ts.map +1 -1
  98. package/dist/summary/summaryManager.js.map +1 -1
  99. package/lib/batchTracker.d.ts +1 -1
  100. package/lib/batchTracker.d.ts.map +1 -1
  101. package/lib/batchTracker.js +1 -1
  102. package/lib/batchTracker.js.map +1 -1
  103. package/lib/blobManager/blobManagerSnapSum.d.ts +2 -2
  104. package/lib/blobManager/blobManagerSnapSum.d.ts.map +1 -1
  105. package/lib/blobManager/blobManagerSnapSum.js.map +1 -1
  106. package/lib/connectionTelemetry.js.map +1 -1
  107. package/lib/containerRuntime.d.ts +16 -5
  108. package/lib/containerRuntime.d.ts.map +1 -1
  109. package/lib/containerRuntime.js +160 -18
  110. package/lib/containerRuntime.js.map +1 -1
  111. package/lib/dataStore.d.ts +2 -2
  112. package/lib/dataStore.d.ts.map +1 -1
  113. package/lib/dataStore.js.map +1 -1
  114. package/lib/dataStoreContext.d.ts.map +1 -1
  115. package/lib/dataStoreContext.js +2 -5
  116. package/lib/dataStoreContext.js.map +1 -1
  117. package/lib/dataStoreContexts.d.ts.map +1 -1
  118. package/lib/dataStoreContexts.js.map +1 -1
  119. package/lib/deltaScheduler.d.ts +2 -2
  120. package/lib/deltaScheduler.d.ts.map +1 -1
  121. package/lib/deltaScheduler.js +1 -1
  122. package/lib/deltaScheduler.js.map +1 -1
  123. package/lib/gc/garbageCollection.d.ts +2 -2
  124. package/lib/gc/garbageCollection.d.ts.map +1 -1
  125. package/lib/gc/garbageCollection.js +12 -5
  126. package/lib/gc/garbageCollection.js.map +1 -1
  127. package/lib/gc/gcDefinitions.d.ts +3 -3
  128. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  129. package/lib/gc/gcDefinitions.js.map +1 -1
  130. package/lib/gc/gcTelemetry.d.ts +3 -3
  131. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  132. package/lib/gc/gcTelemetry.js.map +1 -1
  133. package/lib/inboundBatchAggregator.d.ts +2 -2
  134. package/lib/inboundBatchAggregator.d.ts.map +1 -1
  135. package/lib/inboundBatchAggregator.js.map +1 -1
  136. package/lib/opLifecycle/duplicateBatchDetector.d.ts +39 -3
  137. package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -1
  138. package/lib/opLifecycle/duplicateBatchDetector.js +57 -15
  139. package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -1
  140. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  141. package/lib/opLifecycle/opCompressor.js.map +1 -1
  142. package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
  143. package/lib/opLifecycle/opDecompressor.js.map +1 -1
  144. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  145. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  146. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  147. package/lib/opLifecycle/opSplitter.js.map +1 -1
  148. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  149. package/lib/opLifecycle/outbox.js.map +1 -1
  150. package/lib/packageVersion.d.ts +1 -1
  151. package/lib/packageVersion.js +1 -1
  152. package/lib/packageVersion.js.map +1 -1
  153. package/lib/pendingStateManager.d.ts +48 -1
  154. package/lib/pendingStateManager.d.ts.map +1 -1
  155. package/lib/pendingStateManager.js +54 -1
  156. package/lib/pendingStateManager.js.map +1 -1
  157. package/lib/runtimeLayerCompatState.d.ts +2 -2
  158. package/lib/signalTelemetryProcessing.d.ts +2 -2
  159. package/lib/signalTelemetryProcessing.d.ts.map +1 -1
  160. package/lib/signalTelemetryProcessing.js.map +1 -1
  161. package/lib/summary/documentSchema.d.ts +2 -2
  162. package/lib/summary/documentSchema.d.ts.map +1 -1
  163. package/lib/summary/documentSchema.js +35 -3
  164. package/lib/summary/documentSchema.js.map +1 -1
  165. package/lib/summary/orderedClientElection.d.ts +2 -2
  166. package/lib/summary/orderedClientElection.d.ts.map +1 -1
  167. package/lib/summary/orderedClientElection.js.map +1 -1
  168. package/lib/summary/summarizerClientElection.d.ts +2 -2
  169. package/lib/summary/summarizerClientElection.d.ts.map +1 -1
  170. package/lib/summary/summarizerClientElection.js.map +1 -1
  171. package/lib/summary/summarizerNode/summarizerNode.d.ts +3 -3
  172. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  173. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  174. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts +2 -2
  175. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  176. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  177. package/lib/summary/summarizerTypes.d.ts +3 -3
  178. package/lib/summary/summarizerTypes.d.ts.map +1 -1
  179. package/lib/summary/summarizerTypes.js.map +1 -1
  180. package/lib/summary/summaryCollection.d.ts.map +1 -1
  181. package/lib/summary/summaryCollection.js.map +1 -1
  182. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +2 -2
  183. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
  184. package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
  185. package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts +2 -2
  186. package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
  187. package/lib/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
  188. package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts +2 -2
  189. package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts.map +1 -1
  190. package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.js.map +1 -1
  191. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +2 -2
  192. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
  193. package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
  194. package/lib/summary/summaryManager.d.ts.map +1 -1
  195. package/lib/summary/summaryManager.js.map +1 -1
  196. package/package.json +18 -18
  197. package/src/batchTracker.ts +3 -3
  198. package/src/blobManager/blobManagerSnapSum.ts +2 -2
  199. package/src/connectionTelemetry.ts +3 -3
  200. package/src/containerRuntime.ts +188 -25
  201. package/src/dataStore.ts +3 -3
  202. package/src/dataStoreContext.ts +2 -4
  203. package/src/dataStoreContexts.ts +2 -2
  204. package/src/deltaScheduler.ts +2 -5
  205. package/src/gc/garbageCollection.ts +16 -9
  206. package/src/gc/gcDefinitions.ts +3 -3
  207. package/src/gc/gcTelemetry.ts +3 -3
  208. package/src/inboundBatchAggregator.ts +2 -2
  209. package/src/opLifecycle/duplicateBatchDetector.ts +103 -23
  210. package/src/opLifecycle/opCompressor.ts +2 -2
  211. package/src/opLifecycle/opDecompressor.ts +2 -2
  212. package/src/opLifecycle/opGroupingManager.ts +2 -2
  213. package/src/opLifecycle/opSplitter.ts +2 -2
  214. package/src/opLifecycle/outbox.ts +2 -2
  215. package/src/packageVersion.ts +1 -1
  216. package/src/pendingStateManager.ts +80 -2
  217. package/src/signalTelemetryProcessing.ts +2 -2
  218. package/src/summary/documentSchema.ts +58 -5
  219. package/src/summary/orderedClientElection.ts +3 -3
  220. package/src/summary/summarizerClientElection.ts +2 -2
  221. package/src/summary/summarizerNode/summarizerNode.ts +3 -3
  222. package/src/summary/summarizerNode/summarizerNodeUtils.ts +2 -2
  223. package/src/summary/summarizerTypes.ts +3 -3
  224. package/src/summary/summaryCollection.ts +2 -2
  225. package/src/summary/summaryDelayLoadedModule/runningSummarizer.ts +2 -4
  226. package/src/summary/summaryDelayLoadedModule/summarizer.ts +3 -3
  227. package/src/summary/summaryDelayLoadedModule/summarizerHeuristics.ts +2 -2
  228. package/src/summary/summaryDelayLoadedModule/summaryGenerator.ts +2 -2
  229. package/src/summary/summaryManager.ts +2 -2
@@ -632,14 +632,31 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
632
632
  this.electedSummarizerData = electedSummarizerData;
633
633
  this.runtimeOptions = runtimeOptions;
634
634
  this.containerScope = containerScope;
635
- this.baseLogger = baseLogger;
636
635
  this._storage = _storage;
637
636
  this.createIdCompressorFn = createIdCompressorFn;
638
637
  this.documentsSchemaController = documentsSchemaController;
639
638
  this.minVersionForCollab = minVersionForCollab;
640
639
  this.requestHandler = requestHandler;
641
640
  this.summaryConfiguration = summaryConfiguration;
642
- this.isReadOnly = () => this.deltaManager.readOnlyInfo.readonly === true;
641
+ /**
642
+ * Whether local op submission is currently disallowed.
643
+ *
644
+ * This is `true` in two distinct situations.
645
+ *
646
+ * First: the delta manager reports a read-only connection (host/service-imposed permission or connection state — the historical meaning of `readOnly`).
647
+ *
648
+ * Second: the `PendingStateManager` is replaying stashed ops (`isApplyingStashedOps`). During this window DDSes must not submit new local ops, as doing so would interleave fresh content ahead of the stashed pending stream and corrupt pending local state. Surfacing it through `isReadOnly()` lets DDSes that consult `readOnly` at realize time self-suppress; see the apply-lifecycle docs on `PendingStateManager` for the full rationale.
649
+ *
650
+ * Note this layers a third meaning ("transiently quiescing for stashed-op replay") onto the `readOnly` predicate, which is broader than its host/connection-permission origin.
651
+ */
652
+ this.isReadOnly = () =>
653
+ // `_deltaManager` and `pendingStateManager` are both assigned partway
654
+ // through the constructor; `baseLogger` is built earlier and stamps
655
+ // `isReadOnly` on every error event (e.g. layer-compat failures
656
+ // during construction), so this can be called before either is
657
+ // assigned. Optional chains keep that window safe.
658
+ this._deltaManager?.readOnlyInfo.readonly === true ||
659
+ this.pendingStateManager?.isApplyingStashedOps === true;
643
660
  // We accumulate Id compressor Ops while Id compressor is not loaded yet (only for "delayed" mode)
644
661
  // Once it loads, it will process all such ops and we will stop accumulating further ops - ops will be processes as they come in.
645
662
  this.pendingIdCompressorOps = [];
@@ -658,7 +675,13 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
658
675
  expiry: { policy: "absolute", durationMs: 60000 },
659
676
  });
660
677
  this.extensions = new Map();
661
- this.notifyReadOnlyState = (readonly) => this.channelCollection.notifyReadOnlyState(readonly);
678
+ // Boolean payload from the `"readonly"` delta-manager event is intentionally
679
+ // ignored — `isReadOnly()` aggregates delta-manager readonly with the PSM
680
+ // apply window, and that aggregation is the source of truth for fanout.
681
+ // `channelCollection?.` guards against future wiring changes; both callers
682
+ // today (the `"readonly"` listener and `onAfterStashedOpsApplied`) fire
683
+ // after `channelCollection` is assigned.
684
+ this.notifyReadOnlyState = (_readonly) => this.channelCollection?.notifyReadOnlyState(this.isReadOnly());
662
685
  /**
663
686
  * Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
664
687
  * To exit Staging Mode, call either discardChanges or commitChanges on the Stage Controls returned from this method.
@@ -817,15 +840,20 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
817
840
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
818
841
  this.disposeFn = disposeFn ?? closeFn;
819
842
  this.isSnapshotInstanceOfISnapshot = snapshotWithContents !== undefined;
820
- this.mc = (0, internal_8.createChildMonitoringContext)({
821
- logger: this.baseLogger,
822
- namespace: "ContainerRuntime",
843
+ this.baseLogger = (0, internal_8.createChildLogger)({
844
+ logger: baseLogger,
823
845
  properties: {
824
- all: {
825
- inStagingMode: this.inStagingMode,
846
+ error: {
847
+ inStagingMode: () => this.inStagingMode,
848
+ isApplyingStashedOps: () => this.pendingStateManager?.isApplyingStashedOps,
849
+ isReadOnly: () => this.isReadOnly(),
826
850
  },
827
851
  },
828
852
  });
853
+ this.mc = (0, internal_8.createChildMonitoringContext)({
854
+ logger: this.baseLogger,
855
+ namespace: "ContainerRuntime",
856
+ });
829
857
  // Validate that the Loader is compatible with this Runtime.
830
858
  const maybeLoaderCompatDetailsForRuntime = context;
831
859
  (0, runtimeLayerCompatState_js_1.validateLoaderCompatibility)(maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails, this.disposeFn, this.mc);
@@ -948,7 +976,18 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
948
976
  reSubmitBatch: this.reSubmitBatch.bind(this),
949
977
  isActiveConnection: () => this.innerDeltaManager.active,
950
978
  isAttached: () => this.attachState !== container_definitions_1.AttachState.Detached,
951
- }, pendingRuntimeState?.pending, this.baseLogger);
979
+ }, pendingRuntimeState?.pending, this.baseLogger, {
980
+ // PSM has cleared `isApplyingStashedOps`; `isReadOnly()` now
981
+ // reflects the network-readonly state again. Fan out so DDSes
982
+ // know they can submit once more. No open hook is needed —
983
+ // the apply window opens before `channelCollection` exists,
984
+ // so a fanout there would be a no-op; data stores instead
985
+ // pick up the initial readonly state from `isReadOnly()`
986
+ // when they're first asked.
987
+ onAfterStashedOpsApplied: () => {
988
+ this.notifyReadOnlyState();
989
+ },
990
+ });
952
991
  let outerDeltaManager = this.innerDeltaManager;
953
992
  this.useDeltaManagerOpsProxy =
954
993
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") === true;
@@ -985,18 +1024,39 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
985
1024
  this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
986
1025
  runtimeOptions.stagingModeAutoFlushThreshold ??
987
1026
  defaultStagingModeAutoFlushThreshold;
988
- this.batchIdTrackingEnabled =
989
- this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
990
- this.mc.config.getBoolean("Fluid.ContainerRuntime.enableBatchIdTracking") ??
991
- false;
992
- if (this.batchIdTrackingEnabled && this._flushMode !== internal_6.FlushMode.TurnBased) {
1027
+ // BatchId tracking powers DuplicateBatchDetector (catching forked-container duplicates)
1028
+ // and is also a prerequisite for the Offline Load feature. It is enabled by default
1029
+ // when both TurnBased flush mode and grouped batching are active; the kill-switch
1030
+ // below allows disabling it without a code change if a regression is observed.
1031
+ // Grouped batching is required because resubmits can produce empty batches that must
1032
+ // still be sent on the wire as a placeholder grouped batch to preserve their batchId
1033
+ // (see OpGroupingManager.createEmptyGroupedBatch / outbox.flushEmptyBatch).
1034
+ // Offline Load requires all three prerequisites (TurnBased, grouped batching, and
1035
+ // batchId tracking not killed by config), so a consumer that opts into it without
1036
+ // them gets an explicit UsageError rather than silent degradation.
1037
+ const offlineLoadRequested = this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") === true;
1038
+ const disableBatchIdTracking = this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableBatchIdTracking") === true;
1039
+ if (offlineLoadRequested && this._flushMode !== internal_6.FlushMode.TurnBased) {
993
1040
  const error = new internal_8.UsageError("Offline mode is only supported in turn-based mode");
994
1041
  this.closeFn(error);
995
1042
  throw error;
996
1043
  }
997
- // DuplicateBatchDetection is only enabled if Offline Load is enabled
998
- // It maintains a cache of all batchIds/sequenceNumbers within the collab window.
999
- // Don't waste resources doing so if not needed.
1044
+ if (offlineLoadRequested && !this.groupedBatchingEnabled) {
1045
+ const error = new internal_8.UsageError("Offline mode requires grouped batching to be enabled");
1046
+ this.closeFn(error);
1047
+ throw error;
1048
+ }
1049
+ if (offlineLoadRequested && disableBatchIdTracking) {
1050
+ const error = new internal_8.UsageError("Offline mode requires batchId tracking; remove Fluid.ContainerRuntime.DisableBatchIdTracking");
1051
+ this.closeFn(error);
1052
+ throw error;
1053
+ }
1054
+ this.batchIdTrackingEnabled =
1055
+ !disableBatchIdTracking &&
1056
+ this._flushMode === internal_6.FlushMode.TurnBased &&
1057
+ this.groupedBatchingEnabled;
1058
+ // DuplicateBatchDetector maintains a cache of all batchIds/sequenceNumbers within the
1059
+ // collab window. Skip allocating it when batchId tracking is off.
1000
1060
  if (this.batchIdTrackingEnabled) {
1001
1061
  this.duplicateBatchDetector = new index_js_3.DuplicateBatchDetector(recentBatchInfo);
1002
1062
  }
@@ -1193,6 +1253,13 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1193
1253
  telemetryDocumentId: this.telemetryDocumentId,
1194
1254
  groupedBatchingEnabled: this.groupedBatchingEnabled,
1195
1255
  initialSequenceNumber: this.deltaManager.initialSequenceNumber,
1256
+ // Number of ops since the last summary that this client is aware of (including ops still
1257
+ // queued for processing). Computed as the gap between the latest known op sequence number
1258
+ // and the sequence number of the message at which the last summary was taken (per snapshot
1259
+ // metadata). Falls back to lastKnownSeqNumber when no prior summary message is recorded
1260
+ // (e.g. new container or older snapshot without metadata).
1261
+ numUnsummarizedOps: this.deltaManager.lastKnownSeqNumber -
1262
+ (this.messageAtLastSummary?.sequenceNumber ?? 0),
1196
1263
  minVersionForCollab: this.minVersionForCollab,
1197
1264
  // logging hardware telemetry
1198
1265
  deviceSpec: { ...getDeviceSpec() },
@@ -1645,6 +1712,24 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1645
1712
  if (!this.shouldSendOps()) {
1646
1713
  return;
1647
1714
  }
1715
+ // Invariant: the canSendOps edge in `setConnectionStateCore` — the
1716
+ // only caller of this method — cannot fire while
1717
+ // `applyStashedOpsAt` is in flight, because the loader awaits the
1718
+ // apply before transitioning the runtime to a write-capable
1719
+ // connection. If this assert ever fires, that contract has changed
1720
+ // and the submit guard at `submit()` would catch a runtime-internal
1721
+ // resubmit (`Rejoin`, `GC`, `FluidDataStoreOp`) for an op type
1722
+ // outside the apply-window allowlist.
1723
+ //
1724
+ // The precondition is held by the load sequence: `loadRuntime2`
1725
+ // awaits `pendingStateManager.applyStashedOpsAt(...)` before
1726
+ // returning the runtime, and the loader gates `setLoaded` on that
1727
+ // completion before any write-capable connection edge fires. A
1728
+ // maintainer reordering either sequence (or adding a new
1729
+ // `canSendOps` edge that fires before the apply resolves) is what
1730
+ // would trip this assert.
1731
+ // @see {@link ContainerRuntime.loadRuntime2} (awaits `applyStashedOpsAt`)
1732
+ (0, internal_2.assert)(!this.pendingStateManager.isApplyingStashedOps, 0xd01 /* replayPendingStates must not be called during stashed-op apply window */);
1648
1733
  // Replaying is an internal operation and we don't want to generate noise while doing it.
1649
1734
  // So temporarily disable dirty state change events, and save the old state.
1650
1735
  // When we're done, we'll emit the event if the state changed.
@@ -1918,14 +2003,32 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1918
2003
  eventName: "DuplicateBatch",
1919
2004
  details: {
1920
2005
  batchId: batchStart.batchId,
2006
+ batchIdExplicit: batchStart.batchId !== undefined,
1921
2007
  clientId: batchStart.clientId,
1922
2008
  batchStartCsn: batchStart.batchStartCsn,
1923
2009
  size: inboundResult.length,
1924
2010
  duplicateBatchSequenceNumber: result.otherSequenceNumber,
2011
+ // Identifying info for the ORIGINAL occurrence of this batch, so we can
2012
+ // disambiguate the duplicate's source (e.g. resubmit vs fresh submit, same
2013
+ // vs different wire clientId). Undefined fields indicate the original was
2014
+ // loaded from a summary snapshot rather than seen at runtime.
2015
+ otherClientId: result.otherBatchInfo?.clientId,
2016
+ otherBatchStartCsn: result.otherBatchInfo?.batchStartCsn,
2017
+ otherBatchIdExplicit: result.otherBatchInfo?.batchIdExplicit,
2018
+ otherFromSnapshot: result.otherBatchInfo === undefined,
1925
2019
  ...(0, internal_8.extractSafePropertiesFromMessage)(batchStart.keyMessage),
2020
+ // For grouped batches, `keyMessage` is one of the sub-messages produced by
2021
+ // `OpGroupingManager.ungroupOp`, which overwrites `clientSequenceNumber`
2022
+ // with a synthetic counter (1, 2, 3, ...). Override with the real outer
2023
+ // envelope's clientSequenceNumber so downstream telemetry doesn't get a
2024
+ // misleading "fake csn" value.
2025
+ messageClientSequenceNumber: batchStart.batchStartCsn,
1926
2026
  },
1927
2027
  }, error);
1928
- throw error;
2028
+ // Due to a live incident where we had a bug in the service that caused duplicate batches to be sent to clients, we want to log when we detect a duplicate batch, but we don't want to throw an error
2029
+ // as it could hit the same service bug. We need to monitor below event to catch legitimate container forking scenarios and reenable throwing the data corruption error once the service bug is fixed and we stop seeing duplicate batches in the wild
2030
+ // or once we are able to identify batch duplication reason (forking vs service bug).
2031
+ // throw error;
1929
2032
  }
1930
2033
  }
1931
2034
  // Reach out to PendingStateManager, either to zip localOpMetadata into the *local* message list,
@@ -3023,6 +3126,45 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3023
3126
  }
3024
3127
  submit(containerRuntimeMessage, localOpMetadata = undefined, metadata) {
3025
3128
  this.verifyNotClosed();
3129
+ // Nothing should be submitting while we're replaying stashed ops.
3130
+ // The runtime is readonly during the apply window (see
3131
+ // `PendingStateManager._applyLifecycle`), so a compliant DDS skips
3132
+ // submits. If we land here anyway, a DDS bypassed the readonly gate
3133
+ // (e.g. a realize-time write that doesn't consult `readOnly`) and
3134
+ // produced a local op that has no counterpart in the saved-op
3135
+ // replay — we cannot reconcile the mismatch, so fail fatally. We
3136
+ // check here (rather than at flush) because outbox flushes are
3137
+ // deferred and the apply window could close before the offending op
3138
+ // reaches the pending queue.
3139
+ //
3140
+ // Allowlist: `BlobAttach` is a runtime-internal op type that may
3141
+ // legitimately fire during apply — produced by `sharePendingBlobs`,
3142
+ // which is invoked from `loadRuntime2` before `applyStashedOpsAt`
3143
+ // resolves. `IdAllocation` is not in this allowlist because the
3144
+ // assert at 0x9a5 below enforces that it never reaches `submit()`
3145
+ // at all; treating that assert as the single source of truth.
3146
+ //
3147
+ // Always surface the error event to telemetry on a bypass so we can
3148
+ // attribute incidents regardless of the on-switch state. The
3149
+ // `EnableSubmitDuringStashedApplyThrow` config opts in to the
3150
+ // throw + container close; by default we log only, so a first- or
3151
+ // third-party DDS that quietly bypasses the readonly gate in
3152
+ // production is observable without escalating to a fatal close.
3153
+ if (this.pendingStateManager.isApplyingStashedOps &&
3154
+ containerRuntimeMessage.type !== messageTypes_js_1.ContainerMessageType.BlobAttach) {
3155
+ const error = new internal_8.UsageError("Local op submitted during stashed-op apply window", {
3156
+ messageType: containerRuntimeMessage.type,
3157
+ });
3158
+ this.mc.logger.sendErrorEvent({ eventName: "SubmitDuringStashedOpApply" }, error);
3159
+ if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableSubmitDuringStashedApplyThrow") === true) {
3160
+ // Close the container before throwing so the "throw + close"
3161
+ // contract is enforced by this code path rather than by
3162
+ // whichever caller happens to wrap the throw in `.catch(closeFn)`.
3163
+ // `closeFn` is idempotent; a caller that also closes won't double-close.
3164
+ this.closeFn(error);
3165
+ throw error;
3166
+ }
3167
+ }
3026
3168
  // There should be no ops in detached container state!
3027
3169
  (0, internal_2.assert)(this.attachState !== container_definitions_1.AttachState.Detached, 0x132 /* "sending ops in detached container" */);
3028
3170
  (0, internal_2.assert)(metadata === undefined ||