@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
@@ -4,7 +4,7 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.ContainerRuntime = exports.makeLegacySendBatchFn = exports.getDeviceSpec = exports.agentSchedulerId = exports.isRuntimeMessage = exports.defaultPendingOpsRetryDelayMs = exports.defaultPendingOpsWaitTimeoutMs = exports.disabledCompressionConfig = exports.CompressionAlgorithms = exports.defaultRuntimeHeaderData = exports.InactiveResponseHeaderKey = exports.TombstoneResponseHeaderKey = exports.DeletedResponseHeaderKey = exports.DefaultSummaryConfiguration = void 0;
7
+ exports.ContainerRuntime = exports.loadContainerRuntime = exports.makeLegacySendBatchFn = exports.getDeviceSpec = exports.agentSchedulerId = exports.isRuntimeMessage = exports.defaultPendingOpsRetryDelayMs = exports.defaultPendingOpsWaitTimeoutMs = exports.disabledCompressionConfig = exports.CompressionAlgorithms = exports.defaultRuntimeHeaderData = exports.InactiveResponseHeaderKey = exports.TombstoneResponseHeaderKey = exports.DeletedResponseHeaderKey = exports.DefaultSummaryConfiguration = void 0;
8
8
  const client_utils_1 = require("@fluid-internal/client-utils");
9
9
  const container_definitions_1 = require("@fluidframework/container-definitions");
10
10
  const internal_1 = require("@fluidframework/container-definitions/internal");
@@ -232,15 +232,26 @@ function lastMessageFromMetadata(metadata) {
232
232
  * We only want to log this once, to avoid spamming telemetry if we are wrong and these cases are hit commonly.
233
233
  */
234
234
  let getSingleUseLegacyLogCallback = (logger, type) => {
235
- // We only want to log this once per ContainerRuntime instance, to avoid spamming telemetry.
236
- getSingleUseLegacyLogCallback = () => () => { };
237
235
  return (codePath) => {
238
236
  logger.sendTelemetryEvent({
239
237
  eventName: "LegacyMessageFormat",
240
238
  details: { codePath, type },
241
239
  });
240
+ // Now that we've logged, prevent future logging (globally).
241
+ getSingleUseLegacyLogCallback = () => () => { };
242
242
  };
243
243
  };
244
+ /**
245
+ * This is meant to be used by a {@link @fluidframework/container-definitions#IRuntimeFactory} to instantiate a container runtime.
246
+ * @param params - An object which specifies all required and optional params necessary to instantiate a runtime.
247
+ * @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.
248
+ * @legacy
249
+ * @alpha
250
+ */
251
+ async function loadContainerRuntime(params) {
252
+ return ContainerRuntime.loadRuntime(params);
253
+ }
254
+ exports.loadContainerRuntime = loadContainerRuntime;
244
255
  /**
245
256
  * Represents the runtime of the container. Contains helper functions/state of the container.
246
257
  * It will define the store level mappings.
@@ -592,11 +603,16 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
592
603
  this._disposed = false;
593
604
  this.emitDirtyDocumentEvent = true;
594
605
  this.defaultTelemetrySignalSampleCount = 100;
595
- this._perfSignalData = {
606
+ this._signalTracking = {
607
+ totalSignalsSentInLatencyWindow: 0,
596
608
  signalsLost: 0,
597
- signalSequenceNumber: 0,
609
+ signalsOutOfOrder: 0,
610
+ signalsSentSinceLastLatencyMeasurement: 0,
611
+ broadcastSignalSequenceNumber: 0,
598
612
  signalTimestamp: 0,
613
+ roundTripSignalSequenceNumber: undefined,
599
614
  trackingSignalSequenceNumber: undefined,
615
+ minimumTrackingSignalSequenceNumber: undefined,
600
616
  };
601
617
  /**
602
618
  * It a cache for holding mapping for loading groupIds with its snapshot from the service. Add expiry policy of 1 minute.
@@ -704,13 +720,14 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
704
720
  isAttached: () => this.attachState !== container_definitions_1.AttachState.Detached,
705
721
  }, pendingRuntimeState?.pending, this.logger);
706
722
  let outerDeltaManager;
707
- const useDeltaManagerOpsProxy = this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") !== false;
723
+ this.useDeltaManagerOpsProxy =
724
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.DeltaManagerOpsProxy") === true;
708
725
  // The summarizerDeltaManager Proxy is used to lie to the summarizer to convince it is in the right state as a summarizer client.
709
726
  const summarizerDeltaManagerProxy = new deltaManagerProxies_js_1.DeltaManagerSummarizerProxy(this.innerDeltaManager);
710
727
  outerDeltaManager = summarizerDeltaManagerProxy;
711
728
  // The DeltaManagerPendingOpsProxy is used to control the minimum sequence number
712
729
  // It allows us to lie to the layers below so that they can maintain enough local state for rebasing ops.
713
- if (useDeltaManagerOpsProxy) {
730
+ if (this.useDeltaManagerOpsProxy) {
714
731
  const pendingOpsDeltaManagerProxy = new deltaManagerProxies_js_1.DeltaManagerPendingOpsProxy(summarizerDeltaManagerProxy, this.pendingStateManager);
715
732
  outerDeltaManager = pendingOpsDeltaManagerProxy;
716
733
  }
@@ -741,6 +758,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
741
758
  this.closeFn(error);
742
759
  throw error;
743
760
  }
761
+ // DuplicateBatchDetection is only enabled if Offline Load is enabled
762
+ // It maintains a cache of all batchIds/sequenceNumbers within the collab window.
763
+ // Don't waste resources doing so if not needed.
764
+ if (this.offlineEnabled) {
765
+ this.duplicateBatchDetector = new index_js_3.DuplicateBatchDetector();
766
+ }
744
767
  if (context.attachState === container_definitions_1.AttachState.Attached) {
745
768
  const maxSnapshotCacheDurationMs = this._storage?.policies?.maximumCacheDurationMs;
746
769
  if (maxSnapshotCacheDurationMs !== undefined &&
@@ -1138,13 +1161,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1138
1161
  let childTree = snapshotTree;
1139
1162
  for (const part of pathParts) {
1140
1163
  if (hasIsolatedChannels) {
1141
- // TODO Why are we non null asserting here
1142
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1143
- childTree = childTree.trees[internal_5.channelsTreeName];
1164
+ childTree = childTree?.trees[internal_5.channelsTreeName];
1144
1165
  }
1145
- // TODO Why are we non null asserting here
1146
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1147
- childTree = childTree.trees[part];
1166
+ childTree = childTree?.trees[part];
1148
1167
  }
1149
1168
  return childTree;
1150
1169
  }
@@ -1191,8 +1210,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1191
1210
  return this.resolveHandle(requestParser.createSubRequest(1));
1192
1211
  }
1193
1212
  if (id === index_js_1.blobManagerBasePath && requestParser.isLeaf(2)) {
1194
- // TODO why are we non null asserting here?
1195
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1196
1213
  const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
1197
1214
  return blob
1198
1215
  ? {
@@ -1465,9 +1482,14 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1465
1482
  }
1466
1483
  this._connected = connected;
1467
1484
  if (!connected) {
1468
- this._perfSignalData.signalsLost = 0;
1469
- this._perfSignalData.signalTimestamp = 0;
1470
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
1485
+ this._signalTracking.signalsLost = 0;
1486
+ this._signalTracking.signalsOutOfOrder = 0;
1487
+ this._signalTracking.signalTimestamp = 0;
1488
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement = 0;
1489
+ this._signalTracking.totalSignalsSentInLatencyWindow = 0;
1490
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
1491
+ this._signalTracking.trackingSignalSequenceNumber = undefined;
1492
+ this._signalTracking.minimumTrackingSignalSequenceNumber = undefined;
1471
1493
  }
1472
1494
  else {
1473
1495
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
@@ -1514,158 +1536,159 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1514
1536
  if (hasModernRuntimeMessageEnvelope) {
1515
1537
  // If the message has the modern message envelope, then process it here.
1516
1538
  // Here we unpack the message (decompress, unchunk, and/or ungroup) into a batch of messages with ContainerMessageType
1517
- const inboundBatch = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
1518
- if (inboundBatch === undefined) {
1539
+ const inboundResult = this.remoteMessageProcessor.process(messageCopy, logLegacyCase);
1540
+ if (inboundResult === undefined) {
1519
1541
  // This means the incoming message is an incomplete part of a message or batch
1520
1542
  // and we need to process more messages before the rest of the system can understand it.
1521
1543
  return;
1522
1544
  }
1523
- // Reach out to PendingStateManager to zip localOpMetadata into the message list if it's a local batch
1524
- const messagesWithPendingState = this.pendingStateManager.processInboundBatch(inboundBatch, local);
1525
- if (messagesWithPendingState.length > 0) {
1526
- messagesWithPendingState.forEach(({ message, localOpMetadata }) => {
1527
- const msg = {
1528
- message,
1529
- local,
1530
- isRuntimeMessage: true,
1531
- savedOp,
1532
- localOpMetadata,
1533
- };
1534
- this.ensureNoDataModelChanges(() => this.processRuntimeMessage(msg));
1535
- });
1545
+ if ("batchStart" in inboundResult) {
1546
+ const batchStart = inboundResult.batchStart;
1547
+ const result = this.duplicateBatchDetector?.processInboundBatch(batchStart);
1548
+ if (result?.duplicate) {
1549
+ const error = new internal_7.DataCorruptionError("Duplicate batch - The same batch was sequenced twice", { batchId: batchStart.batchId });
1550
+ this.mc.logger.sendTelemetryEvent({
1551
+ eventName: "DuplicateBatch",
1552
+ details: {
1553
+ batchId: batchStart.batchId,
1554
+ clientId: batchStart.clientId,
1555
+ batchStartCsn: batchStart.batchStartCsn,
1556
+ size: inboundResult.length,
1557
+ duplicateBatchSequenceNumber: result.otherSequenceNumber,
1558
+ ...(0, internal_7.extractSafePropertiesFromMessage)(batchStart.keyMessage),
1559
+ },
1560
+ }, error);
1561
+ throw error;
1562
+ }
1536
1563
  }
1537
- else {
1538
- this.ensureNoDataModelChanges(() => this.processEmptyBatch(inboundBatch, local));
1564
+ let runtimeBatch = true;
1565
+ // Reach out to PendingStateManager, either to zip localOpMetadata into the *local* message list,
1566
+ // or to check to ensure the *remote* messages don't match the batchId of a pending local batch.
1567
+ // This latter case would indicate that the container has forked - two copies are trying to persist the same local changes.
1568
+ let messagesWithPendingState = this.pendingStateManager.processInboundMessages(inboundResult, local);
1569
+ if (inboundResult.type !== "fullBatch") {
1570
+ (0, internal_2.assert)(messagesWithPendingState.length === 1, 0xa3d /* Partial batch should have exactly one message */);
1571
+ }
1572
+ if (messagesWithPendingState.length === 0) {
1573
+ (0, internal_2.assert)(inboundResult.type === "fullBatch", 0xa3e /* Empty batch is always considered a full batch */);
1574
+ /**
1575
+ * We need to process an empty batch, which will execute expected actions while processing even if there
1576
+ * are no inner runtime messages.
1577
+ *
1578
+ * Empty batches are produced by the outbox on resubmit when the resubmit flow resulted in no runtime
1579
+ * messages.
1580
+ * This can happen if changes from a remote client "cancel out" the pending changes being resubmitted by
1581
+ * this client. We submit an empty batch if "offline load" (aka rehydrating from stashed state) is
1582
+ * enabled, to ensure we account for this batch when comparing batchIds, checking for a forked container.
1583
+ * Otherwise, we would not realize this container has forked in the case where it did fork, and a batch
1584
+ * became empty but wasn't submitted as such.
1585
+ */
1586
+ messagesWithPendingState = [
1587
+ {
1588
+ message: inboundResult.batchStart.keyMessage,
1589
+ localOpMetadata: undefined,
1590
+ },
1591
+ ];
1592
+ // Empty batch message is a non-runtime message as it was generated by the op grouping manager.
1593
+ runtimeBatch = false;
1539
1594
  }
1595
+ const locationInBatch = inboundResult.type === "fullBatch"
1596
+ ? { batchStart: true, batchEnd: true }
1597
+ : inboundResult.type === "batchStartingMessage"
1598
+ ? { batchStart: true, batchEnd: false }
1599
+ : { batchStart: false, batchEnd: inboundResult.batchEnd === true };
1600
+ this.processInboundMessages(messagesWithPendingState, locationInBatch, local, savedOp, runtimeBatch);
1540
1601
  }
1541
1602
  else {
1542
- // Check if message.type is one of values in ContainerMessageType
1543
- // eslint-disable-next-line import/no-deprecated
1544
- if (isRuntimeMessage(messageCopy)) {
1545
- // Legacy op received
1546
- this.ensureNoDataModelChanges(() => this.processRuntimeMessage({
1547
- message: messageCopy,
1548
- local,
1549
- isRuntimeMessage: true,
1550
- savedOp,
1551
- }));
1552
- }
1553
- else {
1554
- // A non container runtime message (like other system ops - join, ack, leave, nack etc.)
1555
- this.ensureNoDataModelChanges(() => this.observeNonRuntimeMessage({
1556
- message: messageCopy,
1557
- local,
1558
- isRuntimeMessage: false,
1559
- savedOp,
1560
- }));
1561
- }
1603
+ this.processInboundMessages([{ message: messageCopy, localOpMetadata: undefined }], { batchStart: true, batchEnd: true }, // Single message
1604
+ local, savedOp, isRuntimeMessage(messageCopy) /* runtimeBatch */);
1605
+ }
1606
+ if (local) {
1607
+ // If we have processed a local op, this means that the container is
1608
+ // making progress and we can reset the counter for how many times
1609
+ // we have consecutively replayed the pending states
1610
+ this.resetReconnectCount();
1562
1611
  }
1563
1612
  }
1564
1613
  /**
1565
- * Processes messages that are intended for the runtime layer to process.
1566
- * It redirects the message to the correct subsystem for processing, and implement other side effects
1567
- * @param messageWithContext - message to process with additional context and isRuntimeMessage prop as true
1614
+ * Processes inbound message(s). It calls schedule manager according to the messages' location in the batch.
1615
+ * @param messages - messages to process.
1616
+ * @param locationInBatch - Are we processing the start and/or end of a batch?
1617
+ * @param local - true if the messages were originally generated by the client receiving it.
1618
+ * @param savedOp - true if the message is a replayed saved op.
1619
+ * @param runtimeBatch - true if these are runtime messages.
1568
1620
  */
1569
- processRuntimeMessage(messageWithContext) {
1570
- const { message, local, localOpMetadata } = messageWithContext;
1571
- // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
1572
- // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
1573
- if (this.deltaManager.minimumSequenceNumber <
1574
- messageWithContext.message.minimumSequenceNumber) {
1575
- messageWithContext.message.minimumSequenceNumber =
1576
- this.deltaManager.minimumSequenceNumber;
1577
- }
1578
- // Surround the actual processing of the operation with messages to the schedule manager indicating
1579
- // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1580
- // messages once a batch has been fully processed.
1581
- this.scheduleManager.beforeOpProcessing(message);
1582
- this._processedClientSequenceNumber = message.clientSequenceNumber;
1621
+ processInboundMessages(messages, locationInBatch, local, savedOp, runtimeBatch) {
1622
+ if (locationInBatch.batchStart) {
1623
+ const firstMessage = messages[0]?.message;
1624
+ (0, internal_2.assert)(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
1625
+ this.scheduleManager.batchBegin(firstMessage);
1626
+ }
1627
+ let error;
1583
1628
  try {
1584
- // RemoteMessageProcessor would have already reconstituted Chunked Ops into the original op type
1585
- (0, internal_2.assert)(message.type !== messageTypes_js_1.ContainerMessageType.ChunkedOp, 0x93b /* we should never get here with chunked ops */);
1586
- // If there are no more pending messages after processing a local message,
1587
- // the document is no longer dirty.
1588
- if (!this.hasPendingMessages()) {
1589
- this.updateDocumentDirtyState(false);
1590
- }
1591
- this.validateAndProcessRuntimeMessage(messageWithContext, localOpMetadata);
1592
- this.emit("op", message, messageWithContext.isRuntimeMessage);
1593
- this.scheduleManager.afterOpProcessing(undefined, message);
1594
- if (local) {
1595
- // If we have processed a local op, this means that the container is
1596
- // making progress and we can reset the counter for how many times
1597
- // we have consecutively replayed the pending states
1598
- this.resetReconnectCount();
1599
- }
1629
+ messages.forEach(({ message, localOpMetadata }) => {
1630
+ this.ensureNoDataModelChanges(() => {
1631
+ if (runtimeBatch) {
1632
+ this.validateAndProcessRuntimeMessage({
1633
+ message: message,
1634
+ local,
1635
+ savedOp,
1636
+ localOpMetadata,
1637
+ });
1638
+ }
1639
+ else {
1640
+ this.observeNonRuntimeMessage(message);
1641
+ }
1642
+ });
1643
+ });
1600
1644
  }
1601
1645
  catch (e) {
1602
- this.scheduleManager.afterOpProcessing(e, message);
1603
- throw e;
1604
- }
1605
- }
1606
- /**
1607
- * Process an empty batch, which will execute expected actions while processing even if there are no messages.
1608
- * This is a separate function because the processCore function expects at least one message to process.
1609
- * It is expected to happen only when the outbox produces an empty batch due to a resubmit flow.
1610
- */
1611
- processEmptyBatch(emptyBatch, local) {
1612
- const { emptyBatchSequenceNumber: sequenceNumber, batchStartCsn } = emptyBatch;
1613
- (0, internal_2.assert)(sequenceNumber !== undefined, 0x9fa /* emptyBatchSequenceNumber must be defined */);
1614
- this.emit("batchBegin", { sequenceNumber });
1615
- this._processedClientSequenceNumber = batchStartCsn;
1616
- if (!this.hasPendingMessages()) {
1617
- this.updateDocumentDirtyState(false);
1646
+ error = e;
1647
+ throw error;
1618
1648
  }
1619
- this.emit("batchEnd", undefined, { sequenceNumber });
1620
- if (local) {
1621
- this.resetReconnectCount();
1649
+ finally {
1650
+ if (locationInBatch.batchEnd) {
1651
+ const lastMessage = messages[messages.length - 1]?.message;
1652
+ (0, internal_2.assert)(lastMessage !== undefined, 0xa32 /* Batch must have at least one message */);
1653
+ this.scheduleManager.batchEnd(error, lastMessage);
1654
+ }
1622
1655
  }
1623
1656
  }
1624
1657
  /**
1625
1658
  * Observes messages that are not intended for the runtime layer, updating/notifying Runtime systems as needed.
1626
- * @param messageWithContext - non-runtime messages to process with additional context and isRuntimeMessage prop as false
1659
+ * @param message - non-runtime message to process.
1627
1660
  */
1628
- observeNonRuntimeMessage(messageWithContext) {
1629
- const { message, local } = messageWithContext;
1630
- // Intercept to reduce minimum sequence number to the delta manager's minimum sequence number.
1631
- // Sequence numbers are not guaranteed to follow any sort of order. Re-entrancy is one of those situations
1632
- if (this.deltaManager.minimumSequenceNumber <
1633
- messageWithContext.message.minimumSequenceNumber) {
1634
- messageWithContext.message.minimumSequenceNumber =
1635
- this.deltaManager.minimumSequenceNumber;
1636
- }
1637
- // Surround the actual processing of the operation with messages to the schedule manager indicating
1638
- // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1639
- // messages once a batch has been fully processed.
1640
- this.scheduleManager.beforeOpProcessing(message);
1641
- this._processedClientSequenceNumber = message.clientSequenceNumber;
1642
- try {
1643
- // If there are no more pending messages after processing a local message,
1644
- // the document is no longer dirty.
1645
- if (!this.hasPendingMessages()) {
1646
- this.updateDocumentDirtyState(false);
1647
- }
1648
- this.emit("op", message, messageWithContext.isRuntimeMessage);
1649
- this.scheduleManager.afterOpProcessing(undefined, message);
1650
- if (local) {
1651
- // If we have processed a local op, this means that the container is
1652
- // making progress and we can reset the counter for how many times
1653
- // we have consecutively replayed the pending states
1654
- this.resetReconnectCount();
1655
- }
1661
+ observeNonRuntimeMessage(message) {
1662
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
1663
+ if (this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber) {
1664
+ message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1656
1665
  }
1657
- catch (e) {
1658
- this.scheduleManager.afterOpProcessing(e, message);
1659
- throw e;
1666
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
1667
+ // If there are no more pending messages after processing a local message,
1668
+ // the document is no longer dirty.
1669
+ if (!this.hasPendingMessages()) {
1670
+ this.updateDocumentDirtyState(false);
1660
1671
  }
1672
+ this.emit("op", message, false /* runtimeMessage */);
1661
1673
  }
1662
1674
  /**
1663
1675
  * Assuming the given message is also a TypedContainerRuntimeMessage,
1664
1676
  * checks its type and dispatches the message to the appropriate handler in the runtime.
1665
1677
  * Throws a DataProcessingError if the message looks like but doesn't conform to a known TypedContainerRuntimeMessage type.
1666
1678
  */
1667
- validateAndProcessRuntimeMessage(messageWithContext, localOpMetadata) {
1668
- const { local, message, savedOp } = messageWithContext;
1679
+ validateAndProcessRuntimeMessage(messageWithContext) {
1680
+ const { local, message, savedOp, localOpMetadata } = messageWithContext;
1681
+ // Set the minimum sequence number to the containerRuntime's understanding of minimum sequence number.
1682
+ if (this.useDeltaManagerOpsProxy &&
1683
+ this.deltaManager.minimumSequenceNumber < message.minimumSequenceNumber) {
1684
+ message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1685
+ }
1686
+ this._processedClientSequenceNumber = message.clientSequenceNumber;
1687
+ // If there are no more pending messages after processing a local message,
1688
+ // the document is no longer dirty.
1689
+ if (!this.hasPendingMessages()) {
1690
+ this.updateDocumentDirtyState(false);
1691
+ }
1669
1692
  switch (message.type) {
1670
1693
  case messageTypes_js_1.ContainerMessageType.Attach:
1671
1694
  case messageTypes_js_1.ContainerMessageType.Alias:
@@ -1726,20 +1749,25 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1726
1749
  }
1727
1750
  }
1728
1751
  }
1752
+ this.emit("op", message, true /* runtimeMessage */);
1729
1753
  }
1730
1754
  /**
1731
1755
  * Emits the Signal event and update the perf signal data.
1732
- * @param clientSignalSequenceNumber - is the client signal sequence number to be uploaded.
1733
1756
  */
1734
- sendSignalTelemetryEvent(clientSignalSequenceNumber) {
1735
- const duration = Date.now() - this._perfSignalData.signalTimestamp;
1757
+ sendSignalTelemetryEvent() {
1758
+ const duration = Date.now() - this._signalTracking.signalTimestamp;
1736
1759
  this.mc.logger.sendPerformanceEvent({
1737
1760
  eventName: "SignalLatency",
1738
- duration,
1739
- signalsLost: this._perfSignalData.signalsLost,
1761
+ duration, // Roundtrip duration of the tracked signal in milliseconds.
1762
+ signalsSent: this._signalTracking.totalSignalsSentInLatencyWindow, // Signals sent since the last logged SignalLatency event.
1763
+ signalsLost: this._signalTracking.signalsLost, // Signals lost since the last logged SignalLatency event.
1764
+ outOfOrderSignals: this._signalTracking.signalsOutOfOrder, // Out of order signals since the last logged SignalLatency event.
1765
+ reconnectCount: this.consecutiveReconnects, // Container reconnect count.
1740
1766
  });
1741
- this._perfSignalData.signalsLost = 0;
1742
- this._perfSignalData.signalTimestamp = 0;
1767
+ this._signalTracking.signalsLost = 0;
1768
+ this._signalTracking.signalsOutOfOrder = 0;
1769
+ this._signalTracking.signalTimestamp = 0;
1770
+ this._signalTracking.totalSignalsSentInLatencyWindow = 0;
1743
1771
  }
1744
1772
  processSignal(message, local) {
1745
1773
  const envelope = message.content;
@@ -1747,29 +1775,55 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1747
1775
  clientId: message.clientId,
1748
1776
  content: envelope.contents.content,
1749
1777
  type: envelope.contents.type,
1778
+ targetClientId: message.targetClientId,
1750
1779
  };
1751
- // Only collect signal telemetry for messages sent by the current client.
1752
- if (message.clientId === this.clientId && this.connected) {
1753
- // Check to see if the signal was lost.
1754
- if (this._perfSignalData.trackingSignalSequenceNumber !== undefined &&
1755
- envelope.clientSignalSequenceNumber > this._perfSignalData.trackingSignalSequenceNumber) {
1756
- this._perfSignalData.signalsLost++;
1757
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
1758
- this.mc.logger.sendErrorEvent({
1759
- eventName: "SignalLost",
1760
- type: envelope.contents.type,
1761
- signalsLost: this._perfSignalData.signalsLost,
1762
- trackingSequenceNumber: this._perfSignalData.trackingSignalSequenceNumber,
1763
- clientSignalSequenceNumber: envelope.clientSignalSequenceNumber,
1764
- });
1765
- }
1766
- else if (envelope.clientSignalSequenceNumber ===
1767
- this._perfSignalData.trackingSignalSequenceNumber) {
1768
- // only logging for the first connection and the trackingSignalSequenceNUmber.
1769
- if (this.consecutiveReconnects === 0) {
1770
- this.sendSignalTelemetryEvent(envelope.clientSignalSequenceNumber);
1780
+ // Only collect signal telemetry for broadcast messages sent by the current client.
1781
+ if (message.clientId === this.clientId &&
1782
+ this.connected &&
1783
+ envelope.clientBroadcastSignalSequenceNumber !== undefined) {
1784
+ if (this._signalTracking.trackingSignalSequenceNumber !== undefined &&
1785
+ this._signalTracking.minimumTrackingSignalSequenceNumber !== undefined) {
1786
+ if (envelope.clientBroadcastSignalSequenceNumber >=
1787
+ this._signalTracking.trackingSignalSequenceNumber) {
1788
+ // Calculate the number of signals lost and log the event.
1789
+ const signalsLost = envelope.clientBroadcastSignalSequenceNumber -
1790
+ this._signalTracking.trackingSignalSequenceNumber;
1791
+ if (signalsLost > 0) {
1792
+ this._signalTracking.signalsLost += signalsLost;
1793
+ this.mc.logger.sendErrorEvent({
1794
+ eventName: "SignalLost",
1795
+ signalsLost, // Number of lost signals detected.
1796
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
1797
+ clientBroadcastSignalSequenceNumber: envelope.clientBroadcastSignalSequenceNumber, // Actual signal sequence number received.
1798
+ });
1799
+ }
1800
+ // Update the tracking signal sequence number to the next expected signal in the sequence.
1801
+ this._signalTracking.trackingSignalSequenceNumber =
1802
+ envelope.clientBroadcastSignalSequenceNumber + 1;
1803
+ }
1804
+ else if (envelope.clientBroadcastSignalSequenceNumber >=
1805
+ this._signalTracking.minimumTrackingSignalSequenceNumber) {
1806
+ this._signalTracking.signalsOutOfOrder++;
1807
+ this.mc.logger.sendTelemetryEvent({
1808
+ eventName: "SignalOutOfOrder",
1809
+ type: envelope.contents.type, // Type of signal that was received out of order.
1810
+ trackingSequenceNumber: this._signalTracking.trackingSignalSequenceNumber, // The next expected signal sequence number.
1811
+ clientBroadcastSignalSequenceNumber: envelope.clientBroadcastSignalSequenceNumber, // Sequence number of the out of order signal.
1812
+ });
1813
+ }
1814
+ if (this._signalTracking.roundTripSignalSequenceNumber !== undefined &&
1815
+ envelope.clientBroadcastSignalSequenceNumber >=
1816
+ this._signalTracking.roundTripSignalSequenceNumber) {
1817
+ if (envelope.clientBroadcastSignalSequenceNumber ===
1818
+ this._signalTracking.roundTripSignalSequenceNumber) {
1819
+ // Latency tracked signal has been received.
1820
+ // We now log the roundtrip duration of the tracked signal.
1821
+ // This telemetry event also logs metrics for signals sent, signals lost, and out of order signals received.
1822
+ // These metrics are reset after logging the telemetry event.
1823
+ this.sendSignalTelemetryEvent();
1824
+ }
1825
+ this._signalTracking.roundTripSignalSequenceNumber = undefined;
1771
1826
  }
1772
- this._perfSignalData.trackingSignalSequenceNumber = undefined;
1773
1827
  }
1774
1828
  }
1775
1829
  if (envelope.address === undefined) {
@@ -1945,18 +1999,36 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1945
1999
  }
1946
2000
  return true;
1947
2001
  }
1948
- createNewSignalEnvelope(address, type, content) {
1949
- const newSequenceNumber = ++this._perfSignalData.signalSequenceNumber;
2002
+ createNewSignalEnvelope(address, type, content, targetClientId) {
1950
2003
  const newEnvelope = {
1951
2004
  address,
1952
- clientSignalSequenceNumber: newSequenceNumber,
1953
2005
  contents: { type, content },
1954
2006
  };
1955
- // We should not track any signals in case we already have a tracking number.
1956
- if (newSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
1957
- this._perfSignalData.trackingSignalSequenceNumber === undefined) {
1958
- this._perfSignalData.signalTimestamp = Date.now();
1959
- this._perfSignalData.trackingSignalSequenceNumber = newSequenceNumber;
2007
+ const isBroadcastSignal = targetClientId === undefined;
2008
+ if (isBroadcastSignal) {
2009
+ const clientBroadcastSignalSequenceNumber = ++this._signalTracking
2010
+ .broadcastSignalSequenceNumber;
2011
+ newEnvelope.clientBroadcastSignalSequenceNumber = clientBroadcastSignalSequenceNumber;
2012
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement++;
2013
+ if (this._signalTracking.minimumTrackingSignalSequenceNumber === undefined ||
2014
+ this._signalTracking.trackingSignalSequenceNumber === undefined) {
2015
+ // Signal monitoring window is undefined
2016
+ // Initialize tracking to expect the next signal sent by the connected client.
2017
+ this._signalTracking.minimumTrackingSignalSequenceNumber =
2018
+ clientBroadcastSignalSequenceNumber;
2019
+ this._signalTracking.trackingSignalSequenceNumber =
2020
+ clientBroadcastSignalSequenceNumber;
2021
+ }
2022
+ // We should not track the round trip of a new signal in the case we are already tracking one.
2023
+ if (clientBroadcastSignalSequenceNumber % this.defaultTelemetrySignalSampleCount === 1 &&
2024
+ this._signalTracking.roundTripSignalSequenceNumber === undefined) {
2025
+ this._signalTracking.signalTimestamp = Date.now();
2026
+ this._signalTracking.roundTripSignalSequenceNumber =
2027
+ clientBroadcastSignalSequenceNumber;
2028
+ this._signalTracking.totalSignalsSentInLatencyWindow +=
2029
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement;
2030
+ this._signalTracking.signalsSentSinceLastLatencyMeasurement = 0;
2031
+ }
1960
2032
  }
1961
2033
  return newEnvelope;
1962
2034
  }
@@ -1965,10 +2037,16 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1965
2037
  * @param type - Type of the signal.
1966
2038
  * @param content - Content of the signal. Should be a JSON serializable object or primitive.
1967
2039
  * @param targetClientId - When specified, the signal is only sent to the provided client id.
2040
+ *
2041
+ * @remarks
2042
+ *
2043
+ * The `targetClientId` parameter here is currently intended for internal testing purposes only.
2044
+ * Support for this option at container runtime is planned to be deprecated in the future.
2045
+ *
1968
2046
  */
1969
2047
  submitSignal(type, content, targetClientId) {
1970
2048
  this.verifyNotClosed();
1971
- const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content);
2049
+ const envelope = this.createNewSignalEnvelope(undefined /* address */, type, content, targetClientId);
1972
2050
  return this.submitSignalFn(envelope, targetClientId);
1973
2051
  }
1974
2052
  setAttachState(attachState) {
@@ -2371,8 +2449,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2371
2449
  // Counting dataStores and handles
2372
2450
  // Because handles are unchanged dataStores in the current logic,
2373
2451
  // summarized dataStore count is total dataStore count minus handle count
2374
- // TODO why are we non null asserting here
2375
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2376
2452
  const dataStoreTree = summaryTree.tree[internal_5.channelsTreeName];
2377
2453
  (0, internal_2.assert)(dataStoreTree.type === driver_definitions_1.SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
2378
2454
  const handleCount = Object.values(dataStoreTree.tree).filter((value) => value.type === driver_definitions_1.SummaryType.Handle).length;
@@ -2695,6 +2771,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2695
2771
  throw new Error("Runtime is closed");
2696
2772
  }
2697
2773
  }
2774
+ /**
2775
+ * Resubmits each message in the batch, and then flushes the outbox.
2776
+ *
2777
+ * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2778
+ * for correlation to detect container forking.
2779
+ */
2698
2780
  reSubmitBatch(batch, batchId) {
2699
2781
  this.orderSequentially(() => {
2700
2782
  for (const message of batch) {