@fluidframework/container-runtime 2.1.1 → 2.2.1

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