@fluidframework/container-runtime 2.1.0-281041 → 2.2.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 (207) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -6
  3. package/api-report/container-runtime.legacy.alpha.api.md +4 -3
  4. package/container-runtime.test-files.tar +0 -0
  5. package/dist/batchTracker.d.ts.map +1 -1
  6. package/dist/batchTracker.js.map +1 -1
  7. package/dist/blobManager/blobManager.d.ts.map +1 -1
  8. package/dist/blobManager/blobManager.js +9 -0
  9. package/dist/blobManager/blobManager.js.map +1 -1
  10. package/dist/channelCollection.d.ts +0 -14
  11. package/dist/channelCollection.d.ts.map +1 -1
  12. package/dist/channelCollection.js +2 -12
  13. package/dist/channelCollection.js.map +1 -1
  14. package/dist/containerRuntime.d.ts +34 -6
  15. package/dist/containerRuntime.d.ts.map +1 -1
  16. package/dist/containerRuntime.js +177 -74
  17. package/dist/containerRuntime.js.map +1 -1
  18. package/dist/dataStoreContext.d.ts +9 -18
  19. package/dist/dataStoreContext.d.ts.map +1 -1
  20. package/dist/dataStoreContext.js +40 -78
  21. package/dist/dataStoreContext.js.map +1 -1
  22. package/dist/gc/garbageCollection.d.ts +0 -6
  23. package/dist/gc/garbageCollection.d.ts.map +1 -1
  24. package/dist/gc/garbageCollection.js +23 -66
  25. package/dist/gc/garbageCollection.js.map +1 -1
  26. package/dist/gc/gcConfigs.d.ts.map +1 -1
  27. package/dist/gc/gcConfigs.js +11 -34
  28. package/dist/gc/gcConfigs.js.map +1 -1
  29. package/dist/gc/gcDefinitions.d.ts +9 -52
  30. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  31. package/dist/gc/gcDefinitions.js +3 -23
  32. package/dist/gc/gcDefinitions.js.map +1 -1
  33. package/dist/gc/gcHelpers.d.ts.map +1 -1
  34. package/dist/gc/gcHelpers.js +2 -6
  35. package/dist/gc/gcHelpers.js.map +1 -1
  36. package/dist/gc/gcSummaryStateTracker.d.ts +1 -1
  37. package/dist/gc/gcSummaryStateTracker.d.ts.map +1 -1
  38. package/dist/gc/gcSummaryStateTracker.js +4 -8
  39. package/dist/gc/gcSummaryStateTracker.js.map +1 -1
  40. package/dist/gc/gcTelemetry.d.ts +1 -9
  41. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  42. package/dist/gc/gcTelemetry.js +3 -25
  43. package/dist/gc/gcTelemetry.js.map +1 -1
  44. package/dist/gc/index.d.ts +2 -2
  45. package/dist/gc/index.d.ts.map +1 -1
  46. package/dist/gc/index.js +2 -7
  47. package/dist/gc/index.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/messageTypes.d.ts +6 -5
  53. package/dist/messageTypes.d.ts.map +1 -1
  54. package/dist/messageTypes.js.map +1 -1
  55. package/dist/metadata.d.ts +9 -1
  56. package/dist/metadata.d.ts.map +1 -1
  57. package/dist/metadata.js +6 -1
  58. package/dist/metadata.js.map +1 -1
  59. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  60. package/dist/opLifecycle/batchManager.js +1 -1
  61. package/dist/opLifecycle/batchManager.js.map +1 -1
  62. package/dist/opLifecycle/index.d.ts +1 -1
  63. package/dist/opLifecycle/index.d.ts.map +1 -1
  64. package/dist/opLifecycle/index.js +2 -1
  65. package/dist/opLifecycle/index.js.map +1 -1
  66. package/dist/opLifecycle/opGroupingManager.d.ts +8 -0
  67. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  68. package/dist/opLifecycle/opGroupingManager.js +34 -2
  69. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  70. package/dist/opLifecycle/outbox.d.ts +1 -0
  71. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  72. package/dist/opLifecycle/outbox.js +21 -1
  73. package/dist/opLifecycle/outbox.js.map +1 -1
  74. package/dist/opLifecycle/remoteMessageProcessor.d.ts +35 -14
  75. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  76. package/dist/opLifecycle/remoteMessageProcessor.js +71 -46
  77. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  78. package/dist/packageVersion.d.ts +1 -1
  79. package/dist/packageVersion.d.ts.map +1 -1
  80. package/dist/packageVersion.js +1 -1
  81. package/dist/packageVersion.js.map +1 -1
  82. package/dist/pendingStateManager.d.ts +28 -27
  83. package/dist/pendingStateManager.d.ts.map +1 -1
  84. package/dist/pendingStateManager.js +143 -112
  85. package/dist/pendingStateManager.js.map +1 -1
  86. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  87. package/dist/summary/summarizerNode/summarizerNode.js +5 -1
  88. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  89. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -4
  90. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  91. package/dist/summary/summarizerNode/summarizerNodeWithGc.js +16 -15
  92. package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  93. package/lib/batchTracker.d.ts.map +1 -1
  94. package/lib/batchTracker.js.map +1 -1
  95. package/lib/blobManager/blobManager.d.ts.map +1 -1
  96. package/lib/blobManager/blobManager.js +9 -0
  97. package/lib/blobManager/blobManager.js.map +1 -1
  98. package/lib/channelCollection.d.ts +0 -14
  99. package/lib/channelCollection.d.ts.map +1 -1
  100. package/lib/channelCollection.js +2 -11
  101. package/lib/channelCollection.js.map +1 -1
  102. package/lib/containerRuntime.d.ts +34 -6
  103. package/lib/containerRuntime.d.ts.map +1 -1
  104. package/lib/containerRuntime.js +178 -75
  105. package/lib/containerRuntime.js.map +1 -1
  106. package/lib/dataStoreContext.d.ts +9 -18
  107. package/lib/dataStoreContext.d.ts.map +1 -1
  108. package/lib/dataStoreContext.js +27 -65
  109. package/lib/dataStoreContext.js.map +1 -1
  110. package/lib/gc/garbageCollection.d.ts +0 -6
  111. package/lib/gc/garbageCollection.d.ts.map +1 -1
  112. package/lib/gc/garbageCollection.js +25 -68
  113. package/lib/gc/garbageCollection.js.map +1 -1
  114. package/lib/gc/gcConfigs.d.ts.map +1 -1
  115. package/lib/gc/gcConfigs.js +12 -35
  116. package/lib/gc/gcConfigs.js.map +1 -1
  117. package/lib/gc/gcDefinitions.d.ts +9 -52
  118. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  119. package/lib/gc/gcDefinitions.js +2 -22
  120. package/lib/gc/gcDefinitions.js.map +1 -1
  121. package/lib/gc/gcHelpers.d.ts.map +1 -1
  122. package/lib/gc/gcHelpers.js +2 -6
  123. package/lib/gc/gcHelpers.js.map +1 -1
  124. package/lib/gc/gcSummaryStateTracker.d.ts +1 -1
  125. package/lib/gc/gcSummaryStateTracker.d.ts.map +1 -1
  126. package/lib/gc/gcSummaryStateTracker.js +4 -8
  127. package/lib/gc/gcSummaryStateTracker.js.map +1 -1
  128. package/lib/gc/gcTelemetry.d.ts +1 -9
  129. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  130. package/lib/gc/gcTelemetry.js +3 -24
  131. package/lib/gc/gcTelemetry.js.map +1 -1
  132. package/lib/gc/index.d.ts +2 -2
  133. package/lib/gc/index.d.ts.map +1 -1
  134. package/lib/gc/index.js +2 -2
  135. package/lib/gc/index.js.map +1 -1
  136. package/lib/index.d.ts +1 -1
  137. package/lib/index.d.ts.map +1 -1
  138. package/lib/index.js +1 -1
  139. package/lib/index.js.map +1 -1
  140. package/lib/messageTypes.d.ts +6 -5
  141. package/lib/messageTypes.d.ts.map +1 -1
  142. package/lib/messageTypes.js.map +1 -1
  143. package/lib/metadata.d.ts +9 -1
  144. package/lib/metadata.d.ts.map +1 -1
  145. package/lib/metadata.js +4 -0
  146. package/lib/metadata.js.map +1 -1
  147. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  148. package/lib/opLifecycle/batchManager.js +1 -1
  149. package/lib/opLifecycle/batchManager.js.map +1 -1
  150. package/lib/opLifecycle/index.d.ts +1 -1
  151. package/lib/opLifecycle/index.d.ts.map +1 -1
  152. package/lib/opLifecycle/index.js +1 -1
  153. package/lib/opLifecycle/index.js.map +1 -1
  154. package/lib/opLifecycle/opGroupingManager.d.ts +8 -0
  155. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  156. package/lib/opLifecycle/opGroupingManager.js +34 -2
  157. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  158. package/lib/opLifecycle/outbox.d.ts +1 -0
  159. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  160. package/lib/opLifecycle/outbox.js +21 -1
  161. package/lib/opLifecycle/outbox.js.map +1 -1
  162. package/lib/opLifecycle/remoteMessageProcessor.d.ts +35 -14
  163. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  164. package/lib/opLifecycle/remoteMessageProcessor.js +69 -45
  165. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  166. package/lib/packageVersion.d.ts +1 -1
  167. package/lib/packageVersion.d.ts.map +1 -1
  168. package/lib/packageVersion.js +1 -1
  169. package/lib/packageVersion.js.map +1 -1
  170. package/lib/pendingStateManager.d.ts +28 -27
  171. package/lib/pendingStateManager.d.ts.map +1 -1
  172. package/lib/pendingStateManager.js +144 -113
  173. package/lib/pendingStateManager.js.map +1 -1
  174. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  175. package/lib/summary/summarizerNode/summarizerNode.js +5 -1
  176. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  177. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -4
  178. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  179. package/lib/summary/summarizerNode/summarizerNodeWithGc.js +16 -15
  180. package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  181. package/package.json +23 -32
  182. package/src/batchTracker.ts +4 -2
  183. package/src/blobManager/blobManager.ts +9 -0
  184. package/src/channelCollection.ts +2 -11
  185. package/src/containerRuntime.ts +214 -100
  186. package/src/dataStoreContext.ts +29 -93
  187. package/src/gc/garbageCollection.ts +26 -79
  188. package/src/gc/gcConfigs.ts +12 -45
  189. package/src/gc/gcDefinitions.ts +10 -55
  190. package/src/gc/gcHelpers.ts +10 -8
  191. package/src/gc/gcSummaryStateTracker.ts +6 -9
  192. package/src/gc/gcTelemetry.ts +3 -38
  193. package/src/gc/index.ts +2 -6
  194. package/src/index.ts +0 -1
  195. package/src/messageTypes.ts +12 -11
  196. package/src/metadata.ts +16 -2
  197. package/src/opLifecycle/batchManager.ts +4 -1
  198. package/src/opLifecycle/index.ts +6 -1
  199. package/src/opLifecycle/opGroupingManager.ts +42 -3
  200. package/src/opLifecycle/outbox.ts +31 -1
  201. package/src/opLifecycle/remoteMessageProcessor.ts +116 -57
  202. package/src/packageVersion.ts +1 -1
  203. package/src/pendingStateManager.ts +199 -177
  204. package/src/summary/README.md +31 -28
  205. package/src/summary/summarizerNode/summarizerNode.ts +6 -1
  206. package/src/summary/summarizerNode/summarizerNodeWithGc.ts +20 -43
  207. package/src/summary/summaryFormats.md +25 -22
@@ -162,7 +162,7 @@ import {
162
162
  ContainerRuntimeGCMessage,
163
163
  type ContainerRuntimeIdAllocationMessage,
164
164
  type InboundSequencedContainerRuntimeMessage,
165
- type InboundSequencedContainerRuntimeMessageOrSystemMessage,
165
+ type InboundSequencedNonContainerRuntimeMessage,
166
166
  type LocalContainerRuntimeMessage,
167
167
  type OutboundContainerRuntimeMessage,
168
168
  type UnknownContainerRuntimeMessage,
@@ -171,6 +171,7 @@ import { IBatchMetadata, ISavedOpMetadata } from "./metadata.js";
171
171
  import {
172
172
  BatchId,
173
173
  BatchMessage,
174
+ ensureContentsDeserialized,
174
175
  IBatch,
175
176
  IBatchCheckpoint,
176
177
  OpCompressor,
@@ -179,6 +180,7 @@ import {
179
180
  OpSplitter,
180
181
  Outbox,
181
182
  RemoteMessageProcessor,
183
+ type InboundBatch,
182
184
  } from "./opLifecycle/index.js";
183
185
  import { pkgVersion } from "./packageVersion.js";
184
186
  import {
@@ -521,6 +523,9 @@ export const TombstoneResponseHeaderKey = "isTombstoned";
521
523
  * Inactive error responses will have this header set to true
522
524
  * @legacy
523
525
  * @alpha
526
+ *
527
+ * @deprecated this header is deprecated and will be removed in the future. The functionality corresponding
528
+ * to this was experimental and is no longer supported.
524
529
  */
525
530
  export const InactiveResponseHeaderKey = "isInactive";
526
531
 
@@ -532,7 +537,6 @@ export interface RuntimeHeaderData {
532
537
  wait?: boolean;
533
538
  viaHandle?: boolean;
534
539
  allowTombstone?: boolean;
535
- allowInactive?: boolean;
536
540
  }
537
541
 
538
542
  /** Default values for Runtime Headers */
@@ -540,7 +544,6 @@ export const defaultRuntimeHeaderData: Required<RuntimeHeaderData> = {
540
544
  wait: true,
541
545
  viaHandle: false,
542
546
  allowTombstone: false,
543
- allowInactive: false,
544
547
  };
545
548
 
546
549
  /**
@@ -628,6 +631,7 @@ export const defaultPendingOpsRetryDelayMs = 1000;
628
631
  const defaultCloseSummarizerDelayMs = 5000; // 5 seconds
629
632
 
630
633
  /**
634
+ * Checks whether a message.type is one of the values in ContainerMessageType
631
635
  * @deprecated please use version in driver-utils
632
636
  * @internal
633
637
  */
@@ -687,9 +691,7 @@ export const makeLegacySendBatchFn =
687
691
  /** Helper type for type constraints passed through several functions.
688
692
  * local - Did this client send the op?
689
693
  * savedOp - Is this op being replayed after being serialized (having been sequenced previously)
690
- * batchStartCsn - The clientSequenceNumber given on submit to the start of this batch
691
- * message - The unpacked message. Likely a TypedContainerRuntimeMessage, but could also be a system op
692
- * modernRuntimeMessage - Does this appear like a current TypedContainerRuntimeMessage?
694
+ * localOpMetadata - Metadata maintained locally for a local op.
693
695
  */
694
696
  type MessageWithContext = {
695
697
  local: boolean;
@@ -698,14 +700,13 @@ type MessageWithContext = {
698
700
  } & (
699
701
  | {
700
702
  message: InboundSequencedContainerRuntimeMessage;
701
- modernRuntimeMessage: true;
703
+ isRuntimeMessage: true;
702
704
  }
703
705
  | {
704
- message: InboundSequencedContainerRuntimeMessageOrSystemMessage;
705
- modernRuntimeMessage: false;
706
+ message: InboundSequencedNonContainerRuntimeMessage;
707
+ isRuntimeMessage: false;
706
708
  }
707
709
  );
708
-
709
710
  const summarizerRequestUrl = "_summarizer";
710
711
 
711
712
  /**
@@ -760,6 +761,23 @@ function lastMessageFromMetadata(metadata: IContainerRuntimeMetadata | undefined
760
761
  : metadata?.message;
761
762
  }
762
763
 
764
+ /**
765
+ * There is some ancient back-compat code that we'd like to instrument
766
+ * to understand if/when it is hit.
767
+ * We only want to log this once, to avoid spamming telemetry if we are wrong and these cases are hit commonly.
768
+ */
769
+ let getSingleUseLegacyLogCallback = (logger: ITelemetryLoggerExt, type: string) => {
770
+ // We only want to log this once per ContainerRuntime instance, to avoid spamming telemetry.
771
+ getSingleUseLegacyLogCallback = () => () => {};
772
+
773
+ return (codePath: string) => {
774
+ logger.sendTelemetryEvent({
775
+ eventName: "LegacyMessageFormat",
776
+ details: { codePath, type },
777
+ });
778
+ };
779
+ };
780
+
763
781
  /**
764
782
  * Represents the runtime of the container. Contains helper functions/state of the container.
765
783
  * It will define the store level mappings.
@@ -1218,6 +1236,7 @@ export class ContainerRuntime
1218
1236
 
1219
1237
  private _orderSequentiallyCalls: number = 0;
1220
1238
  private readonly _flushMode: FlushMode;
1239
+ private readonly offlineEnabled: boolean;
1221
1240
  private flushTaskExists = false;
1222
1241
 
1223
1242
  private _connected: boolean;
@@ -1329,14 +1348,22 @@ export class ContainerRuntime
1329
1348
  */
1330
1349
  private nextSummaryNumber: number;
1331
1350
 
1332
- /** If false, loading or using a Tombstoned object should merely log, not fail */
1351
+ /**
1352
+ * If false, loading or using a Tombstoned object should merely log, not fail.
1353
+ * @deprecated NOT SUPPORTED - hardcoded to return false since it's deprecated.
1354
+ */
1355
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
1333
1356
  public get gcTombstoneEnforcementAllowed(): boolean {
1334
- return this.garbageCollector.tombstoneEnforcementAllowed;
1357
+ return false;
1335
1358
  }
1336
1359
 
1337
- /** If true, throw an error when a tombstone data store is used. */
1360
+ /**
1361
+ * If true, throw an error when a tombstone data store is used.
1362
+ * @deprecated NOT SUPPORTED - hardcoded to return false since it's deprecated.
1363
+ */
1364
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
1338
1365
  public get gcThrowOnTombstoneUsage(): boolean {
1339
- return this.garbageCollector.throwOnTombstoneUsage;
1366
+ return false;
1340
1367
  }
1341
1368
 
1342
1369
  /**
@@ -1546,7 +1573,6 @@ export class ContainerRuntime
1546
1573
  {
1547
1574
  applyStashedOp: this.applyStashedOp.bind(this),
1548
1575
  clientId: () => this.clientId,
1549
- close: this.closeFn,
1550
1576
  connected: () => this.connected,
1551
1577
  reSubmitBatch: this.reSubmitBatch.bind(this),
1552
1578
  isActiveConnection: () => this.innerDeltaManager.active,
@@ -1601,6 +1627,14 @@ export class ContainerRuntime
1601
1627
  } else {
1602
1628
  this._flushMode = runtimeOptions.flushMode;
1603
1629
  }
1630
+ this.offlineEnabled =
1631
+ this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ?? false;
1632
+
1633
+ if (this.offlineEnabled && this._flushMode !== FlushMode.TurnBased) {
1634
+ const error = new UsageError("Offline mode is only supported in turn-based mode");
1635
+ this.closeFn(error);
1636
+ throw error;
1637
+ }
1604
1638
 
1605
1639
  if (context.attachState === AttachState.Attached) {
1606
1640
  const maxSnapshotCacheDurationMs = this._storage?.policies?.maximumCacheDurationMs;
@@ -2608,62 +2642,92 @@ export class ContainerRuntime
2608
2642
  await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
2609
2643
  }
2610
2644
 
2611
- public process(messageArg: ISequencedDocumentMessage, local: boolean) {
2645
+ /**
2646
+ * Processes the op.
2647
+ * @param messageCopy - Sequenced message for a distributed document.
2648
+ * @param local - true if the message was originally generated by the client receiving it.
2649
+ */
2650
+ public process({ ...messageCopy }: ISequencedDocumentMessage, local: boolean) {
2651
+ // spread operator above ensure we make a shallow copy of message, as the processing flow will modify it.
2652
+ // There might be multiple container instances receiving the same message.
2653
+
2612
2654
  this.verifyNotClosed();
2613
2655
 
2614
2656
  // Whether or not the message appears to be a runtime message from an up-to-date client.
2615
2657
  // It may be a legacy runtime message (ie already unpacked and ContainerMessageType)
2616
2658
  // or something different, like a system message.
2617
- const modernRuntimeMessage = messageArg.type === MessageType.Operation;
2618
-
2619
- // Do shallow copy of message, as the processing flow will modify it.
2620
- // There might be multiple container instances receiving the same message.
2621
- // We do not need to make a deep copy. Each layer will just replace message.contents itself,
2622
- // but will not modify the contents object (likely it will replace it on the message).
2623
- const messageCopy = { ...messageArg };
2659
+ const hasModernRuntimeMessageEnvelope = messageCopy.type === MessageType.Operation;
2624
2660
  const savedOp = (messageCopy.metadata as ISavedOpMetadata)?.savedOp;
2625
- if (modernRuntimeMessage) {
2626
- const processResult = this.remoteMessageProcessor.process(messageCopy);
2627
- if (processResult === undefined) {
2661
+ const logLegacyCase = getSingleUseLegacyLogCallback(this.logger, messageCopy.type);
2662
+
2663
+ // We expect runtime messages to have JSON contents - deserialize it in place.
2664
+ ensureContentsDeserialized(messageCopy, hasModernRuntimeMessageEnvelope, logLegacyCase);
2665
+ if (hasModernRuntimeMessageEnvelope) {
2666
+ // If the message has the modern message envelope, then process it here.
2667
+ // Here we unpack the message (decompress, unchunk, and/or ungroup) into a batch of messages with ContainerMessageType
2668
+ const inboundBatch = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
2669
+ if (inboundBatch === undefined) {
2628
2670
  // This means the incoming message is an incomplete part of a message or batch
2629
2671
  // and we need to process more messages before the rest of the system can understand it.
2630
2672
  return;
2631
2673
  }
2632
- const batchStartCsn = processResult.batchStartCsn;
2633
- const batch = processResult.messages;
2634
- const messages: {
2635
- message: InboundSequencedContainerRuntimeMessage;
2636
- localOpMetadata: unknown;
2637
- }[] = local
2638
- ? this.pendingStateManager.processPendingLocalBatch(batch, batchStartCsn)
2639
- : batch.map((message) => ({ message, localOpMetadata: undefined }));
2640
- messages.forEach(({ message, localOpMetadata }) => {
2641
- const msg: MessageWithContext = {
2642
- message,
2643
- local,
2644
- modernRuntimeMessage,
2645
- savedOp,
2646
- localOpMetadata,
2647
- };
2648
- this.ensureNoDataModelChanges(() => this.processCore(msg));
2649
- });
2650
- } else {
2651
- const msg: MessageWithContext = {
2652
- message: messageCopy as InboundSequencedContainerRuntimeMessageOrSystemMessage,
2674
+
2675
+ // Reach out to PendingStateManager to zip localOpMetadata into the message list if it's a local batch
2676
+ const messagesWithPendingState = this.pendingStateManager.processInboundBatch(
2677
+ inboundBatch,
2653
2678
  local,
2654
- modernRuntimeMessage,
2655
- savedOp,
2656
- };
2657
- this.ensureNoDataModelChanges(() => this.processCore(msg));
2679
+ );
2680
+ if (messagesWithPendingState.length > 0) {
2681
+ messagesWithPendingState.forEach(({ message, localOpMetadata }) => {
2682
+ const msg: MessageWithContext = {
2683
+ message,
2684
+ local,
2685
+ isRuntimeMessage: true,
2686
+ savedOp,
2687
+ localOpMetadata,
2688
+ };
2689
+ this.ensureNoDataModelChanges(() => this.processRuntimeMessage(msg));
2690
+ });
2691
+ } else {
2692
+ this.ensureNoDataModelChanges(() => this.processEmptyBatch(inboundBatch, local));
2693
+ }
2694
+ } else {
2695
+ // Check if message.type is one of values in ContainerMessageType
2696
+ // eslint-disable-next-line import/no-deprecated
2697
+ if (isRuntimeMessage(messageCopy)) {
2698
+ // Legacy op received
2699
+ this.ensureNoDataModelChanges(() =>
2700
+ this.processRuntimeMessage({
2701
+ message: messageCopy as InboundSequencedContainerRuntimeMessage,
2702
+ local,
2703
+ isRuntimeMessage: true,
2704
+ savedOp,
2705
+ }),
2706
+ );
2707
+ } else {
2708
+ // A non container runtime message (like other system ops - join, ack, leave, nack etc.)
2709
+ this.ensureNoDataModelChanges(() =>
2710
+ this.observeNonRuntimeMessage({
2711
+ message: messageCopy as InboundSequencedNonContainerRuntimeMessage,
2712
+ local,
2713
+ isRuntimeMessage: false,
2714
+ savedOp,
2715
+ }),
2716
+ );
2717
+ }
2658
2718
  }
2659
2719
  }
2660
2720
 
2661
2721
  private _processedClientSequenceNumber: number | undefined;
2662
2722
 
2663
2723
  /**
2664
- * Direct the message to the correct subsystem for processing, and implement other side effects
2724
+ * Processes messages that are intended for the runtime layer to process.
2725
+ * It redirects the message to the correct subsystem for processing, and implement other side effects
2726
+ * @param messageWithContext - message to process with additional context and isRuntimeMessage prop as true
2665
2727
  */
2666
- private processCore(messageWithContext: MessageWithContext) {
2728
+ private processRuntimeMessage(
2729
+ messageWithContext: MessageWithContext & { isRuntimeMessage: true },
2730
+ ) {
2667
2731
  const { message, local, localOpMetadata } = messageWithContext;
2668
2732
 
2669
2733
  // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
@@ -2698,7 +2762,75 @@ export class ContainerRuntime
2698
2762
 
2699
2763
  this.validateAndProcessRuntimeMessage(messageWithContext, localOpMetadata);
2700
2764
 
2701
- this.emit("op", message, messageWithContext.modernRuntimeMessage);
2765
+ this.emit("op", message, messageWithContext.isRuntimeMessage);
2766
+
2767
+ this.scheduleManager.afterOpProcessing(undefined, message);
2768
+
2769
+ if (local) {
2770
+ // If we have processed a local op, this means that the container is
2771
+ // making progress and we can reset the counter for how many times
2772
+ // we have consecutively replayed the pending states
2773
+ this.resetReconnectCount();
2774
+ }
2775
+ } catch (e) {
2776
+ this.scheduleManager.afterOpProcessing(e, message);
2777
+ throw e;
2778
+ }
2779
+ }
2780
+
2781
+ /**
2782
+ * Process an empty batch, which will execute expected actions while processing even if there are no messages.
2783
+ * This is a separate function because the processCore function expects at least one message to process.
2784
+ * It is expected to happen only when the outbox produces an empty batch due to a resubmit flow.
2785
+ */
2786
+ private processEmptyBatch(emptyBatch: InboundBatch, local: boolean) {
2787
+ const { emptyBatchSequenceNumber: sequenceNumber, batchStartCsn } = emptyBatch;
2788
+ assert(sequenceNumber !== undefined, 0x9fa /* emptyBatchSequenceNumber must be defined */);
2789
+ this.emit("batchBegin", { sequenceNumber });
2790
+ this._processedClientSequenceNumber = batchStartCsn;
2791
+ if (!this.hasPendingMessages()) {
2792
+ this.updateDocumentDirtyState(false);
2793
+ }
2794
+ this.emit("batchEnd", undefined, { sequenceNumber });
2795
+ if (local) {
2796
+ this.resetReconnectCount();
2797
+ }
2798
+ }
2799
+
2800
+ /**
2801
+ * Observes messages that are not intended for the runtime layer, updating/notifying Runtime systems as needed.
2802
+ * @param messageWithContext - non-runtime messages to process with additional context and isRuntimeMessage prop as false
2803
+ */
2804
+ private observeNonRuntimeMessage(
2805
+ messageWithContext: MessageWithContext & { isRuntimeMessage: false },
2806
+ ) {
2807
+ const { message, local } = messageWithContext;
2808
+
2809
+ // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
2810
+ // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
2811
+ if (
2812
+ this.deltaManager.minimumSequenceNumber <
2813
+ messageWithContext.message.minimumSequenceNumber
2814
+ ) {
2815
+ messageWithContext.message.minimumSequenceNumber =
2816
+ this.deltaManager.minimumSequenceNumber;
2817
+ }
2818
+
2819
+ // Surround the actual processing of the operation with messages to the schedule manager indicating
2820
+ // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
2821
+ // messages once a batch has been fully processed.
2822
+ this.scheduleManager.beforeOpProcessing(message);
2823
+
2824
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
2825
+
2826
+ try {
2827
+ // If there are no more pending messages after processing a local message,
2828
+ // the document is no longer dirty.
2829
+ if (!this.hasPendingMessages()) {
2830
+ this.updateDocumentDirtyState(false);
2831
+ }
2832
+
2833
+ this.emit("op", message, messageWithContext.isRuntimeMessage);
2702
2834
 
2703
2835
  this.scheduleManager.afterOpProcessing(undefined, message);
2704
2836
 
@@ -2713,33 +2845,33 @@ export class ContainerRuntime
2713
2845
  throw e;
2714
2846
  }
2715
2847
  }
2848
+
2716
2849
  /**
2717
2850
  * Assuming the given message is also a TypedContainerRuntimeMessage,
2718
2851
  * checks its type and dispatches the message to the appropriate handler in the runtime.
2719
2852
  * Throws a DataProcessingError if the message looks like but doesn't conform to a known TypedContainerRuntimeMessage type.
2720
2853
  */
2721
2854
  private validateAndProcessRuntimeMessage(
2722
- messageWithContext: MessageWithContext,
2855
+ messageWithContext: MessageWithContext & { isRuntimeMessage: true },
2723
2856
  localOpMetadata: unknown,
2724
2857
  ): void {
2725
- // TODO: destructure message and modernRuntimeMessage once using typescript 5.2.2+
2726
- const { local } = messageWithContext;
2727
- switch (messageWithContext.message.type) {
2858
+ const { local, message, savedOp } = messageWithContext;
2859
+ switch (message.type) {
2728
2860
  case ContainerMessageType.Attach:
2729
2861
  case ContainerMessageType.Alias:
2730
2862
  case ContainerMessageType.FluidDataStoreOp:
2731
- this.channelCollection.process(messageWithContext.message, local, localOpMetadata);
2863
+ this.channelCollection.process(message, local, localOpMetadata);
2732
2864
  break;
2733
2865
  case ContainerMessageType.BlobAttach:
2734
- this.blobManager.processBlobAttachOp(messageWithContext.message, local);
2866
+ this.blobManager.processBlobAttachOp(message, local);
2735
2867
  break;
2736
2868
  case ContainerMessageType.IdAllocation:
2737
2869
  // Don't re-finalize the range if we're processing a "savedOp" in
2738
2870
  // stashed ops flow. The compressor is stashed with these ops already processed.
2739
2871
  // That said, in idCompressorMode === "delayed", we might not serialize ID compressor, and
2740
2872
  // thus we need to process all the ops.
2741
- if (!(this.skipSavedCompressorOps && messageWithContext.savedOp === true)) {
2742
- const range = messageWithContext.message.contents;
2873
+ if (!(this.skipSavedCompressorOps && savedOp === true)) {
2874
+ const range = message.contents;
2743
2875
  // Some other client turned on the id compressor. If we have not turned it on,
2744
2876
  // put it in a pending queue and delay finalization.
2745
2877
  if (this._idCompressor === undefined) {
@@ -2758,11 +2890,7 @@ export class ContainerRuntime
2758
2890
  }
2759
2891
  break;
2760
2892
  case ContainerMessageType.GC:
2761
- this.garbageCollector.processMessage(
2762
- messageWithContext.message,
2763
- messageWithContext.message.timestamp,
2764
- local,
2765
- );
2893
+ this.garbageCollector.processMessage(message, message.timestamp, local);
2766
2894
  break;
2767
2895
  case ContainerMessageType.ChunkedOp:
2768
2896
  // From observability POV, we should not exppse the rest of the system (including "op" events on object) to these messages.
@@ -2772,23 +2900,14 @@ export class ContainerRuntime
2772
2900
  break;
2773
2901
  case ContainerMessageType.DocumentSchemaChange:
2774
2902
  this.documentsSchemaController.processDocumentSchemaOp(
2775
- messageWithContext.message.contents,
2776
- messageWithContext.local,
2777
- messageWithContext.message.sequenceNumber,
2903
+ message.contents,
2904
+ local,
2905
+ message.sequenceNumber,
2778
2906
  );
2779
2907
  break;
2780
2908
  default: {
2781
- // If we didn't necessarily expect a runtime message type, then no worries - just return
2782
- // e.g. this case applies to system ops, or legacy ops that would have fallen into the above cases anyway.
2783
- if (!messageWithContext.modernRuntimeMessage) {
2784
- return;
2785
- }
2786
-
2787
- const compatBehavior = messageWithContext.message.compatDetails?.behavior;
2788
- if (
2789
- !compatBehaviorAllowsMessageType(messageWithContext.message.type, compatBehavior)
2790
- ) {
2791
- const { message } = messageWithContext;
2909
+ const compatBehavior = message.compatDetails?.behavior;
2910
+ if (!compatBehaviorAllowsMessageType(message.type, compatBehavior)) {
2792
2911
  const error = DataProcessingError.create(
2793
2912
  // Former assert 0x3ce
2794
2913
  "Runtime message of unknown type",
@@ -3970,8 +4089,6 @@ export class ContainerRuntime
3970
4089
  0x93f /* metadata */,
3971
4090
  );
3972
4091
 
3973
- const serializedContent = JSON.stringify(containerRuntimeMessage);
3974
-
3975
4092
  // Note that the real (non-proxy) delta manager is used here to get the readonly info. This is because
3976
4093
  // container runtime's ability to submit ops depend on the actual readonly state of the delta manager.
3977
4094
  if (this.innerDeltaManager.readOnlyInfo.readonly) {
@@ -3986,12 +4103,6 @@ export class ContainerRuntime
3986
4103
  type !== ContainerMessageType.IdAllocation,
3987
4104
  0x9a5 /* IdAllocation should be submitted directly to outbox. */,
3988
4105
  );
3989
- const message: BatchMessage = {
3990
- contents: serializedContent,
3991
- metadata,
3992
- localOpMetadata,
3993
- referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
3994
- };
3995
4106
 
3996
4107
  try {
3997
4108
  this.submitIdAllocationOpIfNeeded(false);
@@ -3999,19 +4110,19 @@ export class ContainerRuntime
3999
4110
  // Allow document schema controller to send a message if it needs to propose change in document schema.
4000
4111
  // If it needs to send a message, it will call provided callback with payload of such message and rely
4001
4112
  // on this callback to do actual sending.
4002
- const contents = this.documentsSchemaController.maybeSendSchemaMessage();
4003
- if (contents) {
4113
+ const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
4114
+ if (schemaChangeMessage) {
4004
4115
  this.logger.sendTelemetryEvent({
4005
4116
  eventName: "SchemaChangeProposal",
4006
- refSeq: contents.refSeq,
4007
- version: contents.version,
4008
- newRuntimeSchema: JSON.stringify(contents.runtime),
4117
+ refSeq: schemaChangeMessage.refSeq,
4118
+ version: schemaChangeMessage.version,
4119
+ newRuntimeSchema: JSON.stringify(schemaChangeMessage.runtime),
4009
4120
  sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
4010
4121
  oldRuntimeSchema: JSON.stringify(this.metadata?.documentSchema?.runtime),
4011
4122
  });
4012
4123
  const msg: ContainerRuntimeDocumentSchemaMessage = {
4013
4124
  type: ContainerMessageType.DocumentSchemaChange,
4014
- contents,
4125
+ contents: schemaChangeMessage,
4015
4126
  };
4016
4127
  this.outbox.submit({
4017
4128
  contents: JSON.stringify(msg),
@@ -4019,6 +4130,12 @@ export class ContainerRuntime
4019
4130
  });
4020
4131
  }
4021
4132
 
4133
+ const message: BatchMessage = {
4134
+ contents: JSON.stringify(containerRuntimeMessage) /* serialized content */,
4135
+ metadata,
4136
+ localOpMetadata,
4137
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
4138
+ };
4022
4139
  if (type === ContainerMessageType.BlobAttach) {
4023
4140
  // BlobAttach ops must have their metadata visible and cannot be grouped (see opGroupingManager.ts)
4024
4141
  this.outbox.submitBlobAttach(message);
@@ -4118,10 +4235,7 @@ export class ContainerRuntime
4118
4235
 
4119
4236
  // Only include Batch ID if "Offline Load" feature is enabled
4120
4237
  // It's only needed to identify batches across container forks arising from misuse of offline load.
4121
- const includeBatchId =
4122
- this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ?? false;
4123
-
4124
- this.flush(includeBatchId ? batchId : undefined);
4238
+ this.flush(this.offlineEnabled ? batchId : undefined);
4125
4239
  }
4126
4240
 
4127
4241
  private reSubmit(message: PendingMessageResubmitData) {
@@ -4324,7 +4438,7 @@ export class ContainerRuntime
4324
4438
  fetchSource: FetchSource.noCache,
4325
4439
  });
4326
4440
  const id = snapshot.snapshotTree.id;
4327
- assert(id !== undefined, "id of the fetched snapshot should be defined");
4441
+ assert(id !== undefined, 0x9d0 /* id of the fetched snapshot should be defined */);
4328
4442
  props.snapshotVersion = id;
4329
4443
  snapshotTree = snapshot.snapshotTree;
4330
4444
  } else {