@fluidframework/container-runtime 2.41.0 → 2.43.0-343119

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 (136) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/channelCollection.d.ts +1 -1
  4. package/dist/channelCollection.d.ts.map +1 -1
  5. package/dist/channelCollection.js +4 -4
  6. package/dist/channelCollection.js.map +1 -1
  7. package/dist/compatUtils.d.ts +24 -1
  8. package/dist/compatUtils.d.ts.map +1 -1
  9. package/dist/compatUtils.js +109 -7
  10. package/dist/compatUtils.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +36 -15
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +186 -71
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStore.d.ts.map +1 -1
  16. package/dist/dataStore.js +5 -0
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +2 -0
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcDefinitions.d.ts +1 -1
  22. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  23. package/dist/gc/gcDefinitions.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/messageTypes.d.ts +5 -4
  28. package/dist/messageTypes.d.ts.map +1 -1
  29. package/dist/messageTypes.js.map +1 -1
  30. package/dist/metadata.d.ts +1 -1
  31. package/dist/metadata.d.ts.map +1 -1
  32. package/dist/metadata.js.map +1 -1
  33. package/dist/opLifecycle/definitions.d.ts +6 -5
  34. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  35. package/dist/opLifecycle/definitions.js.map +1 -1
  36. package/dist/opLifecycle/index.d.ts +1 -1
  37. package/dist/opLifecycle/index.d.ts.map +1 -1
  38. package/dist/opLifecycle/index.js.map +1 -1
  39. package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
  40. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  41. package/dist/opLifecycle/opGroupingManager.js +6 -4
  42. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  43. package/dist/opLifecycle/opSerialization.d.ts +2 -1
  44. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  45. package/dist/opLifecycle/opSerialization.js.map +1 -1
  46. package/dist/packageVersion.d.ts +1 -1
  47. package/dist/packageVersion.d.ts.map +1 -1
  48. package/dist/packageVersion.js +1 -1
  49. package/dist/packageVersion.js.map +1 -1
  50. package/dist/pendingStateManager.d.ts +18 -5
  51. package/dist/pendingStateManager.d.ts.map +1 -1
  52. package/dist/pendingStateManager.js +20 -13
  53. package/dist/pendingStateManager.js.map +1 -1
  54. package/dist/summary/documentSchema.d.ts +79 -16
  55. package/dist/summary/documentSchema.d.ts.map +1 -1
  56. package/dist/summary/documentSchema.js +119 -53
  57. package/dist/summary/documentSchema.js.map +1 -1
  58. package/dist/summary/index.d.ts +1 -1
  59. package/dist/summary/index.d.ts.map +1 -1
  60. package/dist/summary/index.js.map +1 -1
  61. package/lib/channelCollection.d.ts +1 -1
  62. package/lib/channelCollection.d.ts.map +1 -1
  63. package/lib/channelCollection.js +4 -4
  64. package/lib/channelCollection.js.map +1 -1
  65. package/lib/compatUtils.d.ts +24 -1
  66. package/lib/compatUtils.d.ts.map +1 -1
  67. package/lib/compatUtils.js +102 -3
  68. package/lib/compatUtils.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +36 -15
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +188 -73
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/dataStore.d.ts.map +1 -1
  74. package/lib/dataStore.js +5 -0
  75. package/lib/dataStore.js.map +1 -1
  76. package/lib/gc/garbageCollection.d.ts.map +1 -1
  77. package/lib/gc/garbageCollection.js +2 -0
  78. package/lib/gc/garbageCollection.js.map +1 -1
  79. package/lib/gc/gcDefinitions.d.ts +1 -1
  80. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  81. package/lib/gc/gcDefinitions.js.map +1 -1
  82. package/lib/index.d.ts +2 -2
  83. package/lib/index.d.ts.map +1 -1
  84. package/lib/index.js.map +1 -1
  85. package/lib/messageTypes.d.ts +5 -4
  86. package/lib/messageTypes.d.ts.map +1 -1
  87. package/lib/messageTypes.js.map +1 -1
  88. package/lib/metadata.d.ts +1 -1
  89. package/lib/metadata.d.ts.map +1 -1
  90. package/lib/metadata.js.map +1 -1
  91. package/lib/opLifecycle/definitions.d.ts +6 -5
  92. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  93. package/lib/opLifecycle/definitions.js.map +1 -1
  94. package/lib/opLifecycle/index.d.ts +1 -1
  95. package/lib/opLifecycle/index.d.ts.map +1 -1
  96. package/lib/opLifecycle/index.js.map +1 -1
  97. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  98. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  99. package/lib/opLifecycle/opGroupingManager.js +6 -4
  100. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  101. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  102. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  103. package/lib/opLifecycle/opSerialization.js.map +1 -1
  104. package/lib/packageVersion.d.ts +1 -1
  105. package/lib/packageVersion.d.ts.map +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/pendingStateManager.d.ts +18 -5
  109. package/lib/pendingStateManager.d.ts.map +1 -1
  110. package/lib/pendingStateManager.js +20 -13
  111. package/lib/pendingStateManager.js.map +1 -1
  112. package/lib/summary/documentSchema.d.ts +79 -16
  113. package/lib/summary/documentSchema.d.ts.map +1 -1
  114. package/lib/summary/documentSchema.js +119 -53
  115. package/lib/summary/documentSchema.js.map +1 -1
  116. package/lib/summary/index.d.ts +1 -1
  117. package/lib/summary/index.d.ts.map +1 -1
  118. package/lib/summary/index.js.map +1 -1
  119. package/package.json +18 -18
  120. package/src/channelCollection.ts +4 -4
  121. package/src/compatUtils.ts +147 -10
  122. package/src/containerRuntime.ts +242 -85
  123. package/src/dataStore.ts +7 -0
  124. package/src/gc/garbageCollection.ts +2 -0
  125. package/src/gc/gcDefinitions.ts +1 -1
  126. package/src/index.ts +4 -2
  127. package/src/messageTypes.ts +12 -5
  128. package/src/metadata.ts +1 -1
  129. package/src/opLifecycle/definitions.ts +7 -3
  130. package/src/opLifecycle/index.ts +1 -0
  131. package/src/opLifecycle/opGroupingManager.ts +17 -4
  132. package/src/opLifecycle/opSerialization.ts +6 -1
  133. package/src/packageVersion.ts +1 -1
  134. package/src/pendingStateManager.ts +49 -22
  135. package/src/summary/documentSchema.ts +228 -83
  136. package/src/summary/index.ts +3 -1
@@ -16,6 +16,7 @@ const internal_5 = require("@fluidframework/id-compressor/internal");
16
16
  const internal_6 = require("@fluidframework/runtime-definitions/internal");
17
17
  const internal_7 = require("@fluidframework/runtime-utils/internal");
18
18
  const internal_8 = require("@fluidframework/telemetry-utils/internal");
19
+ const semver_ts_1 = require("semver-ts");
19
20
  const uuid_1 = require("uuid");
20
21
  const batchTracker_js_1 = require("./batchTracker.js");
21
22
  const index_js_1 = require("./blobManager/index.js");
@@ -197,6 +198,20 @@ async function loadContainerRuntime(params) {
197
198
  }
198
199
  exports.loadContainerRuntime = loadContainerRuntime;
199
200
  const defaultMaxConsecutiveReconnects = 7;
201
+ /**
202
+ * These are the ONLY message types that are allowed to be submitted while in staging mode
203
+ * (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
204
+ */
205
+ function canStageMessageOfType(type) {
206
+ return (
207
+ // These are user changes coming up from the runtime's DataStores
208
+ type === messageTypes_js_1.ContainerMessageType.FluidDataStoreOp ||
209
+ // GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
210
+ // These can be submitted at any time, including while in Staging Mode.
211
+ type === messageTypes_js_1.ContainerMessageType.GC ||
212
+ // These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
213
+ type === messageTypes_js_1.ContainerMessageType.DocumentSchemaChange);
214
+ }
200
215
  /**
201
216
  * Represents the runtime of the container. Contains helper functions/state of the container.
202
217
  * It will define the store level mappings.
@@ -245,6 +260,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
245
260
  if (!(0, compatUtils_js_1.isValidMinVersionForCollab)(minVersionForCollab)) {
246
261
  throw new internal_8.UsageError(`Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`);
247
262
  }
263
+ // We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
264
+ // were manually set.
265
+ (0, compatUtils_js_1.validateRuntimeOptions)(minVersionForCollab, runtimeOptions);
248
266
  const defaultsAffectingDocSchema = (0, compatUtils_js_1.getMinVersionForCollabDefaults)(minVersionForCollab);
249
267
  // The following are the default values for the options that do not affect the DocumentSchema.
250
268
  const defaultsNotAffectingDocSchema = {
@@ -402,7 +420,13 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
402
420
  disallowedVersions: [],
403
421
  }, (schema) => {
404
422
  runtime.onSchemaChange(schema);
405
- });
423
+ }, { minVersionForCollab }, logger);
424
+ // If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
425
+ const existingMinVersionForCollab = documentSchemaController.sessionSchema.info.minVersionForCollab;
426
+ const updatedMinVersionForCollab = existingMinVersionForCollab === undefined ||
427
+ (0, semver_ts_1.gt)(minVersionForCollab, existingMinVersionForCollab)
428
+ ? minVersionForCollab
429
+ : existingMinVersionForCollab;
406
430
  if (compressionLz4 && !enableGroupedBatching) {
407
431
  throw new internal_8.UsageError("If compression is enabled, op grouping must be enabled too");
408
432
  }
@@ -421,7 +445,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
421
445
  explicitSchemaControl,
422
446
  createBlobPayloadPending,
423
447
  };
424
- const runtime = new containerRuntimeCtor(context, registry, metadata, electedSummarizerData, chunks ?? [], aliases ?? [], internalRuntimeOptions, containerScope, logger, existing, blobManagerLoadInfo, context.storage, createIdCompressorFn, documentSchemaController, featureGatesForTelemetry, provideEntryPoint, minVersionForCollab, requestHandler, undefined, // summaryConfiguration
448
+ const runtime = new containerRuntimeCtor(context, registry, metadata, electedSummarizerData, chunks ?? [], aliases ?? [], internalRuntimeOptions, containerScope, logger, existing, blobManagerLoadInfo, context.storage, createIdCompressorFn, documentSchemaController, featureGatesForTelemetry, provideEntryPoint, updatedMinVersionForCollab, requestHandler, undefined, // summaryConfiguration
425
449
  recentBatchInfo);
426
450
  runtime.blobManager.stashedBlobsUploadP.then(() => {
427
451
  // make sure we didn't reconnect before the promise resolved
@@ -510,8 +534,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
510
534
  ensureNoDataModelChanges(callback) {
511
535
  return this.dataModelChangeRunner.run(callback);
512
536
  }
537
+ /**
538
+ * Indicates whether the container is in a state where it is able to send
539
+ * ops (connected to op stream and not in readonly mode).
540
+ */
513
541
  get connected() {
514
- return this._connected;
542
+ return this.canSendOps;
515
543
  }
516
544
  /**
517
545
  * clientId of parent (non-summarizing) container that owns summarizer container
@@ -599,20 +627,31 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
599
627
  // eslint-disable-next-line import/no-deprecated
600
628
  this.enterStagingMode = () => {
601
629
  if (this.stageControls !== undefined) {
602
- throw new Error("already in staging mode");
630
+ throw new internal_8.UsageError("already in staging mode");
631
+ }
632
+ if (this.attachState === container_definitions_1.AttachState.Detached) {
633
+ throw new internal_8.UsageError("cannot enter staging mode while detached");
603
634
  }
604
- // Make sure all BatchManagers are empty before entering staging mode,
635
+ // Make sure Outbox is empty before entering staging mode,
605
636
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
606
- this.outbox.flush();
637
+ this.flush();
607
638
  const exitStagingMode = (discardOrCommit) => () => {
608
- // Final flush of any last staged changes
609
- this.outbox.flush();
610
- this.stageControls = undefined;
611
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
612
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
613
- this.submitIdAllocationOpIfNeeded({ staged: false });
614
- discardOrCommit();
615
- this.channelCollection.notifyStagingMode(false);
639
+ try {
640
+ // Final flush of any last staged changes
641
+ // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
642
+ this.outbox.flush();
643
+ this.stageControls = undefined;
644
+ // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
645
+ // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
646
+ this.submitIdAllocationOpIfNeeded({ staged: false });
647
+ discardOrCommit();
648
+ this.channelCollection.notifyStagingMode(false);
649
+ }
650
+ catch (error) {
651
+ const normalizedError = (0, internal_8.normalizeError)(error);
652
+ this.closeFn(normalizedError);
653
+ throw normalizedError;
654
+ }
616
655
  };
617
656
  // eslint-disable-next-line import/no-deprecated
618
657
  const stageControls = {
@@ -620,7 +659,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
620
659
  // Pop all staged batches from the PSM and roll them back in LIFO order
621
660
  this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
622
661
  (0, internal_2.assert)(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
623
- this.rollback(runtimeOp, localOpMetadata);
662
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
624
663
  });
625
664
  this.updateDocumentDirtyState();
626
665
  }),
@@ -657,6 +696,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
657
696
  this.mc = (0, internal_8.createChildMonitoringContext)({
658
697
  logger: this.baseLogger,
659
698
  namespace: "ContainerRuntime",
699
+ properties: {
700
+ all: {
701
+ inStagingMode: this.inStagingMode,
702
+ },
703
+ },
660
704
  });
661
705
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
662
706
  // We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
@@ -697,7 +741,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
697
741
  // Values are generally expected to be set from the runtime side.
698
742
  this.options = options ?? {};
699
743
  this.clientDetails = clientDetails;
700
- const isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
744
+ this.isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
701
745
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
702
746
  // eslint-disable-next-line unicorn/consistent-destructuring
703
747
  this._getClientId = () => context.clientId;
@@ -734,7 +778,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
734
778
  details: { attachState: this.attachState },
735
779
  }));
736
780
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
737
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
781
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
738
782
  let loadSummaryNumber;
739
783
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
740
784
  // get the values from the metadata blob.
@@ -758,7 +802,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
758
802
  this.messageAtLastSummary = lastMessageFromMetadata(metadata);
759
803
  // Note that we only need to pull the *initial* connected state from the context.
760
804
  // Later updates come through calls to setConnectionState.
761
- this._connected = connected;
805
+ this.canSendOps = connected;
762
806
  this.mc.logger.sendTelemetryEvent({
763
807
  eventName: "GCFeatureMatrix",
764
808
  metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
@@ -851,7 +895,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
851
895
  existing,
852
896
  metadata,
853
897
  createContainerMetadata: this.createContainerMetadata,
854
- isSummarizerClient,
898
+ isSummarizerClient: this.isSummarizerClient,
855
899
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
856
900
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
857
901
  readAndParseBlob: async (id) => (0, internal_4.readAndParse)(this.storage, id),
@@ -935,7 +979,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
935
979
  // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
936
980
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
937
981
  this.outbox = new index_js_3.Outbox({
938
- shouldSend: () => this.canSendOps(),
982
+ shouldSend: () => this.shouldSendOps(),
939
983
  pendingStateManager: this.pendingStateManager,
940
984
  submitBatchFn,
941
985
  legacySendBatchFn,
@@ -1074,7 +1118,14 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1074
1118
  await this.initializeSummarizer(loader);
1075
1119
  if (this.sessionSchema.idCompressorMode === "on" ||
1076
1120
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
1077
- this._idCompressor = this.createIdCompressorFn();
1121
+ internal_8.PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnBoot" }, (event) => {
1122
+ this._idCompressor = this.createIdCompressorFn();
1123
+ event.end({
1124
+ details: {
1125
+ idCompressorMode: this.sessionSchema.idCompressorMode,
1126
+ },
1127
+ });
1128
+ });
1078
1129
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
1079
1130
  (0, internal_2.assert)(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
1080
1131
  }
@@ -1103,8 +1154,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1103
1154
  const orderedClientCollection = new index_js_4.OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
1104
1155
  const orderedClientElectionForSummarizer = new index_js_4.OrderedClientElection(orderedClientLogger, orderedClientCollection, this.electedSummarizerData ?? this.innerDeltaManager.lastSequenceNumber, index_js_4.SummarizerClientElection.isClientEligible, this.mc.config.getBoolean("Fluid.ContainerRuntime.OrderedClientElection.EnablePerformanceEvents"));
1105
1156
  this.summarizerClientElection = new index_js_4.SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
1106
- const isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
1107
- if (isSummarizerClient) {
1157
+ if (this.isSummarizerClient) {
1108
1158
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
1109
1159
  // so that all non summarizer clients don't have to load the code inside this module.
1110
1160
  const module = await import(
@@ -1452,7 +1502,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1452
1502
  }
1453
1503
  replayPendingStates() {
1454
1504
  // We need to be able to send ops to replay states
1455
- if (!this.canSendOps()) {
1505
+ if (!this.shouldSendOps()) {
1456
1506
  return;
1457
1507
  }
1458
1508
  // Replaying is an internal operation and we don't want to generate noise while doing it.
@@ -1534,25 +1584,35 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1534
1584
  loadIdCompressor() {
1535
1585
  if (this._idCompressor === undefined &&
1536
1586
  this.sessionSchema.idCompressorMode !== undefined) {
1537
- this._idCompressor = this.createIdCompressorFn();
1538
- // Finalize any ranges we received while the compressor was turned off.
1539
- const ops = this.pendingIdCompressorOps;
1540
- this.pendingIdCompressorOps = [];
1541
- for (const range of ops) {
1542
- this._idCompressor.finalizeCreationRange(range);
1543
- }
1587
+ internal_8.PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnDelayedLoad" }, (event) => {
1588
+ this._idCompressor = this.createIdCompressorFn();
1589
+ // Finalize any ranges we received while the compressor was turned off.
1590
+ const ops = this.pendingIdCompressorOps;
1591
+ this.pendingIdCompressorOps = [];
1592
+ const trace = client_utils_1.Trace.start();
1593
+ for (const range of ops) {
1594
+ this._idCompressor.finalizeCreationRange(range);
1595
+ }
1596
+ event.end({
1597
+ details: {
1598
+ finalizeCreationRangeDuration: trace.trace().duration,
1599
+ idCompressorMode: this.sessionSchema.idCompressorMode,
1600
+ pendingIdCompressorOps: ops.length,
1601
+ },
1602
+ });
1603
+ });
1544
1604
  (0, internal_2.assert)(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
1545
1605
  }
1546
1606
  }
1547
- setConnectionState(connected, clientId) {
1607
+ setConnectionState(canSendOps, clientId) {
1548
1608
  // Validate we have consistent state
1549
1609
  const currentClientId = this._audience.getSelf()?.clientId;
1550
1610
  (0, internal_2.assert)(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
1551
1611
  (0, internal_2.assert)(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
1552
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
1612
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
1553
1613
  this.loadIdCompressor();
1554
1614
  }
1555
- if (connected === false && this.delayConnectClientId !== undefined) {
1615
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
1556
1616
  this.delayConnectClientId = undefined;
1557
1617
  this.mc.logger.sendTelemetryEvent({
1558
1618
  eventName: "UnsuccessfulConnectedTransition",
@@ -1560,37 +1620,39 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1560
1620
  // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1561
1621
  return;
1562
1622
  }
1563
- if (!connected) {
1564
- this.documentsSchemaController.onDisconnect();
1565
- }
1566
1623
  // If there are stashed blobs in the pending state, we need to delay
1567
1624
  // propagation of the "connected" event until we have uploaded them to
1568
1625
  // ensure we don't submit ops referencing a blob that has not been uploaded
1569
- const connecting = connected && !this._connected;
1626
+ const connecting = canSendOps && !this.canSendOps;
1570
1627
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
1571
1628
  (0, internal_2.assert)(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
1572
1629
  (0, internal_2.assert)(!!clientId, 0x792 /* Must have clientId when connecting */);
1573
1630
  this.delayConnectClientId = clientId;
1574
1631
  return;
1575
1632
  }
1576
- this.setConnectionStateCore(connected, clientId);
1633
+ this.setConnectionStateCore(canSendOps, clientId);
1577
1634
  }
1578
- setConnectionStateCore(connected, clientId) {
1635
+ /**
1636
+ * Raises and propagates connected events.
1637
+ * @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
1638
+ * @remarks The connection state from container context used here when raising connected events.
1639
+ */
1640
+ setConnectionStateCore(canSendOps, clientId) {
1579
1641
  (0, internal_2.assert)(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
1580
1642
  this.verifyNotClosed();
1581
1643
  // There might be no change of state due to Container calling this API after loading runtime.
1582
- const changeOfState = this._connected !== connected;
1583
- const reconnection = changeOfState && !connected;
1644
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
1645
+ const reconnection = canSendOpsChanged && !canSendOps;
1584
1646
  // We need to flush the ops currently collected by Outbox to preserve original order.
1585
1647
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
1586
1648
  // We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
1587
- if (changeOfState && connected) {
1649
+ if (canSendOpsChanged && canSendOps) {
1588
1650
  this.flush();
1589
1651
  }
1590
- this._connected = connected;
1591
- if (connected) {
1652
+ this.canSendOps = canSendOps;
1653
+ if (canSendOps) {
1592
1654
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
1593
- if (changeOfState) {
1655
+ if (canSendOpsChanged) {
1594
1656
  this.signalTelemetryManager.resetTracking();
1595
1657
  }
1596
1658
  }
@@ -1606,12 +1668,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1606
1668
  return;
1607
1669
  }
1608
1670
  }
1609
- if (changeOfState) {
1671
+ if (canSendOpsChanged) {
1610
1672
  this.replayPendingStates();
1611
1673
  }
1612
- this.channelCollection.setConnectionState(connected, clientId);
1613
- this.garbageCollector.setConnectionState(connected, clientId);
1614
- (0, internal_8.raiseConnectedEvent)(this.mc.logger, this, connected, clientId);
1674
+ this.channelCollection.setConnectionState(canSendOps, clientId);
1675
+ this.garbageCollector.setConnectionState(canSendOps, clientId);
1676
+ (0, internal_8.raiseConnectedEvent)(this.mc.logger, this, this.connected /* canSendOps */, clientId);
1615
1677
  }
1616
1678
  async notifyOpReplay(message) {
1617
1679
  await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
@@ -2018,7 +2080,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2018
2080
  if (checkpoint) {
2019
2081
  // This will throw and close the container if rollback fails
2020
2082
  try {
2021
- checkpoint.rollback((message) => this.rollback(message.runtimeOp, message.localOpMetadata));
2083
+ checkpoint.rollback((message) =>
2084
+ // These changes are staged since we entered staging mode above
2085
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
2022
2086
  this.updateDocumentDirtyState();
2023
2087
  stageControls?.discardChanges();
2024
2088
  stageControls = undefined;
@@ -2095,7 +2159,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2095
2159
  const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
2096
2160
  return (0, dataStore_js_1.channelToDataStore)(await context.realize(), context.id, this.channelCollection, this.mc.logger);
2097
2161
  }
2098
- canSendOps() {
2162
+ shouldSendOps() {
2099
2163
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
2100
2164
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2101
2165
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
@@ -2784,6 +2848,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2784
2848
  try {
2785
2849
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
2786
2850
  const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
2851
+ (0, internal_2.assert)(!staged || canStageMessageOfType(type), 0xbba /* Unexpected message type submitted in Staging Mode */);
2787
2852
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
2788
2853
  if (!staged) {
2789
2854
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -2791,7 +2856,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2791
2856
  // Allow document schema controller to send a message if it needs to propose change in document schema.
2792
2857
  // If it needs to send a message, it will call provided callback with payload of such message and rely
2793
2858
  // on this callback to do actual sending.
2794
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
2859
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
2795
2860
  if (schemaChangeMessage) {
2796
2861
  this.mc.logger.sendTelemetryEvent({
2797
2862
  eventName: "SchemaChangeProposal",
@@ -2800,6 +2865,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2800
2865
  newRuntimeSchema: JSON.stringify(schemaChangeMessage.runtime),
2801
2866
  sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
2802
2867
  oldRuntimeSchema: JSON.stringify(this.metadata?.documentSchema?.runtime),
2868
+ minVersionForCollab: schemaChangeMessage.info?.minVersionForCollab,
2803
2869
  });
2804
2870
  const msg = {
2805
2871
  type: messageTypes_js_1.ContainerMessageType.DocumentSchemaChange,
@@ -2889,43 +2955,72 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2889
2955
  }
2890
2956
  /**
2891
2957
  * Resubmits each message in the batch, and then flushes the outbox.
2958
+ * This typically happens when we reconnect and there are pending messages.
2959
+ *
2960
+ * @remarks
2961
+ * Attempting to resubmit a batch that has been successfully sequenced will not happen due to
2962
+ * checks in the ConnectionStateHandler (Loader layer)
2892
2963
  *
2893
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2964
+ * The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
2965
+ * If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2894
2966
  * for correlation to detect container forking.
2895
2967
  */
2896
2968
  reSubmitBatch(batch, { batchId, staged, squash }) {
2969
+ (0, internal_2.assert)(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2897
2970
  const resubmitInfo = {
2898
2971
  // Only include Batch ID if "Offline Load" feature is enabled
2899
2972
  // It's only needed to identify batches across container forks arising from misuse of offline load.
2900
2973
  batchId: this.offlineEnabled ? batchId : undefined,
2901
2974
  staged,
2902
2975
  };
2976
+ const resubmitFn = squash
2977
+ ? this.reSubmitWithSquashing.bind(this)
2978
+ : this.reSubmit.bind(this);
2903
2979
  this.batchRunner.run(() => {
2904
2980
  for (const message of batch) {
2905
- this.reSubmit(message, squash);
2981
+ resubmitFn(message);
2906
2982
  }
2907
2983
  }, resubmitInfo);
2908
2984
  this.flush(resubmitInfo);
2909
2985
  }
2910
- reSubmit(message, squash) {
2911
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
2986
+ /**
2987
+ * Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
2988
+ * How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
2989
+ */
2990
+ reSubmitWithSquashing(resubmitData) {
2991
+ const message = resubmitData.runtimeOp;
2992
+ (0, internal_2.assert)(canStageMessageOfType(message.type), 0xbbb /* Expected message type to be compatible with staging */);
2993
+ switch (message.type) {
2994
+ case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
2995
+ this.channelCollection.reSubmit(message.type, message.contents, resubmitData.localOpMetadata,
2996
+ /* squash: */ true);
2997
+ break;
2998
+ }
2999
+ // NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
3000
+ case messageTypes_js_1.ContainerMessageType.GC:
3001
+ case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
3002
+ this.reSubmit(resubmitData);
3003
+ break;
3004
+ }
3005
+ default: {
3006
+ (0, internal_2.unreachableCase)(message.type);
3007
+ }
3008
+ }
2912
3009
  }
2913
3010
  /**
2914
- * Finds the right store and asks it to resubmit the message. This typically happens when we
2915
- * reconnect and there are pending messages.
2916
- * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
2917
- * @param message - The original LocalContainerRuntimeMessage.
2918
- * @param localOpMetadata - The local metadata associated with the original message.
3011
+ * Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
3012
+ * transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
3013
+ * How to resubmit is up to the subsystem that submitted the op to begin with
2919
3014
  */
2920
- reSubmitCore(message, localOpMetadata, opMetadata, squash) {
2921
- (0, internal_2.assert)(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
3015
+ reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
2922
3016
  switch (message.type) {
2923
3017
  case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp:
2924
3018
  case messageTypes_js_1.ContainerMessageType.Attach:
2925
3019
  case messageTypes_js_1.ContainerMessageType.Alias: {
2926
3020
  // For Operations, call resubmitDataStoreOp which will find the right store
2927
3021
  // and trigger resubmission on it.
2928
- this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata, squash);
3022
+ this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
3023
+ /* squash: */ false);
2929
3024
  break;
2930
3025
  }
2931
3026
  case messageTypes_js_1.ContainerMessageType.IdAllocation: {
@@ -2951,9 +3046,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2951
3046
  break;
2952
3047
  }
2953
3048
  case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
2954
- // There is no need to resend this message. Document schema controller will properly resend it again (if needed)
2955
- // on a first occasion (any ops sent after reconnect). There is a good chance, though, that it will not want to
2956
- // send any ops, as some other client already changed schema.
3049
+ // We shouldn't directly resubmit due to Compare-And-Swap semantics.
3050
+ // If needed it will be generated from scratch before other ops are submitted.
3051
+ this.documentsSchemaController.pendingOpNotAcked();
2957
3052
  break;
2958
3053
  }
2959
3054
  default: {
@@ -2963,8 +3058,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2963
3058
  }
2964
3059
  }
2965
3060
  }
2966
- rollback(runtimeOp, localOpMetadata) {
2967
- const { type, contents } = runtimeOp;
3061
+ /**
3062
+ * Rollback the given op which was only staged but not yet submitted.
3063
+ */
3064
+ rollbackStagedChanges({ type, contents }, localOpMetadata) {
3065
+ (0, internal_2.assert)(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
2968
3066
  switch (type) {
2969
3067
  case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
2970
3068
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -2972,8 +3070,24 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2972
3070
  this.channelCollection.rollback(type, contents, localOpMetadata);
2973
3071
  break;
2974
3072
  }
3073
+ case messageTypes_js_1.ContainerMessageType.GC: {
3074
+ // Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
3075
+ // Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
3076
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
3077
+ this.mc.logger.sendErrorEvent({
3078
+ eventName: "GC_OpDiscarded",
3079
+ details: { subType: contents.type },
3080
+ });
3081
+ break;
3082
+ }
3083
+ case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
3084
+ // Notify the document schema controller that the pending op was not acked.
3085
+ // This will allow it to propose the schema change again if needed.
3086
+ this.documentsSchemaController.pendingOpNotAcked();
3087
+ break;
3088
+ }
2975
3089
  default: {
2976
- throw new Error(`Can't rollback ${type}`);
3090
+ (0, internal_2.unreachableCase)(type);
2977
3091
  }
2978
3092
  }
2979
3093
  }
@@ -3151,6 +3265,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3151
3265
  },
3152
3266
  getQuorum: this.getQuorum.bind(this),
3153
3267
  getAudience: this.getAudience.bind(this),
3268
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
3154
3269
  };
3155
3270
  entry = new factory(runtime, ...useContext);
3156
3271
  this.extensions.set(id, entry);