@fluidframework/container-runtime 2.2.0 → 2.3.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 (181) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +15 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/channelCollection.d.ts +1 -1
  5. package/dist/channelCollection.d.ts.map +1 -1
  6. package/dist/channelCollection.js +1 -16
  7. package/dist/channelCollection.js.map +1 -1
  8. package/dist/connectionTelemetry.d.ts +27 -3
  9. package/dist/connectionTelemetry.d.ts.map +1 -1
  10. package/dist/connectionTelemetry.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +68 -13
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +262 -180
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/deltaManagerProxies.d.ts.map +1 -1
  16. package/dist/deltaManagerProxies.js +11 -4
  17. package/dist/deltaManagerProxies.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +0 -2
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcHelpers.d.ts.map +1 -1
  22. package/dist/gc/gcHelpers.js +0 -8
  23. package/dist/gc/gcHelpers.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/legacy.d.ts +3 -1
  29. package/dist/messageTypes.d.ts +0 -9
  30. package/dist/messageTypes.d.ts.map +1 -1
  31. package/dist/messageTypes.js.map +1 -1
  32. package/dist/opLifecycle/batchManager.d.ts +9 -0
  33. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  34. package/dist/opLifecycle/batchManager.js +19 -6
  35. package/dist/opLifecycle/batchManager.js.map +1 -1
  36. package/dist/opLifecycle/duplicateBatchDetector.d.ts +32 -0
  37. package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
  38. package/dist/opLifecycle/duplicateBatchDetector.js +68 -0
  39. package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -0
  40. package/dist/opLifecycle/index.d.ts +3 -2
  41. package/dist/opLifecycle/index.d.ts.map +1 -1
  42. package/dist/opLifecycle/index.js +4 -1
  43. package/dist/opLifecycle/index.js.map +1 -1
  44. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  45. package/dist/opLifecycle/opCompressor.js +0 -4
  46. package/dist/opLifecycle/opCompressor.js.map +1 -1
  47. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  48. package/dist/opLifecycle/opGroupingManager.js +0 -4
  49. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  50. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  51. package/dist/opLifecycle/opSplitter.js +1 -6
  52. package/dist/opLifecycle/opSplitter.js.map +1 -1
  53. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  54. package/dist/opLifecycle/outbox.js +1 -4
  55. package/dist/opLifecycle/outbox.js.map +1 -1
  56. package/dist/opLifecycle/remoteMessageProcessor.d.ts +37 -17
  57. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  58. package/dist/opLifecycle/remoteMessageProcessor.js +47 -37
  59. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  60. package/dist/packageVersion.d.ts +1 -1
  61. package/dist/packageVersion.js +1 -1
  62. package/dist/packageVersion.js.map +1 -1
  63. package/dist/pendingStateManager.d.ts +27 -17
  64. package/dist/pendingStateManager.d.ts.map +1 -1
  65. package/dist/pendingStateManager.js +85 -56
  66. package/dist/pendingStateManager.js.map +1 -1
  67. package/dist/scheduleManager.d.ts +2 -4
  68. package/dist/scheduleManager.d.ts.map +1 -1
  69. package/dist/scheduleManager.js +6 -37
  70. package/dist/scheduleManager.js.map +1 -1
  71. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  72. package/dist/summary/summarizerNode/summarizerNodeUtils.js +0 -2
  73. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  74. package/dist/summary/summaryCollection.d.ts.map +1 -1
  75. package/dist/summary/summaryCollection.js +5 -7
  76. package/dist/summary/summaryCollection.js.map +1 -1
  77. package/dist/summary/summaryFormat.d.ts.map +1 -1
  78. package/dist/summary/summaryFormat.js +1 -4
  79. package/dist/summary/summaryFormat.js.map +1 -1
  80. package/lib/channelCollection.d.ts +1 -1
  81. package/lib/channelCollection.d.ts.map +1 -1
  82. package/lib/channelCollection.js +1 -16
  83. package/lib/channelCollection.js.map +1 -1
  84. package/lib/connectionTelemetry.d.ts +27 -3
  85. package/lib/connectionTelemetry.d.ts.map +1 -1
  86. package/lib/connectionTelemetry.js.map +1 -1
  87. package/lib/containerRuntime.d.ts +68 -13
  88. package/lib/containerRuntime.d.ts.map +1 -1
  89. package/lib/containerRuntime.js +262 -181
  90. package/lib/containerRuntime.js.map +1 -1
  91. package/lib/deltaManagerProxies.d.ts.map +1 -1
  92. package/lib/deltaManagerProxies.js +11 -4
  93. package/lib/deltaManagerProxies.js.map +1 -1
  94. package/lib/gc/garbageCollection.d.ts.map +1 -1
  95. package/lib/gc/garbageCollection.js +0 -2
  96. package/lib/gc/garbageCollection.js.map +1 -1
  97. package/lib/gc/gcHelpers.d.ts.map +1 -1
  98. package/lib/gc/gcHelpers.js +0 -8
  99. package/lib/gc/gcHelpers.js.map +1 -1
  100. package/lib/index.d.ts +1 -1
  101. package/lib/index.d.ts.map +1 -1
  102. package/lib/index.js +1 -1
  103. package/lib/index.js.map +1 -1
  104. package/lib/legacy.d.ts +3 -1
  105. package/lib/messageTypes.d.ts +0 -9
  106. package/lib/messageTypes.d.ts.map +1 -1
  107. package/lib/messageTypes.js.map +1 -1
  108. package/lib/opLifecycle/batchManager.d.ts +9 -0
  109. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  110. package/lib/opLifecycle/batchManager.js +17 -5
  111. package/lib/opLifecycle/batchManager.js.map +1 -1
  112. package/lib/opLifecycle/duplicateBatchDetector.d.ts +32 -0
  113. package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
  114. package/lib/opLifecycle/duplicateBatchDetector.js +64 -0
  115. package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -0
  116. package/lib/opLifecycle/index.d.ts +3 -2
  117. package/lib/opLifecycle/index.d.ts.map +1 -1
  118. package/lib/opLifecycle/index.js +2 -1
  119. package/lib/opLifecycle/index.js.map +1 -1
  120. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  121. package/lib/opLifecycle/opCompressor.js +0 -4
  122. package/lib/opLifecycle/opCompressor.js.map +1 -1
  123. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  124. package/lib/opLifecycle/opGroupingManager.js +0 -4
  125. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  126. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  127. package/lib/opLifecycle/opSplitter.js +1 -6
  128. package/lib/opLifecycle/opSplitter.js.map +1 -1
  129. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  130. package/lib/opLifecycle/outbox.js +1 -4
  131. package/lib/opLifecycle/outbox.js.map +1 -1
  132. package/lib/opLifecycle/remoteMessageProcessor.d.ts +37 -17
  133. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  134. package/lib/opLifecycle/remoteMessageProcessor.js +47 -37
  135. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  136. package/lib/packageVersion.d.ts +1 -1
  137. package/lib/packageVersion.js +1 -1
  138. package/lib/packageVersion.js.map +1 -1
  139. package/lib/pendingStateManager.d.ts +27 -17
  140. package/lib/pendingStateManager.d.ts.map +1 -1
  141. package/lib/pendingStateManager.js +85 -56
  142. package/lib/pendingStateManager.js.map +1 -1
  143. package/lib/scheduleManager.d.ts +2 -4
  144. package/lib/scheduleManager.d.ts.map +1 -1
  145. package/lib/scheduleManager.js +6 -37
  146. package/lib/scheduleManager.js.map +1 -1
  147. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  148. package/lib/summary/summarizerNode/summarizerNodeUtils.js +0 -2
  149. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  150. package/lib/summary/summaryCollection.d.ts.map +1 -1
  151. package/lib/summary/summaryCollection.js +5 -7
  152. package/lib/summary/summaryCollection.js.map +1 -1
  153. package/lib/summary/summaryFormat.d.ts.map +1 -1
  154. package/lib/summary/summaryFormat.js +1 -4
  155. package/lib/summary/summaryFormat.js.map +1 -1
  156. package/lib/tsdoc-metadata.json +1 -1
  157. package/package.json +49 -27
  158. package/src/channelCollection.ts +7 -21
  159. package/src/connectionTelemetry.ts +33 -3
  160. package/src/containerRuntime.ts +382 -233
  161. package/src/deltaManagerProxies.ts +11 -4
  162. package/src/gc/garbageCollection.ts +1 -3
  163. package/src/gc/gcHelpers.ts +4 -12
  164. package/src/index.ts +2 -0
  165. package/src/messageTypes.ts +0 -10
  166. package/src/opLifecycle/batchManager.ts +29 -7
  167. package/src/opLifecycle/duplicateBatchDetector.ts +78 -0
  168. package/src/opLifecycle/index.ts +4 -1
  169. package/src/opLifecycle/opCompressor.ts +2 -6
  170. package/src/opLifecycle/opGroupingManager.ts +2 -6
  171. package/src/opLifecycle/opSplitter.ts +2 -6
  172. package/src/opLifecycle/outbox.ts +1 -3
  173. package/src/opLifecycle/remoteMessageProcessor.ts +87 -59
  174. package/src/packageVersion.ts +1 -1
  175. package/src/pendingStateManager.ts +114 -66
  176. package/src/scheduleManager.ts +8 -47
  177. package/src/summary/summarizerNode/summarizerNodeUtils.ts +1 -3
  178. package/src/summary/summaryCollection.ts +7 -9
  179. package/src/summary/summaryFormat.ts +1 -3
  180. package/src/summary/summaryFormats.md +11 -9
  181. package/tsconfig.json +1 -0
@@ -107,6 +107,7 @@ import {
107
107
  ITelemetryLoggerExt,
108
108
  DataCorruptionError,
109
109
  DataProcessingError,
110
+ extractSafePropertiesFromMessage,
110
111
  GenericError,
111
112
  IEventSampler,
112
113
  LoggingError,
@@ -162,7 +163,6 @@ import {
162
163
  ContainerRuntimeGCMessage,
163
164
  type ContainerRuntimeIdAllocationMessage,
164
165
  type InboundSequencedContainerRuntimeMessage,
165
- type InboundSequencedNonContainerRuntimeMessage,
166
166
  type LocalContainerRuntimeMessage,
167
167
  type OutboundContainerRuntimeMessage,
168
168
  type UnknownContainerRuntimeMessage,
@@ -171,6 +171,8 @@ import { IBatchMetadata, ISavedOpMetadata } from "./metadata.js";
171
171
  import {
172
172
  BatchId,
173
173
  BatchMessage,
174
+ BatchStartInfo,
175
+ DuplicateBatchDetector,
174
176
  ensureContentsDeserialized,
175
177
  IBatch,
176
178
  IBatchCheckpoint,
@@ -180,7 +182,6 @@ import {
180
182
  OpSplitter,
181
183
  Outbox,
182
184
  RemoteMessageProcessor,
183
- type InboundBatch,
184
185
  } from "./opLifecycle/index.js";
185
186
  import { pkgVersion } from "./packageVersion.js";
186
187
  import {
@@ -688,25 +689,6 @@ export const makeLegacySendBatchFn =
688
689
  return clientSequenceNumber;
689
690
  };
690
691
 
691
- /** Helper type for type constraints passed through several functions.
692
- * local - Did this client send the op?
693
- * savedOp - Is this op being replayed after being serialized (having been sequenced previously)
694
- * localOpMetadata - Metadata maintained locally for a local op.
695
- */
696
- type MessageWithContext = {
697
- local: boolean;
698
- savedOp?: boolean;
699
- localOpMetadata?: unknown;
700
- } & (
701
- | {
702
- message: InboundSequencedContainerRuntimeMessage;
703
- isRuntimeMessage: true;
704
- }
705
- | {
706
- message: InboundSequencedNonContainerRuntimeMessage;
707
- isRuntimeMessage: false;
708
- }
709
- );
710
692
  const summarizerRequestUrl = "_summarizer";
711
693
 
712
694
  /**
@@ -767,17 +749,68 @@ function lastMessageFromMetadata(metadata: IContainerRuntimeMetadata | undefined
767
749
  * We only want to log this once, to avoid spamming telemetry if we are wrong and these cases are hit commonly.
768
750
  */
769
751
  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
752
  return (codePath: string) => {
774
753
  logger.sendTelemetryEvent({
775
754
  eventName: "LegacyMessageFormat",
776
755
  details: { codePath, type },
777
756
  });
757
+
758
+ // Now that we've logged, prevent future logging (globally).
759
+ getSingleUseLegacyLogCallback = () => () => {};
778
760
  };
779
761
  };
780
762
 
763
+ /**
764
+ * This object holds the parameters necessary for the {@link loadContainerRuntime} function.
765
+ * @legacy
766
+ * @alpha
767
+ */
768
+ export interface LoadContainerRuntimeParams {
769
+ /**
770
+ * Context of the container.
771
+ */
772
+ context: IContainerContext;
773
+ /**
774
+ * Mapping from data store types to their corresponding factories
775
+ */
776
+ registryEntries: NamedFluidDataStoreRegistryEntries;
777
+ /**
778
+ * Pass 'true' if loading from an existing snapshot.
779
+ */
780
+ existing: boolean;
781
+ /**
782
+ * Additional options to be passed to the runtime
783
+ */
784
+ runtimeOptions?: IContainerRuntimeOptions;
785
+ /**
786
+ * runtime services provided with context
787
+ */
788
+ containerScope?: FluidObject;
789
+ /**
790
+ * Promise that resolves to an object which will act as entryPoint for the Container.
791
+ */
792
+ provideEntryPoint: (containerRuntime: IContainerRuntime) => Promise<FluidObject>;
793
+
794
+ /**
795
+ * Request handler for the request() method of the container runtime.
796
+ * Only relevant for back-compat while we remove the request() method and move fully to entryPoint as the main pattern.
797
+ * @deprecated Will be removed once Loader LTS version is "2.0.0-internal.7.0.0". Migrate all usage of IFluidRouter to the "entryPoint" pattern. Refer to Removing-IFluidRouter.md
798
+ * */
799
+ requestHandler?: (request: IRequest, runtime: IContainerRuntime) => Promise<IResponse>;
800
+ }
801
+ /**
802
+ * This is meant to be used by a {@link @fluidframework/container-definitions#IRuntimeFactory} to instantiate a container runtime.
803
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
804
+ * @returns A runtime which provides all the functionality necessary to bind with the loader layer via the {@link @fluidframework/container-definitions#IRuntime} interface and provide a runtime environment via the {@link @fluidframework/container-runtime-definitions#IContainerRuntime} interface.
805
+ * @legacy
806
+ * @alpha
807
+ */
808
+ export async function loadContainerRuntime(
809
+ params: LoadContainerRuntimeParams,
810
+ ): Promise<IContainerRuntime & IRuntime> {
811
+ return ContainerRuntime.loadRuntime(params);
812
+ }
813
+
781
814
  /**
782
815
  * Represents the runtime of the container. Contains helper functions/state of the container.
783
816
  * It will define the store level mappings.
@@ -1285,13 +1318,19 @@ export class ContainerRuntime
1285
1318
  private dirtyContainer: boolean;
1286
1319
  private emitDirtyDocumentEvent = true;
1287
1320
  private readonly disableAttachReorder: boolean | undefined;
1321
+ private readonly useDeltaManagerOpsProxy: boolean;
1288
1322
  private readonly closeSummarizerDelayMs: number;
1289
1323
  private readonly defaultTelemetrySignalSampleCount = 100;
1290
- private readonly _perfSignalData: IPerfSignalReport = {
1324
+ private readonly _signalTracking: IPerfSignalReport = {
1325
+ totalSignalsSentInLatencyWindow: 0,
1291
1326
  signalsLost: 0,
1292
- signalSequenceNumber: 0,
1327
+ signalsOutOfOrder: 0,
1328
+ signalsSentSinceLastLatencyMeasurement: 0,
1329
+ broadcastSignalSequenceNumber: 0,
1293
1330
  signalTimestamp: 0,
1331
+ roundTripSignalSequenceNumber: undefined,
1294
1332
  trackingSignalSequenceNumber: undefined,
1333
+ minimumTrackingSignalSequenceNumber: undefined,
1295
1334
  };
1296
1335
 
1297
1336
  /**
@@ -1303,6 +1342,7 @@ export class ContainerRuntime
1303
1342
  private readonly scheduleManager: ScheduleManager;
1304
1343
  private readonly blobManager: BlobManager;
1305
1344
  private readonly pendingStateManager: PendingStateManager;
1345
+ private readonly duplicateBatchDetector: DuplicateBatchDetector | undefined;
1306
1346
  private readonly outbox: Outbox;
1307
1347
  private readonly garbageCollector: IGarbageCollector;
1308
1348
 
@@ -1583,8 +1623,8 @@ export class ContainerRuntime
1583
1623
  );
1584
1624
 
1585
1625
  let outerDeltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>;
1586
- const useDeltaManagerOpsProxy =
1587
- this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") !== false;
1626
+ this.useDeltaManagerOpsProxy =
1627
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") === true;
1588
1628
  // The summarizerDeltaManager Proxy is used to lie to the summarizer to convince it is in the right state as a summarizer client.
1589
1629
  const summarizerDeltaManagerProxy = new DeltaManagerSummarizerProxy(
1590
1630
  this.innerDeltaManager,
@@ -1593,7 +1633,7 @@ export class ContainerRuntime
1593
1633
 
1594
1634
  // The DeltaManagerPendingOpsProxy is used to control the minimum sequence number
1595
1635
  // It allows us to lie to the layers below so that they can maintain enough local state for rebasing ops.
1596
- if (useDeltaManagerOpsProxy) {
1636
+ if (this.useDeltaManagerOpsProxy) {
1597
1637
  const pendingOpsDeltaManagerProxy = new DeltaManagerPendingOpsProxy(
1598
1638
  summarizerDeltaManagerProxy,
1599
1639
  this.pendingStateManager,
@@ -1636,6 +1676,13 @@ export class ContainerRuntime
1636
1676
  throw error;
1637
1677
  }
1638
1678
 
1679
+ // DuplicateBatchDetection is only enabled if Offline Load is enabled
1680
+ // It maintains a cache of all batchIds/sequenceNumbers within the collab window.
1681
+ // Don't waste resources doing so if not needed.
1682
+ if (this.offlineEnabled) {
1683
+ this.duplicateBatchDetector = new DuplicateBatchDetector();
1684
+ }
1685
+
1639
1686
  if (context.attachState === AttachState.Attached) {
1640
1687
  const maxSnapshotCacheDurationMs = this._storage?.policies?.maximumCacheDurationMs;
1641
1688
  if (
@@ -2199,13 +2246,9 @@ export class ContainerRuntime
2199
2246
  let childTree = snapshotTree;
2200
2247
  for (const part of pathParts) {
2201
2248
  if (hasIsolatedChannels) {
2202
- // TODO Why are we non null asserting here
2203
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2204
- childTree = childTree.trees[channelsTreeName]!;
2249
+ childTree = childTree?.trees[channelsTreeName];
2205
2250
  }
2206
- // TODO Why are we non null asserting here
2207
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2208
- childTree = childTree.trees[part]!;
2251
+ childTree = childTree?.trees[part];
2209
2252
  }
2210
2253
  return childTree;
2211
2254
  }
@@ -2257,9 +2300,7 @@ export class ContainerRuntime
2257
2300
  }
2258
2301
 
2259
2302
  if (id === blobManagerBasePath && requestParser.isLeaf(2)) {
2260
- // TODO why are we non null asserting here?
2261
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2262
- const blob = await this.blobManager.getBlob(requestParser.pathParts[1]!);
2303
+ const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
2263
2304
  return blob
2264
2305
  ? {
2265
2306
  status: 200,
@@ -2597,9 +2638,14 @@ export class ContainerRuntime
2597
2638
  this._connected = connected;
2598
2639
 
2599
2640
  if (!connected) {
2600
- this._perfSignalData.signalsLost = 0;
2601
- this._perfSignalData.signalTimestamp = 0;
2602
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
2641
+ this._signalTracking.signalsLost = 0;
2642
+ this._signalTracking.signalsOutOfOrder = 0;
2643
+ this._signalTracking.signalTimestamp = 0;
2644
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement = 0;
2645
+ this._signalTracking.totalSignalsSentInLatencyWindow = 0;
2646
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
2647
+ this._signalTracking.trackingSignalSequenceNumber = undefined;
2648
+ this._signalTracking.minimumTrackingSignalSequenceNumber = undefined;
2603
2649
  } else {
2604
2650
  assert(
2605
2651
  this.attachState === AttachState.Attached,
@@ -2665,197 +2711,220 @@ export class ContainerRuntime
2665
2711
  if (hasModernRuntimeMessageEnvelope) {
2666
2712
  // If the message has the modern message envelope, then process it here.
2667
2713
  // 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) {
2714
+ const inboundResult = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
2715
+ if (inboundResult === undefined) {
2670
2716
  // This means the incoming message is an incomplete part of a message or batch
2671
2717
  // and we need to process more messages before the rest of the system can understand it.
2672
2718
  return;
2673
2719
  }
2674
2720
 
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,
2678
- local,
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));
2721
+ if ("batchStart" in inboundResult) {
2722
+ const batchStart: BatchStartInfo = inboundResult.batchStart;
2723
+ const result = this.duplicateBatchDetector?.processInboundBatch(batchStart);
2724
+ if (result?.duplicate) {
2725
+ const error = new DataCorruptionError(
2726
+ "Duplicate batch - The same batch was sequenced twice",
2727
+ { batchId: batchStart.batchId },
2728
+ );
2729
+
2730
+ this.mc.logger.sendTelemetryEvent(
2731
+ {
2732
+ eventName: "DuplicateBatch",
2733
+ details: {
2734
+ batchId: batchStart.batchId,
2735
+ clientId: batchStart.clientId,
2736
+ batchStartCsn: batchStart.batchStartCsn,
2737
+ size: inboundResult.length,
2738
+ duplicateBatchSequenceNumber: result.otherSequenceNumber,
2739
+ ...extractSafePropertiesFromMessage(batchStart.keyMessage),
2740
+ },
2741
+ },
2742
+ error,
2743
+ );
2744
+ throw error;
2745
+ }
2693
2746
  }
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
- }),
2747
+
2748
+ let runtimeBatch: boolean = true;
2749
+ // Reach out to PendingStateManager, either to zip localOpMetadata into the *local* message list,
2750
+ // or to check to ensure the *remote* messages don't match the batchId of a pending local batch.
2751
+ // This latter case would indicate that the container has forked - two copies are trying to persist the same local changes.
2752
+ let messagesWithPendingState: {
2753
+ message: ISequencedDocumentMessage;
2754
+ localOpMetadata?: unknown;
2755
+ }[] = this.pendingStateManager.processInboundMessages(inboundResult, local);
2756
+
2757
+ if (inboundResult.type !== "fullBatch") {
2758
+ assert(
2759
+ messagesWithPendingState.length === 1,
2760
+ 0xa3d /* Partial batch should have exactly one message */,
2706
2761
  );
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
- }),
2762
+ }
2763
+
2764
+ if (messagesWithPendingState.length === 0) {
2765
+ assert(
2766
+ inboundResult.type === "fullBatch",
2767
+ 0xa3e /* Empty batch is always considered a full batch */,
2716
2768
  );
2769
+ /**
2770
+ * We need to process an empty batch, which will execute expected actions while processing even if there
2771
+ * are no inner runtime messages.
2772
+ *
2773
+ * Empty batches are produced by the outbox on resubmit when the resubmit flow resulted in no runtime
2774
+ * messages.
2775
+ * This can happen if changes from a remote client "cancel out" the pending changes being resubmitted by
2776
+ * this client. We submit an empty batch if "offline load" (aka rehydrating from stashed state) is
2777
+ * enabled, to ensure we account for this batch when comparing batchIds, checking for a forked container.
2778
+ * Otherwise, we would not realize this container has forked in the case where it did fork, and a batch
2779
+ * became empty but wasn't submitted as such.
2780
+ */
2781
+ messagesWithPendingState = [
2782
+ {
2783
+ message: inboundResult.batchStart.keyMessage,
2784
+ localOpMetadata: undefined,
2785
+ },
2786
+ ];
2787
+ // Empty batch message is a non-runtime message as it was generated by the op grouping manager.
2788
+ runtimeBatch = false;
2717
2789
  }
2790
+
2791
+ const locationInBatch: { batchStart: boolean; batchEnd: boolean } =
2792
+ inboundResult.type === "fullBatch"
2793
+ ? { batchStart: true, batchEnd: true }
2794
+ : inboundResult.type === "batchStartingMessage"
2795
+ ? { batchStart: true, batchEnd: false }
2796
+ : { batchStart: false, batchEnd: inboundResult.batchEnd === true };
2797
+
2798
+ this.processInboundMessages(
2799
+ messagesWithPendingState,
2800
+ locationInBatch,
2801
+ local,
2802
+ savedOp,
2803
+ runtimeBatch,
2804
+ );
2805
+ } else {
2806
+ this.processInboundMessages(
2807
+ [{ message: messageCopy, localOpMetadata: undefined }],
2808
+ { batchStart: true, batchEnd: true }, // Single message
2809
+ local,
2810
+ savedOp,
2811
+ isRuntimeMessage(messageCopy) /* runtimeBatch */,
2812
+ );
2813
+ }
2814
+
2815
+ if (local) {
2816
+ // If we have processed a local op, this means that the container is
2817
+ // making progress and we can reset the counter for how many times
2818
+ // we have consecutively replayed the pending states
2819
+ this.resetReconnectCount();
2718
2820
  }
2719
2821
  }
2720
2822
 
2721
2823
  private _processedClientSequenceNumber: number | undefined;
2722
2824
 
2723
2825
  /**
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
2826
+ * Processes inbound message(s). It calls schedule manager according to the messages' location in the batch.
2827
+ * @param messages - messages to process.
2828
+ * @param locationInBatch - Are we processing the start and/or end of a batch?
2829
+ * @param local - true if the messages were originally generated by the client receiving it.
2830
+ * @param savedOp - true if the message is a replayed saved op.
2831
+ * @param runtimeBatch - true if these are runtime messages.
2727
2832
  */
2728
- private processRuntimeMessage(
2729
- messageWithContext: MessageWithContext & { isRuntimeMessage: true },
2833
+ private processInboundMessages(
2834
+ messages: {
2835
+ message: ISequencedDocumentMessage;
2836
+ localOpMetadata?: unknown;
2837
+ }[],
2838
+ locationInBatch: { batchStart: boolean; batchEnd: boolean },
2839
+ local: boolean,
2840
+ savedOp: boolean | undefined,
2841
+ runtimeBatch: boolean,
2730
2842
  ) {
2731
- const { message, local, localOpMetadata } = messageWithContext;
2732
-
2733
- // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
2734
- // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
2735
- if (
2736
- this.deltaManager.minimumSequenceNumber <
2737
- messageWithContext.message.minimumSequenceNumber
2738
- ) {
2739
- messageWithContext.message.minimumSequenceNumber =
2740
- this.deltaManager.minimumSequenceNumber;
2843
+ if (locationInBatch.batchStart) {
2844
+ const firstMessage = messages[0]?.message;
2845
+ assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
2846
+ this.scheduleManager.batchBegin(firstMessage);
2741
2847
  }
2742
2848
 
2743
- // Surround the actual processing of the operation with messages to the schedule manager indicating
2744
- // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
2745
- // messages once a batch has been fully processed.
2746
- this.scheduleManager.beforeOpProcessing(message);
2747
-
2748
- this._processedClientSequenceNumber = message.clientSequenceNumber;
2749
-
2849
+ let error: unknown;
2750
2850
  try {
2751
- // RemoteMessageProcessor would have already reconstituted Chunked Ops into the original op type
2752
- assert(
2753
- message.type !== ContainerMessageType.ChunkedOp,
2754
- 0x93b /* we should never get here with chunked ops */,
2755
- );
2756
-
2757
- // If there are no more pending messages after processing a local message,
2758
- // the document is no longer dirty.
2759
- if (!this.hasPendingMessages()) {
2760
- this.updateDocumentDirtyState(false);
2761
- }
2762
-
2763
- this.validateAndProcessRuntimeMessage(messageWithContext, localOpMetadata);
2764
-
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
- }
2851
+ messages.forEach(({ message, localOpMetadata }) => {
2852
+ this.ensureNoDataModelChanges(() => {
2853
+ if (runtimeBatch) {
2854
+ this.validateAndProcessRuntimeMessage({
2855
+ message: message as InboundSequencedContainerRuntimeMessage,
2856
+ local,
2857
+ savedOp,
2858
+ localOpMetadata,
2859
+ });
2860
+ } else {
2861
+ this.observeNonRuntimeMessage(message);
2862
+ }
2863
+ });
2864
+ });
2775
2865
  } catch (e) {
2776
- this.scheduleManager.afterOpProcessing(e, message);
2777
- throw e;
2866
+ error = e;
2867
+ throw error;
2868
+ } finally {
2869
+ if (locationInBatch.batchEnd) {
2870
+ const lastMessage = messages[messages.length - 1]?.message;
2871
+ assert(lastMessage !== undefined, 0xa32 /* Batch must have at least one message */);
2872
+ this.scheduleManager.batchEnd(error, lastMessage);
2873
+ }
2778
2874
  }
2779
2875
  }
2780
2876
 
2781
2877
  /**
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.
2878
+ * Observes messages that are not intended for the runtime layer, updating/notifying Runtime systems as needed.
2879
+ * @param message - non-runtime message to process.
2785
2880
  */
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;
2881
+ private observeNonRuntimeMessage(message: ISequencedDocumentMessage) {
2882
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
2883
+ if (this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber) {
2884
+ message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2885
+ }
2886
+
2887
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
2888
+
2889
+ // If there are no more pending messages after processing a local message,
2890
+ // the document is no longer dirty.
2791
2891
  if (!this.hasPendingMessages()) {
2792
2892
  this.updateDocumentDirtyState(false);
2793
2893
  }
2794
- this.emit("batchEnd", undefined, { sequenceNumber });
2795
- if (local) {
2796
- this.resetReconnectCount();
2797
- }
2894
+
2895
+ this.emit("op", message, false /* runtimeMessage */);
2798
2896
  }
2799
2897
 
2800
2898
  /**
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
2899
+ * Assuming the given message is also a TypedContainerRuntimeMessage,
2900
+ * checks its type and dispatches the message to the appropriate handler in the runtime.
2901
+ * Throws a DataProcessingError if the message looks like but doesn't conform to a known TypedContainerRuntimeMessage type.
2803
2902
  */
2804
- private observeNonRuntimeMessage(
2805
- messageWithContext: MessageWithContext & { isRuntimeMessage: false },
2806
- ) {
2807
- const { message, local } = messageWithContext;
2808
2903
 
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
2904
+ private validateAndProcessRuntimeMessage(messageWithContext: {
2905
+ message: InboundSequencedContainerRuntimeMessage;
2906
+ local: boolean;
2907
+ savedOp?: boolean;
2908
+ localOpMetadata?: unknown;
2909
+ }): void {
2910
+ const { local, message, savedOp, localOpMetadata } = messageWithContext;
2911
+
2912
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
2811
2913
  if (
2812
- this.deltaManager.minimumSequenceNumber <
2813
- messageWithContext.message.minimumSequenceNumber
2914
+ this.useDeltaManagerOpsProxy &&
2915
+ this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber
2814
2916
  ) {
2815
- messageWithContext.message.minimumSequenceNumber =
2816
- this.deltaManager.minimumSequenceNumber;
2917
+ message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
2817
2918
  }
2818
2919
 
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
2920
  this._processedClientSequenceNumber = message.clientSequenceNumber;
2825
2921
 
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);
2834
-
2835
- this.scheduleManager.afterOpProcessing(undefined, message);
2836
-
2837
- if (local) {
2838
- // If we have processed a local op, this means that the container is
2839
- // making progress and we can reset the counter for how many times
2840
- // we have consecutively replayed the pending states
2841
- this.resetReconnectCount();
2842
- }
2843
- } catch (e) {
2844
- this.scheduleManager.afterOpProcessing(e, message);
2845
- throw e;
2922
+ // If there are no more pending messages after processing a local message,
2923
+ // the document is no longer dirty.
2924
+ if (!this.hasPendingMessages()) {
2925
+ this.updateDocumentDirtyState(false);
2846
2926
  }
2847
- }
2848
2927
 
2849
- /**
2850
- * Assuming the given message is also a TypedContainerRuntimeMessage,
2851
- * checks its type and dispatches the message to the appropriate handler in the runtime.
2852
- * Throws a DataProcessingError if the message looks like but doesn't conform to a known TypedContainerRuntimeMessage type.
2853
- */
2854
- private validateAndProcessRuntimeMessage(
2855
- messageWithContext: MessageWithContext & { isRuntimeMessage: true },
2856
- localOpMetadata: unknown,
2857
- ): void {
2858
- const { local, message, savedOp } = messageWithContext;
2859
2928
  switch (message.type) {
2860
2929
  case ContainerMessageType.Attach:
2861
2930
  case ContainerMessageType.Alias:
@@ -2929,22 +2998,27 @@ export class ContainerRuntime
2929
2998
  }
2930
2999
  }
2931
3000
  }
3001
+
3002
+ this.emit("op", message, true /* runtimeMessage */);
2932
3003
  }
2933
3004
 
2934
3005
  /**
2935
3006
  * Emits the Signal event and update the perf signal data.
2936
- * @param clientSignalSequenceNumber - is the client signal sequence number to be uploaded.
2937
3007
  */
2938
- private sendSignalTelemetryEvent(clientSignalSequenceNumber: number) {
2939
- const duration = Date.now() - this._perfSignalData.signalTimestamp;
3008
+ private sendSignalTelemetryEvent() {
3009
+ const duration = Date.now() - this._signalTracking.signalTimestamp;
2940
3010
  this.mc.logger.sendPerformanceEvent({
2941
3011
  eventName: "SignalLatency",
2942
- duration,
2943
- signalsLost: this._perfSignalData.signalsLost,
3012
+ duration, // Roundtrip duration of the tracked signal in milliseconds.
3013
+ signalsSent: this._signalTracking.totalSignalsSentInLatencyWindow, // Signals sent since the last logged SignalLatency event.
3014
+ signalsLost: this._signalTracking.signalsLost, // Signals lost since the last logged SignalLatency event.
3015
+ outOfOrderSignals: this._signalTracking.signalsOutOfOrder, // Out of order signals since the last logged SignalLatency event.
3016
+ reconnectCount: this.consecutiveReconnects, // Container reconnect count.
2944
3017
  });
2945
-
2946
- this._perfSignalData.signalsLost = 0;
2947
- this._perfSignalData.signalTimestamp = 0;
3018
+ this._signalTracking.signalsLost = 0;
3019
+ this._signalTracking.signalsOutOfOrder = 0;
3020
+ this._signalTracking.signalTimestamp = 0;
3021
+ this._signalTracking.totalSignalsSentInLatencyWindow = 0;
2948
3022
  }
2949
3023
 
2950
3024
  public processSignal(message: ISignalMessage, local: boolean) {
@@ -2953,33 +3027,69 @@ export class ContainerRuntime
2953
3027
  clientId: message.clientId,
2954
3028
  content: envelope.contents.content,
2955
3029
  type: envelope.contents.type,
3030
+ targetClientId: message.targetClientId,
2956
3031
  };
2957
3032
 
2958
- // Only collect signal telemetry for messages sent by the current client.
2959
- if (message.clientId === this.clientId && this.connected) {
2960
- // Check to see if the signal was lost.
3033
+ // Only collect signal telemetry for broadcast messages sent by the current client.
3034
+ if (
3035
+ message.clientId === this.clientId &&
3036
+ this.connected &&
3037
+ envelope.clientBroadcastSignalSequenceNumber !== undefined
3038
+ ) {
2961
3039
  if (
2962
- this._perfSignalData.trackingSignalSequenceNumber !== undefined &&
2963
- envelope.clientSignalSequenceNumber > this._perfSignalData.trackingSignalSequenceNumber
3040
+ this._signalTracking.trackingSignalSequenceNumber !== undefined &&
3041
+ this._signalTracking.minimumTrackingSignalSequenceNumber !== undefined
2964
3042
  ) {
2965
- this._perfSignalData.signalsLost++;
2966
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
2967
- this.mc.logger.sendErrorEvent({
2968
- eventName: "SignalLost",
2969
- type: envelope.contents.type,
2970
- signalsLost: this._perfSignalData.signalsLost,
2971
- trackingSequenceNumber: this._perfSignalData.trackingSignalSequenceNumber,
2972
- clientSignalSequenceNumber: envelope.clientSignalSequenceNumber,
2973
- });
2974
- } else if (
2975
- envelope.clientSignalSequenceNumber ===
2976
- this._perfSignalData.trackingSignalSequenceNumber
2977
- ) {
2978
- // only logging for the first connection and the trackingSignalSequenceNUmber.
2979
- if (this.consecutiveReconnects === 0) {
2980
- this.sendSignalTelemetryEvent(envelope.clientSignalSequenceNumber);
3043
+ if (
3044
+ envelope.clientBroadcastSignalSequenceNumber >=
3045
+ this._signalTracking.trackingSignalSequenceNumber
3046
+ ) {
3047
+ // Calculate the number of signals lost and log the event.
3048
+ const signalsLost =
3049
+ envelope.clientBroadcastSignalSequenceNumber -
3050
+ this._signalTracking.trackingSignalSequenceNumber;
3051
+ if (signalsLost > 0) {
3052
+ this._signalTracking.signalsLost += signalsLost;
3053
+ this.mc.logger.sendErrorEvent({
3054
+ eventName: "SignalLost",
3055
+ signalsLost, // Number of lost signals detected.
3056
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3057
+ clientBroadcastSignalSequenceNumber:
3058
+ envelope.clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
3059
+ });
3060
+ }
3061
+ // Update the tracking signal sequence number to the next expected signal in the sequence.
3062
+ this._signalTracking.trackingSignalSequenceNumber =
3063
+ envelope.clientBroadcastSignalSequenceNumber + 1;
3064
+ } else if (
3065
+ envelope.clientBroadcastSignalSequenceNumber >=
3066
+ this._signalTracking.minimumTrackingSignalSequenceNumber
3067
+ ) {
3068
+ this._signalTracking.signalsOutOfOrder++;
3069
+ this.mc.logger.sendTelemetryEvent({
3070
+ eventName: "SignalOutOfOrder",
3071
+ type: envelope.contents.type, // Type of signal that was received out of order.
3072
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
3073
+ clientBroadcastSignalSequenceNumber: envelope.clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
3074
+ });
3075
+ }
3076
+ if (
3077
+ this._signalTracking.roundTripSignalSequenceNumber !== undefined &&
3078
+ envelope.clientBroadcastSignalSequenceNumber >=
3079
+ this._signalTracking.roundTripSignalSequenceNumber
3080
+ ) {
3081
+ if (
3082
+ envelope.clientBroadcastSignalSequenceNumber ===
3083
+ this._signalTracking.roundTripSignalSequenceNumber
3084
+ ) {
3085
+ // Latency tracked signal has been received.
3086
+ // We now log the roundtrip duration of the tracked signal.
3087
+ // This telemetry event also logs metrics for signals sent, signals lost, and out of order signals received.
3088
+ // These metrics are reset after logging the telemetry event.
3089
+ this.sendSignalTelemetryEvent();
3090
+ }
3091
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
2981
3092
  }
2982
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
2983
3093
  }
2984
3094
  }
2985
3095
 
@@ -3227,21 +3337,45 @@ export class ContainerRuntime
3227
3337
  address: string | undefined,
3228
3338
  type: string,
3229
3339
  content: any,
3340
+ targetClientId?: string,
3230
3341
  ): ISignalEnvelope {
3231
- const newSequenceNumber = ++this._perfSignalData.signalSequenceNumber;
3232
3342
  const newEnvelope: ISignalEnvelope = {
3233
3343
  address,
3234
- clientSignalSequenceNumber: newSequenceNumber,
3235
3344
  contents: { type, content },
3236
3345
  };
3237
3346
 
3238
- // We should not track any signals in case we already have a tracking number.
3239
- if (
3240
- newSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
3241
- this._perfSignalData.trackingSignalSequenceNumber === undefined
3242
- ) {
3243
- this._perfSignalData.signalTimestamp = Date.now();
3244
- this._perfSignalData.trackingSignalSequenceNumber = newSequenceNumber;
3347
+ const isBroadcastSignal = targetClientId === undefined;
3348
+
3349
+ if (isBroadcastSignal) {
3350
+ const clientBroadcastSignalSequenceNumber = ++this._signalTracking
3351
+ .broadcastSignalSequenceNumber;
3352
+ newEnvelope.clientBroadcastSignalSequenceNumber = clientBroadcastSignalSequenceNumber;
3353
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement++;
3354
+
3355
+ if (
3356
+ this._signalTracking.minimumTrackingSignalSequenceNumber === undefined ||
3357
+ this._signalTracking.trackingSignalSequenceNumber === undefined
3358
+ ) {
3359
+ // Signal monitoring window is undefined
3360
+ // Initialize tracking to expect the next signal sent by the connected client.
3361
+ this._signalTracking.minimumTrackingSignalSequenceNumber =
3362
+ clientBroadcastSignalSequenceNumber;
3363
+ this._signalTracking.trackingSignalSequenceNumber =
3364
+ clientBroadcastSignalSequenceNumber;
3365
+ }
3366
+
3367
+ // We should not track the round trip of a new signal in the case we are already tracking one.
3368
+ if (
3369
+ clientBroadcastSignalSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
3370
+ this._signalTracking.roundTripSignalSequenceNumber === undefined
3371
+ ) {
3372
+ this._signalTracking.signalTimestamp = Date.now();
3373
+ this._signalTracking.roundTripSignalSequenceNumber =
3374
+ clientBroadcastSignalSequenceNumber;
3375
+ this._signalTracking.totalSignalsSentInLatencyWindow +=
3376
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement;
3377
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement = 0;
3378
+ }
3245
3379
  }
3246
3380
 
3247
3381
  return newEnvelope;
@@ -3252,10 +3386,21 @@ export class ContainerRuntime
3252
3386
  * @param type - Type of the signal.
3253
3387
  * @param content - Content of the signal. Should be a JSON serializable object or primitive.
3254
3388
  * @param targetClientId - When specified, the signal is only sent to the provided client id.
3389
+ *
3390
+ * @remarks
3391
+ *
3392
+ * The `targetClientId` parameter here is currently intended for internal testing purposes only.
3393
+ * Support for this option at container runtime is planned to be deprecated in the future.
3394
+ *
3255
3395
  */
3256
3396
  public submitSignal(type: string, content: unknown, targetClientId?: string) {
3257
3397
  this.verifyNotClosed();
3258
- const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content);
3398
+ const envelope = this.createNewSignalEnvelope(
3399
+ undefined /* address */,
3400
+ type,
3401
+ content,
3402
+ targetClientId,
3403
+ );
3259
3404
  return this.submitSignalFn(envelope, targetClientId);
3260
3405
  }
3261
3406
 
@@ -3812,9 +3957,7 @@ export class ContainerRuntime
3812
3957
  // Counting dataStores and handles
3813
3958
  // Because handles are unchanged dataStores in the current logic,
3814
3959
  // summarized dataStore count is total dataStore count minus handle count
3815
- // TODO why are we non null asserting here
3816
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3817
- const dataStoreTree = summaryTree.tree[channelsTreeName]!;
3960
+ const dataStoreTree = summaryTree.tree[channelsTreeName];
3818
3961
 
3819
3962
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
3820
3963
  const handleCount = Object.values(dataStoreTree.tree).filter(
@@ -4226,6 +4369,12 @@ export class ContainerRuntime
4226
4369
  }
4227
4370
  }
4228
4371
 
4372
+ /**
4373
+ * Resubmits each message in the batch, and then flushes the outbox.
4374
+ *
4375
+ * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
4376
+ * for correlation to detect container forking.
4377
+ */
4229
4378
  private reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId) {
4230
4379
  this.orderSequentially(() => {
4231
4380
  for (const message of batch) {