@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
@@ -152,7 +152,7 @@ import type {
152
152
  IEventSampler,
153
153
  IFluidErrorBase,
154
154
  ITelemetryGenericEventExt,
155
- ITelemetryLoggerExt,
155
+ TelemetryLoggerExt,
156
156
  MonitoringContext,
157
157
  } from "@fluidframework/telemetry-utils/internal";
158
158
  import {
@@ -735,7 +735,7 @@ function lastMessageFromMetadata(
735
735
  * to understand if/when it is hit.
736
736
  * We only want to log this once, to avoid spamming telemetry if we are wrong and these cases are hit commonly.
737
737
  */
738
- export let getSingleUseLegacyLogCallback = (logger: ITelemetryLoggerExt, type: string) => {
738
+ export let getSingleUseLegacyLogCallback = (logger: TelemetryLoggerExt, type: string) => {
739
739
  return (codePath: string): void => {
740
740
  logger.sendTelemetryEvent({
741
741
  eventName: "LegacyMessageFormat",
@@ -1360,7 +1360,25 @@ export class ContainerRuntime
1360
1360
  return this._getAttachState();
1361
1361
  }
1362
1362
 
1363
- public readonly isReadOnly = (): boolean => this.deltaManager.readOnlyInfo.readonly === true;
1363
+ /**
1364
+ * Whether local op submission is currently disallowed.
1365
+ *
1366
+ * This is `true` in two distinct situations.
1367
+ *
1368
+ * First: the delta manager reports a read-only connection (host/service-imposed permission or connection state — the historical meaning of `readOnly`).
1369
+ *
1370
+ * 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.
1371
+ *
1372
+ * 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.
1373
+ */
1374
+ public readonly isReadOnly = (): boolean =>
1375
+ // `_deltaManager` and `pendingStateManager` are both assigned partway
1376
+ // through the constructor; `baseLogger` is built earlier and stamps
1377
+ // `isReadOnly` on every error event (e.g. layer-compat failures
1378
+ // during construction), so this can be called before either is
1379
+ // assigned. Optional chains keep that window safe.
1380
+ this._deltaManager?.readOnlyInfo.readonly === true ||
1381
+ this.pendingStateManager?.isApplyingStashedOps === true;
1364
1382
 
1365
1383
  /**
1366
1384
  * Current session schema - defines what options are on & off.
@@ -1597,6 +1615,8 @@ export class ContainerRuntime
1597
1615
 
1598
1616
  private readonly extensions = new Map<ContainerExtensionId, ExtensionEntry>();
1599
1617
 
1618
+ public readonly baseLogger: ITelemetryBaseLogger;
1619
+
1600
1620
  /***/
1601
1621
  protected constructor(
1602
1622
  context: IContainerContext,
@@ -1610,7 +1630,7 @@ export class ContainerRuntime
1610
1630
  private readonly runtimeOptions: Readonly<ContainerRuntimeOptionsInternal>,
1611
1631
  private readonly containerScope: FluidObject,
1612
1632
  // Create a custom ITelemetryBaseLogger to output telemetry events.
1613
- public readonly baseLogger: ITelemetryBaseLogger,
1633
+ baseLogger: ITelemetryBaseLogger,
1614
1634
  existing: boolean,
1615
1635
 
1616
1636
  blobManagerLoadInfo: IBlobManagerLoadInfo,
@@ -1663,15 +1683,20 @@ export class ContainerRuntime
1663
1683
 
1664
1684
  this.isSnapshotInstanceOfISnapshot = snapshotWithContents !== undefined;
1665
1685
 
1666
- this.mc = createChildMonitoringContext({
1667
- logger: this.baseLogger,
1668
- namespace: "ContainerRuntime",
1686
+ this.baseLogger = createChildLogger({
1687
+ logger: baseLogger,
1669
1688
  properties: {
1670
- all: {
1671
- inStagingMode: this.inStagingMode,
1689
+ error: {
1690
+ inStagingMode: () => this.inStagingMode,
1691
+ isApplyingStashedOps: () => this.pendingStateManager?.isApplyingStashedOps,
1692
+ isReadOnly: () => this.isReadOnly(),
1672
1693
  },
1673
1694
  },
1674
1695
  });
1696
+ this.mc = createChildMonitoringContext({
1697
+ logger: this.baseLogger,
1698
+ namespace: "ContainerRuntime",
1699
+ });
1675
1700
 
1676
1701
  // Validate that the Loader is compatible with this Runtime.
1677
1702
  const maybeLoaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
@@ -1840,6 +1865,18 @@ export class ContainerRuntime
1840
1865
  },
1841
1866
  pendingRuntimeState?.pending,
1842
1867
  this.baseLogger,
1868
+ {
1869
+ // PSM has cleared `isApplyingStashedOps`; `isReadOnly()` now
1870
+ // reflects the network-readonly state again. Fan out so DDSes
1871
+ // know they can submit once more. No open hook is needed —
1872
+ // the apply window opens before `channelCollection` exists,
1873
+ // so a fanout there would be a no-op; data stores instead
1874
+ // pick up the initial readonly state from `isReadOnly()`
1875
+ // when they're first asked.
1876
+ onAfterStashedOpsApplied: () => {
1877
+ this.notifyReadOnlyState();
1878
+ },
1879
+ },
1843
1880
  );
1844
1881
 
1845
1882
  let outerDeltaManager: IDeltaManagerFull = this.innerDeltaManager;
@@ -1890,20 +1927,46 @@ export class ContainerRuntime
1890
1927
  this.mc.config.getNumber("Fluid.ContainerRuntime.StagingModeAutoFlushThreshold") ??
1891
1928
  runtimeOptions.stagingModeAutoFlushThreshold ??
1892
1929
  defaultStagingModeAutoFlushThreshold;
1893
- this.batchIdTrackingEnabled =
1894
- this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
1895
- this.mc.config.getBoolean("Fluid.ContainerRuntime.enableBatchIdTracking") ??
1896
- false;
1897
-
1898
- if (this.batchIdTrackingEnabled && this._flushMode !== FlushMode.TurnBased) {
1930
+ // BatchId tracking powers DuplicateBatchDetector (catching forked-container duplicates)
1931
+ // and is also a prerequisite for the Offline Load feature. It is enabled by default
1932
+ // when both TurnBased flush mode and grouped batching are active; the kill-switch
1933
+ // below allows disabling it without a code change if a regression is observed.
1934
+ // Grouped batching is required because resubmits can produce empty batches that must
1935
+ // still be sent on the wire as a placeholder grouped batch to preserve their batchId
1936
+ // (see OpGroupingManager.createEmptyGroupedBatch / outbox.flushEmptyBatch).
1937
+ // Offline Load requires all three prerequisites (TurnBased, grouped batching, and
1938
+ // batchId tracking not killed by config), so a consumer that opts into it without
1939
+ // them gets an explicit UsageError rather than silent degradation.
1940
+ const offlineLoadRequested =
1941
+ this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") === true;
1942
+ const disableBatchIdTracking =
1943
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableBatchIdTracking") === true;
1944
+
1945
+ if (offlineLoadRequested && this._flushMode !== FlushMode.TurnBased) {
1899
1946
  const error = new UsageError("Offline mode is only supported in turn-based mode");
1900
1947
  this.closeFn(error);
1901
1948
  throw error;
1902
1949
  }
1950
+ if (offlineLoadRequested && !this.groupedBatchingEnabled) {
1951
+ const error = new UsageError("Offline mode requires grouped batching to be enabled");
1952
+ this.closeFn(error);
1953
+ throw error;
1954
+ }
1955
+ if (offlineLoadRequested && disableBatchIdTracking) {
1956
+ const error = new UsageError(
1957
+ "Offline mode requires batchId tracking; remove Fluid.ContainerRuntime.DisableBatchIdTracking",
1958
+ );
1959
+ this.closeFn(error);
1960
+ throw error;
1961
+ }
1903
1962
 
1904
- // DuplicateBatchDetection is only enabled if Offline Load is enabled
1905
- // It maintains a cache of all batchIds/sequenceNumbers within the collab window.
1906
- // Don't waste resources doing so if not needed.
1963
+ this.batchIdTrackingEnabled =
1964
+ !disableBatchIdTracking &&
1965
+ this._flushMode === FlushMode.TurnBased &&
1966
+ this.groupedBatchingEnabled;
1967
+
1968
+ // DuplicateBatchDetector maintains a cache of all batchIds/sequenceNumbers within the
1969
+ // collab window. Skip allocating it when batchId tracking is off.
1907
1970
  if (this.batchIdTrackingEnabled) {
1908
1971
  this.duplicateBatchDetector = new DuplicateBatchDetector(recentBatchInfo);
1909
1972
  }
@@ -2161,6 +2224,14 @@ export class ContainerRuntime
2161
2224
  telemetryDocumentId: this.telemetryDocumentId,
2162
2225
  groupedBatchingEnabled: this.groupedBatchingEnabled,
2163
2226
  initialSequenceNumber: this.deltaManager.initialSequenceNumber,
2227
+ // Number of ops since the last summary that this client is aware of (including ops still
2228
+ // queued for processing). Computed as the gap between the latest known op sequence number
2229
+ // and the sequence number of the message at which the last summary was taken (per snapshot
2230
+ // metadata). Falls back to lastKnownSeqNumber when no prior summary message is recorded
2231
+ // (e.g. new container or older snapshot without metadata).
2232
+ numUnsummarizedOps:
2233
+ this.deltaManager.lastKnownSeqNumber -
2234
+ (this.messageAtLastSummary?.sequenceNumber ?? 0),
2164
2235
  minVersionForCollab: this.minVersionForCollab,
2165
2236
  // logging hardware telemetry
2166
2237
  deviceSpec: { ...getDeviceSpec() },
@@ -2779,6 +2850,28 @@ export class ContainerRuntime
2779
2850
  return;
2780
2851
  }
2781
2852
 
2853
+ // Invariant: the canSendOps edge in `setConnectionStateCore` — the
2854
+ // only caller of this method — cannot fire while
2855
+ // `applyStashedOpsAt` is in flight, because the loader awaits the
2856
+ // apply before transitioning the runtime to a write-capable
2857
+ // connection. If this assert ever fires, that contract has changed
2858
+ // and the submit guard at `submit()` would catch a runtime-internal
2859
+ // resubmit (`Rejoin`, `GC`, `FluidDataStoreOp`) for an op type
2860
+ // outside the apply-window allowlist.
2861
+ //
2862
+ // The precondition is held by the load sequence: `loadRuntime2`
2863
+ // awaits `pendingStateManager.applyStashedOpsAt(...)` before
2864
+ // returning the runtime, and the loader gates `setLoaded` on that
2865
+ // completion before any write-capable connection edge fires. A
2866
+ // maintainer reordering either sequence (or adding a new
2867
+ // `canSendOps` edge that fires before the apply resolves) is what
2868
+ // would trip this assert.
2869
+ // @see {@link ContainerRuntime.loadRuntime2} (awaits `applyStashedOpsAt`)
2870
+ assert(
2871
+ !this.pendingStateManager.isApplyingStashedOps,
2872
+ 0xd01 /* replayPendingStates must not be called during stashed-op apply window */,
2873
+ );
2874
+
2782
2875
  // Replaying is an internal operation and we don't want to generate noise while doing it.
2783
2876
  // So temporarily disable dirty state change events, and save the old state.
2784
2877
  // When we're done, we'll emit the event if the state changed.
@@ -2894,8 +2987,14 @@ export class ContainerRuntime
2894
2987
  }
2895
2988
  }
2896
2989
 
2897
- private readonly notifyReadOnlyState = (readonly: boolean): void =>
2898
- this.channelCollection.notifyReadOnlyState(readonly);
2990
+ // Boolean payload from the `"readonly"` delta-manager event is intentionally
2991
+ // ignored — `isReadOnly()` aggregates delta-manager readonly with the PSM
2992
+ // apply window, and that aggregation is the source of truth for fanout.
2993
+ // `channelCollection?.` guards against future wiring changes; both callers
2994
+ // today (the `"readonly"` listener and `onAfterStashedOpsApplied`) fire
2995
+ // after `channelCollection` is assigned.
2996
+ private readonly notifyReadOnlyState = (_readonly?: boolean): void =>
2997
+ this.channelCollection?.notifyReadOnlyState(this.isReadOnly());
2899
2998
 
2900
2999
  public setConnectionState(canSendOps: boolean, clientId?: string): void {
2901
3000
  this.setConnectionStateToConnectedOrDisconnected(canSendOps, clientId);
@@ -3141,16 +3240,34 @@ export class ContainerRuntime
3141
3240
  eventName: "DuplicateBatch",
3142
3241
  details: {
3143
3242
  batchId: batchStart.batchId,
3243
+ batchIdExplicit: batchStart.batchId !== undefined,
3144
3244
  clientId: batchStart.clientId,
3145
3245
  batchStartCsn: batchStart.batchStartCsn,
3146
3246
  size: inboundResult.length,
3147
3247
  duplicateBatchSequenceNumber: result.otherSequenceNumber,
3248
+ // Identifying info for the ORIGINAL occurrence of this batch, so we can
3249
+ // disambiguate the duplicate's source (e.g. resubmit vs fresh submit, same
3250
+ // vs different wire clientId). Undefined fields indicate the original was
3251
+ // loaded from a summary snapshot rather than seen at runtime.
3252
+ otherClientId: result.otherBatchInfo?.clientId,
3253
+ otherBatchStartCsn: result.otherBatchInfo?.batchStartCsn,
3254
+ otherBatchIdExplicit: result.otherBatchInfo?.batchIdExplicit,
3255
+ otherFromSnapshot: result.otherBatchInfo === undefined,
3148
3256
  ...extractSafePropertiesFromMessage(batchStart.keyMessage),
3257
+ // For grouped batches, `keyMessage` is one of the sub-messages produced by
3258
+ // `OpGroupingManager.ungroupOp`, which overwrites `clientSequenceNumber`
3259
+ // with a synthetic counter (1, 2, 3, ...). Override with the real outer
3260
+ // envelope's clientSequenceNumber so downstream telemetry doesn't get a
3261
+ // misleading "fake csn" value.
3262
+ messageClientSequenceNumber: batchStart.batchStartCsn,
3149
3263
  },
3150
3264
  },
3151
3265
  error,
3152
3266
  );
3153
- throw error;
3267
+ // 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
3268
+ // 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
3269
+ // or once we are able to identify batch duplication reason (forking vs service bug).
3270
+ // throw error;
3154
3271
  }
3155
3272
  }
3156
3273
 
@@ -4034,7 +4151,7 @@ export class ContainerRuntime
4034
4151
  /**
4035
4152
  * Logger to use for correlated summary events
4036
4153
  */
4037
- summaryLogger?: ITelemetryLoggerExt;
4154
+ summaryLogger?: TelemetryLoggerExt;
4038
4155
  /**
4039
4156
  * True to run garbage collection before summarizing; defaults to true
4040
4157
  */
@@ -4243,7 +4360,7 @@ export class ContainerRuntime
4243
4360
  /**
4244
4361
  * Logger to use for logging GC events
4245
4362
  */
4246
- logger?: ITelemetryLoggerExt;
4363
+ logger?: TelemetryLoggerExt;
4247
4364
  /**
4248
4365
  * True to run GC sweep phase after the mark phase
4249
4366
  */
@@ -4674,7 +4791,7 @@ export class ContainerRuntime
4674
4791
  * @returns failed summarize result (IBaseSummarizeResult) if summary should be failed, undefined otherwise.
4675
4792
  */
4676
4793
  private async shouldFailSummaryOnPendingOps(
4677
- logger: ITelemetryLoggerExt,
4794
+ logger: TelemetryLoggerExt,
4678
4795
  referenceSequenceNumber: number,
4679
4796
  minimumSequenceNumber: number,
4680
4797
  finalAttempt: boolean,
@@ -4796,6 +4913,52 @@ export class ContainerRuntime
4796
4913
  ): void {
4797
4914
  this.verifyNotClosed();
4798
4915
 
4916
+ // Nothing should be submitting while we're replaying stashed ops.
4917
+ // The runtime is readonly during the apply window (see
4918
+ // `PendingStateManager._applyLifecycle`), so a compliant DDS skips
4919
+ // submits. If we land here anyway, a DDS bypassed the readonly gate
4920
+ // (e.g. a realize-time write that doesn't consult `readOnly`) and
4921
+ // produced a local op that has no counterpart in the saved-op
4922
+ // replay — we cannot reconcile the mismatch, so fail fatally. We
4923
+ // check here (rather than at flush) because outbox flushes are
4924
+ // deferred and the apply window could close before the offending op
4925
+ // reaches the pending queue.
4926
+ //
4927
+ // Allowlist: `BlobAttach` is a runtime-internal op type that may
4928
+ // legitimately fire during apply — produced by `sharePendingBlobs`,
4929
+ // which is invoked from `loadRuntime2` before `applyStashedOpsAt`
4930
+ // resolves. `IdAllocation` is not in this allowlist because the
4931
+ // assert at 0x9a5 below enforces that it never reaches `submit()`
4932
+ // at all; treating that assert as the single source of truth.
4933
+ //
4934
+ // Always surface the error event to telemetry on a bypass so we can
4935
+ // attribute incidents regardless of the on-switch state. The
4936
+ // `EnableSubmitDuringStashedApplyThrow` config opts in to the
4937
+ // throw + container close; by default we log only, so a first- or
4938
+ // third-party DDS that quietly bypasses the readonly gate in
4939
+ // production is observable without escalating to a fatal close.
4940
+ if (
4941
+ this.pendingStateManager.isApplyingStashedOps &&
4942
+ containerRuntimeMessage.type !== ContainerMessageType.BlobAttach
4943
+ ) {
4944
+ const error = new UsageError("Local op submitted during stashed-op apply window", {
4945
+ messageType: containerRuntimeMessage.type,
4946
+ });
4947
+ this.mc.logger.sendErrorEvent({ eventName: "SubmitDuringStashedOpApply" }, error);
4948
+ if (
4949
+ this.mc.config.getBoolean(
4950
+ "Fluid.ContainerRuntime.EnableSubmitDuringStashedApplyThrow",
4951
+ ) === true
4952
+ ) {
4953
+ // Close the container before throwing so the "throw + close"
4954
+ // contract is enforced by this code path rather than by
4955
+ // whichever caller happens to wrap the throw in `.catch(closeFn)`.
4956
+ // `closeFn` is idempotent; a caller that also closes won't double-close.
4957
+ this.closeFn(error);
4958
+ throw error;
4959
+ }
4960
+ }
4961
+
4799
4962
  // There should be no ops in detached container state!
4800
4963
  assert(
4801
4964
  this.attachState !== AttachState.Detached,
@@ -5184,7 +5347,7 @@ export class ContainerRuntime
5184
5347
  private async fetchLatestSnapshotAndMaybeClose(
5185
5348
  targetRefSeq: number,
5186
5349
  targetAckHandle: string,
5187
- logger: ITelemetryLoggerExt,
5350
+ logger: TelemetryLoggerExt,
5188
5351
  ): Promise<void> {
5189
5352
  const fetchedSnapshotRefSeq = await PerformanceEvent.timedExecAsync(
5190
5353
  logger,
package/src/dataStore.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  IFluidDataStoreChannel,
14
14
  } from "@fluidframework/runtime-definitions/internal";
15
15
  import {
16
- type ITelemetryLoggerExt,
16
+ type TelemetryLoggerExt,
17
17
  TelemetryDataTag,
18
18
  UsageError,
19
19
  } from "@fluidframework/telemetry-utils/internal";
@@ -58,7 +58,7 @@ export const channelToDataStore = (
58
58
  fluidDataStoreChannel: IFluidDataStoreChannel,
59
59
  internalId: string,
60
60
  channelCollection: ChannelCollection,
61
- logger: ITelemetryLoggerExt,
61
+ logger: TelemetryLoggerExt,
62
62
  ): IDataStore => new DataStore(fluidDataStoreChannel, internalId, channelCollection, logger);
63
63
 
64
64
  enum AliasState {
@@ -194,7 +194,7 @@ class DataStore implements IDataStore {
194
194
  private readonly fluidDataStoreChannel: IFluidDataStoreChannel,
195
195
  private readonly internalId: string,
196
196
  private readonly channelCollection: ChannelCollection,
197
- private readonly logger: ITelemetryLoggerExt,
197
+ private readonly logger: TelemetryLoggerExt,
198
198
  private readonly parentContext = channelCollection.parentContext,
199
199
  ) {
200
200
  this.pendingAliases = channelCollection.pendingAliases;
@@ -75,6 +75,7 @@ import { channelsTreeName } from "@fluidframework/runtime-definitions/internal";
75
75
  import {
76
76
  addBlobToSummary,
77
77
  isSnapshotFetchRequiredForLoadingGroupId,
78
+ dataStoreLoadTelemetryProps,
78
79
  } from "@fluidframework/runtime-utils/internal";
79
80
  import {
80
81
  DataProcessingError,
@@ -588,10 +589,7 @@ export abstract class FluidDataStoreContext
588
589
  "realizeFluidDataStoreContext",
589
590
  );
590
591
  errorWrapped.addTelemetryProperties(
591
- tagCodeArtifacts({
592
- fullPackageName: this.pkg?.join("/"),
593
- fluidDataStoreId: this.id,
594
- }),
592
+ dataStoreLoadTelemetryProps({ id: this.id, packagePath: this.pkg ?? [] }),
595
593
  );
596
594
  this.mc.logger.sendErrorEvent({ eventName: "RealizeError" }, errorWrapped);
597
595
  throw errorWrapped;
@@ -6,7 +6,7 @@
6
6
  import type { IDisposable, ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
7
7
  import { assert, Deferred, Lazy } from "@fluidframework/core-utils/internal";
8
8
  import {
9
- type ITelemetryLoggerExt,
9
+ type TelemetryLoggerExt,
10
10
  createChildLogger,
11
11
  } from "@fluidframework/telemetry-utils/internal";
12
12
 
@@ -68,7 +68,7 @@ export class DataStoreContexts
68
68
  }
69
69
  });
70
70
 
71
- private readonly _logger: ITelemetryLoggerExt;
71
+ private readonly _logger: TelemetryLoggerExt;
72
72
 
73
73
  constructor(baseLogger: ITelemetryBaseLogger) {
74
74
  this._logger = createChildLogger({ logger: baseLogger });
@@ -7,10 +7,7 @@ import { performanceNow, type TypedEventEmitter } from "@fluid-internal/client-u
7
7
  import type { IDeltaManagerFull } from "@fluidframework/container-definitions/internal";
8
8
  import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
9
9
  import type { IContainerRuntimeBaseEvents } from "@fluidframework/runtime-definitions/internal";
10
- import {
11
- type ITelemetryLoggerExt,
12
- formatTick,
13
- } from "@fluidframework/telemetry-utils/internal";
10
+ import { type TelemetryLoggerExt, formatTick } from "@fluidframework/telemetry-utils/internal";
14
11
 
15
12
  /**
16
13
  * DeltaScheduler is responsible for the scheduling of inbound delta queue in cases where there
@@ -55,7 +52,7 @@ export class DeltaScheduler {
55
52
  constructor(
56
53
  private readonly deltaManager: IDeltaManagerFull,
57
54
  private readonly runtimeEventsEmitter: TypedEventEmitter<IContainerRuntimeBaseEvents>,
58
- private readonly logger: ITelemetryLoggerExt,
55
+ private readonly logger: TelemetryLoggerExt,
59
56
  ) {
60
57
  this.deltaManager.inbound.on("idle", this.inboundQueueIdle);
61
58
  runtimeEventsEmitter.on("batchBegin", this.batchBegin);
@@ -19,7 +19,7 @@ import {
19
19
  responseToException,
20
20
  } from "@fluidframework/runtime-utils/internal";
21
21
  import {
22
- type ITelemetryLoggerExt,
22
+ type TelemetryLoggerExt,
23
23
  DataProcessingError,
24
24
  type MonitoringContext,
25
25
  PerformanceEvent,
@@ -498,7 +498,7 @@ export class GarbageCollector implements IGarbageCollector {
498
498
  /**
499
499
  * Logger to use for logging GC events
500
500
  */
501
- logger?: ITelemetryLoggerExt;
501
+ logger?: TelemetryLoggerExt;
502
502
  /**
503
503
  * True to run GC sweep phase after the mark phase
504
504
  */
@@ -605,7 +605,7 @@ export class GarbageCollector implements IGarbageCollector {
605
605
  private async runGC(
606
606
  fullGC: boolean,
607
607
  currentReferenceTimestampMs: number,
608
- logger: ITelemetryLoggerExt,
608
+ logger: TelemetryLoggerExt,
609
609
  ): Promise<IGCStats> {
610
610
  // 1. Generate / analyze the runtime's reference graph.
611
611
  // Get the reference graph (gcData) and run GC algorithm to get referenced / unreferenced nodes.
@@ -796,7 +796,7 @@ export class GarbageCollector implements IGarbageCollector {
796
796
  private findAllNodesReferencedBetweenGCs(
797
797
  currentGCData: IGarbageCollectionData,
798
798
  previousGCData: IGarbageCollectionData | undefined,
799
- logger: ITelemetryLoggerExt,
799
+ logger: TelemetryLoggerExt,
800
800
  ): string[] | undefined {
801
801
  // If we haven't run GC before there is nothing to do.
802
802
  // No previousGCData, means nothing is unreferenced, and there are no reference state trackers to clear
@@ -840,14 +840,21 @@ export class GarbageCollector implements IGarbageCollector {
840
840
  const gcDataSuperSet = concatGarbageCollectionData(previousGCData, currentGCData);
841
841
  const newOutboundRoutesSinceLastRun: string[] = [];
842
842
  for (const [sourceNodeId, outboundRoutes] of this.newReferencesSinceLastRun) {
843
- if (gcDataSuperSet.gcNodes[sourceNodeId] === undefined) {
843
+ const target: string[] | undefined = gcDataSuperSet.gcNodes[sourceNodeId];
844
+ if (target === undefined) {
844
845
  gcDataSuperSet.gcNodes[sourceNodeId] = outboundRoutes;
845
846
  } else {
846
- // TODO: Fix this violation and remove the disable
847
- // eslint-disable-next-line @fluid-internal/fluid/no-unchecked-record-access
848
- gcDataSuperSet.gcNodes[sourceNodeId].push(...outboundRoutes);
847
+ // Avoid `push(...outboundRoutes)`: spreading a large array into a variadic call
848
+ // can exceed the engine's argument-count limit and throw RangeError.
849
+ for (const route of outboundRoutes) {
850
+ target.push(route);
851
+ }
852
+ }
853
+ // Avoid `push(...outboundRoutes)`: spreading a large array into a variadic call
854
+ // can exceed the engine's argument-count limit and throw RangeError.
855
+ for (const route of outboundRoutes) {
856
+ newOutboundRoutesSinceLastRun.push(route);
849
857
  }
850
- newOutboundRoutesSinceLastRun.push(...outboundRoutes);
851
858
  }
852
859
 
853
860
  /**
@@ -14,7 +14,7 @@ import type {
14
14
  } from "@fluidframework/runtime-definitions/internal";
15
15
  import type { ReadAndParseBlob } from "@fluidframework/runtime-utils/internal";
16
16
  import type {
17
- ITelemetryLoggerExt,
17
+ TelemetryLoggerExt,
18
18
  ITelemetryPropertiesExt,
19
19
  } from "@fluidframework/telemetry-utils/internal";
20
20
 
@@ -396,7 +396,7 @@ export interface IGarbageCollector {
396
396
  */
397
397
  collectGarbage(
398
398
  options: {
399
- logger?: ITelemetryLoggerExt;
399
+ logger?: TelemetryLoggerExt;
400
400
  runSweep?: boolean;
401
401
  fullGC?: boolean;
402
402
  },
@@ -505,7 +505,7 @@ export interface IGarbageCollectorCreateParams {
505
505
  readonly closeFn: (error: ICriticalContainerError) => void;
506
506
 
507
507
  readonly gcOptions: IGCRuntimeOptions;
508
- readonly baseLogger: ITelemetryLoggerExt;
508
+ readonly baseLogger: TelemetryLoggerExt;
509
509
  readonly existing: boolean;
510
510
 
511
511
  readonly metadata: IContainerRuntimeMetadata | undefined;
@@ -6,7 +6,7 @@
6
6
  import type { Tagged } from "@fluidframework/core-interfaces";
7
7
  import type { IGarbageCollectionData } from "@fluidframework/runtime-definitions/internal";
8
8
  import {
9
- type ITelemetryLoggerExt,
9
+ type TelemetryLoggerExt,
10
10
  type MonitoringContext,
11
11
  generateStack,
12
12
  tagCodeArtifacts,
@@ -350,7 +350,7 @@ export class GCTelemetryTracker {
350
350
  currentGCData: IGarbageCollectionData,
351
351
  previousGCData: IGarbageCollectionData,
352
352
  explicitReferences: Map<string, string[]>,
353
- logger: ITelemetryLoggerExt,
353
+ logger: TelemetryLoggerExt,
354
354
  ): void {
355
355
  for (const [nodeId, currentOutboundRoutes] of Object.entries(currentGCData.gcNodes)) {
356
356
  const previousRoutes = previousGCData.gcNodes[nodeId] ?? [];
@@ -395,7 +395,7 @@ export class GCTelemetryTracker {
395
395
  * Log events that are pending in pendingEventsQueue. This is called after GC runs in the summarizer client
396
396
  * so that the state of an unreferenced node is updated.
397
397
  */
398
- public async logPendingEvents(logger: ITelemetryLoggerExt): Promise<void> {
398
+ public async logPendingEvents(logger: TelemetryLoggerExt): Promise<void> {
399
399
  // Events sent come only from the summarizer client. In between summaries, events are pushed to a queue and at
400
400
  // summary time they are then logged.
401
401
  // Events generated:
@@ -9,7 +9,7 @@ import { assert } from "@fluidframework/core-utils/internal";
9
9
  import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
10
10
  import { isRuntimeMessage } from "@fluidframework/driver-utils/internal";
11
11
  import {
12
- type ITelemetryLoggerExt,
12
+ type TelemetryLoggerExt,
13
13
  DataCorruptionError,
14
14
  DataProcessingError,
15
15
  extractSafePropertiesFromMessage,
@@ -38,7 +38,7 @@ export class InboundBatchAggregator {
38
38
  constructor(
39
39
  private readonly deltaManager: IDeltaManagerFull,
40
40
  private readonly getClientId: () => string | undefined,
41
- private readonly logger: ITelemetryLoggerExt,
41
+ private readonly logger: TelemetryLoggerExt,
42
42
  ) {
43
43
  // Listen for updates and peek at the inbound
44
44
  this.deltaManager.inbound.on("push", this.trackPending);