@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
@@ -33,6 +33,8 @@ import type {
33
33
  IContainerRuntime,
34
34
  IContainerRuntimeEvents,
35
35
  IContainerRuntimeInternal,
36
+ // eslint-disable-next-line import/no-deprecated
37
+ IContainerRuntimeWithResolveHandle_Deprecated,
36
38
  OutboundExtensionMessage,
37
39
  } from "@fluidframework/container-runtime-definitions/internal";
38
40
  import type {
@@ -60,6 +62,7 @@ import {
60
62
  PromiseCache,
61
63
  delay,
62
64
  fail,
65
+ unreachableCase,
63
66
  } from "@fluidframework/core-utils/internal";
64
67
  import type {
65
68
  IClientDetails,
@@ -157,6 +160,7 @@ import {
157
160
  tagCodeArtifacts,
158
161
  normalizeError,
159
162
  } from "@fluidframework/telemetry-utils/internal";
163
+ import { gt } from "semver-ts";
160
164
  import { v4 as uuid } from "uuid";
161
165
 
162
166
  import { BindBatchTracker } from "./batchTracker.js";
@@ -180,6 +184,8 @@ import {
180
184
  isValidMinVersionForCollab,
181
185
  type RuntimeOptionsAffectingDocSchema,
182
186
  type MinimumVersionForCollab,
187
+ type SemanticVersion,
188
+ validateRuntimeOptions,
183
189
  } from "./compatUtils.js";
184
190
  import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
185
191
  import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
@@ -206,7 +212,7 @@ import {
206
212
  import { InboundBatchAggregator } from "./inboundBatchAggregator.js";
207
213
  import {
208
214
  ContainerMessageType,
209
- type ContainerRuntimeDocumentSchemaMessage,
215
+ type OutboundContainerRuntimeDocumentSchemaMessage,
210
216
  ContainerRuntimeGCMessage,
211
217
  type ContainerRuntimeIdAllocationMessage,
212
218
  type InboundSequencedContainerRuntimeMessage,
@@ -244,7 +250,7 @@ import {
244
250
  import { SignalTelemetryManager } from "./signalTelemetryProcessing.js";
245
251
  // These types are imported as types here because they are present in summaryDelayLoadedModule, which is loaded dynamically when required.
246
252
  import type {
247
- IDocumentSchemaChangeMessage,
253
+ IDocumentSchemaChangeMessageIncoming,
248
254
  IDocumentSchemaCurrent,
249
255
  Summarizer,
250
256
  IDocumentSchemaFeatures,
@@ -772,6 +778,27 @@ export async function loadContainerRuntime(
772
778
 
773
779
  const defaultMaxConsecutiveReconnects = 7;
774
780
 
781
+ /**
782
+ * These are the ONLY message types that are allowed to be submitted while in staging mode
783
+ * (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
784
+ */
785
+ function canStageMessageOfType(
786
+ type: LocalContainerRuntimeMessage["type"],
787
+ ): type is
788
+ | ContainerMessageType.FluidDataStoreOp
789
+ | ContainerMessageType.GC
790
+ | ContainerMessageType.DocumentSchemaChange {
791
+ return (
792
+ // These are user changes coming up from the runtime's DataStores
793
+ type === ContainerMessageType.FluidDataStoreOp ||
794
+ // GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
795
+ // These can be submitted at any time, including while in Staging Mode.
796
+ type === ContainerMessageType.GC ||
797
+ // These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
798
+ type === ContainerMessageType.DocumentSchemaChange
799
+ );
800
+ }
801
+
775
802
  /**
776
803
  * Represents the runtime of the container. Contains helper functions/state of the container.
777
804
  * It will define the store level mappings.
@@ -784,6 +811,8 @@ export class ContainerRuntime
784
811
  IContainerRuntimeInternal,
785
812
  // eslint-disable-next-line import/no-deprecated
786
813
  IContainerRuntimeBaseExperimental,
814
+ // eslint-disable-next-line import/no-deprecated
815
+ IContainerRuntimeWithResolveHandle_Deprecated,
787
816
  IRuntime,
788
817
  IGarbageCollectionRuntime,
789
818
  ISummarizerRuntime,
@@ -862,6 +891,10 @@ export class ContainerRuntime
862
891
  `Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`,
863
892
  );
864
893
  }
894
+ // We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
895
+ // were manually set.
896
+ validateRuntimeOptions(minVersionForCollab, runtimeOptions);
897
+
865
898
  const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);
866
899
 
867
900
  // The following are the default values for the options that do not affect the DocumentSchema.
@@ -1083,8 +1116,19 @@ export class ContainerRuntime
1083
1116
  (schema) => {
1084
1117
  runtime.onSchemaChange(schema);
1085
1118
  },
1119
+ { minVersionForCollab },
1120
+ logger,
1086
1121
  );
1087
1122
 
1123
+ // If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
1124
+ const existingMinVersionForCollab =
1125
+ documentSchemaController.sessionSchema.info.minVersionForCollab;
1126
+ const updatedMinVersionForCollab =
1127
+ existingMinVersionForCollab === undefined ||
1128
+ gt(minVersionForCollab, existingMinVersionForCollab)
1129
+ ? minVersionForCollab
1130
+ : existingMinVersionForCollab;
1131
+
1088
1132
  if (compressionLz4 && !enableGroupedBatching) {
1089
1133
  throw new UsageError("If compression is enabled, op grouping must be enabled too");
1090
1134
  }
@@ -1123,7 +1167,7 @@ export class ContainerRuntime
1123
1167
  documentSchemaController,
1124
1168
  featureGatesForTelemetry,
1125
1169
  provideEntryPoint,
1126
- minVersionForCollab,
1170
+ updatedMinVersionForCollab,
1127
1171
  requestHandler,
1128
1172
  undefined, // summaryConfiguration
1129
1173
  recentBatchInfo,
@@ -1160,6 +1204,8 @@ export class ContainerRuntime
1160
1204
 
1161
1205
  public readonly clientDetails: IClientDetails;
1162
1206
 
1207
+ private readonly isSummarizerClient: boolean;
1208
+
1163
1209
  public get storage(): IDocumentStorageService {
1164
1210
  return this._storage;
1165
1211
  }
@@ -1292,7 +1338,7 @@ export class ContainerRuntime
1292
1338
  private readonly offlineEnabled: boolean;
1293
1339
  private flushScheduled = false;
1294
1340
 
1295
- private _connected: boolean;
1341
+ private canSendOps: boolean;
1296
1342
 
1297
1343
  private consecutiveReconnects = 0;
1298
1344
 
@@ -1314,8 +1360,12 @@ export class ContainerRuntime
1314
1360
  return this.dataModelChangeRunner.run(callback);
1315
1361
  }
1316
1362
 
1363
+ /**
1364
+ * Indicates whether the container is in a state where it is able to send
1365
+ * ops (connected to op stream and not in readonly mode).
1366
+ */
1317
1367
  public get connected(): boolean {
1318
- return this._connected;
1368
+ return this.canSendOps;
1319
1369
  }
1320
1370
 
1321
1371
  /**
@@ -1454,7 +1504,7 @@ export class ContainerRuntime
1454
1504
  private readonly documentsSchemaController: DocumentsSchemaController,
1455
1505
  featureGatesForTelemetry: Record<string, boolean | number | undefined>,
1456
1506
  provideEntryPoint: (containerRuntime: IContainerRuntime) => Promise<FluidObject>,
1457
- private readonly minVersionForCollab: MinimumVersionForCollab,
1507
+ private readonly minVersionForCollab: SemanticVersion,
1458
1508
  private readonly requestHandler?: (
1459
1509
  request: IRequest,
1460
1510
  runtime: IContainerRuntime,
@@ -1502,6 +1552,11 @@ export class ContainerRuntime
1502
1552
  this.mc = createChildMonitoringContext({
1503
1553
  logger: this.baseLogger,
1504
1554
  namespace: "ContainerRuntime",
1555
+ properties: {
1556
+ all: {
1557
+ inStagingMode: this.inStagingMode,
1558
+ },
1559
+ },
1505
1560
  });
1506
1561
 
1507
1562
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
@@ -1558,7 +1613,7 @@ export class ContainerRuntime
1558
1613
  // Values are generally expected to be set from the runtime side.
1559
1614
  this.options = options ?? {};
1560
1615
  this.clientDetails = clientDetails;
1561
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
1616
+ this.isSummarizerClient = this.clientDetails.type === summarizerClientType;
1562
1617
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
1563
1618
  // eslint-disable-next-line unicorn/consistent-destructuring
1564
1619
  this._getClientId = () => context.clientId;
@@ -1599,7 +1654,7 @@ export class ContainerRuntime
1599
1654
  );
1600
1655
 
1601
1656
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
1602
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
1657
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
1603
1658
 
1604
1659
  let loadSummaryNumber: number;
1605
1660
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
@@ -1625,7 +1680,7 @@ export class ContainerRuntime
1625
1680
 
1626
1681
  // Note that we only need to pull the *initial* connected state from the context.
1627
1682
  // Later updates come through calls to setConnectionState.
1628
- this._connected = connected;
1683
+ this.canSendOps = connected;
1629
1684
 
1630
1685
  this.mc.logger.sendTelemetryEvent({
1631
1686
  eventName: "GCFeatureMatrix",
@@ -1760,7 +1815,7 @@ export class ContainerRuntime
1760
1815
  existing,
1761
1816
  metadata,
1762
1817
  createContainerMetadata: this.createContainerMetadata,
1763
- isSummarizerClient,
1818
+ isSummarizerClient: this.isSummarizerClient,
1764
1819
  getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
1765
1820
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
1766
1821
  readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
@@ -1887,7 +1942,7 @@ export class ContainerRuntime
1887
1942
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
1888
1943
 
1889
1944
  this.outbox = new Outbox({
1890
- shouldSend: () => this.canSendOps(),
1945
+ shouldSend: () => this.shouldSendOps(),
1891
1946
  pendingStateManager: this.pendingStateManager,
1892
1947
  submitBatchFn,
1893
1948
  legacySendBatchFn,
@@ -2068,7 +2123,18 @@ export class ContainerRuntime
2068
2123
  this.sessionSchema.idCompressorMode === "on" ||
2069
2124
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)
2070
2125
  ) {
2071
- this._idCompressor = this.createIdCompressorFn();
2126
+ PerformanceEvent.timedExec(
2127
+ this.mc.logger,
2128
+ { eventName: "CreateIdCompressorOnBoot" },
2129
+ (event) => {
2130
+ this._idCompressor = this.createIdCompressorFn();
2131
+ event.end({
2132
+ details: {
2133
+ idCompressorMode: this.sessionSchema.idCompressorMode,
2134
+ },
2135
+ });
2136
+ },
2137
+ );
2072
2138
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
2073
2139
  assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
2074
2140
  }
@@ -2125,8 +2191,7 @@ export class ContainerRuntime
2125
2191
  maxOpsSinceLastSummary,
2126
2192
  );
2127
2193
 
2128
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
2129
- if (isSummarizerClient) {
2194
+ if (this.isSummarizerClient) {
2130
2195
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
2131
2196
  // so that all non summarizer clients don't have to load the code inside this module.
2132
2197
  const module = await import(
@@ -2575,7 +2640,7 @@ export class ContainerRuntime
2575
2640
 
2576
2641
  private replayPendingStates(): void {
2577
2642
  // We need to be able to send ops to replay states
2578
- if (!this.canSendOps()) {
2643
+ if (!this.shouldSendOps()) {
2579
2644
  return;
2580
2645
  }
2581
2646
 
@@ -2671,13 +2736,27 @@ export class ContainerRuntime
2671
2736
  this._idCompressor === undefined &&
2672
2737
  this.sessionSchema.idCompressorMode !== undefined
2673
2738
  ) {
2674
- this._idCompressor = this.createIdCompressorFn();
2675
- // Finalize any ranges we received while the compressor was turned off.
2676
- const ops = this.pendingIdCompressorOps;
2677
- this.pendingIdCompressorOps = [];
2678
- for (const range of ops) {
2679
- this._idCompressor.finalizeCreationRange(range);
2680
- }
2739
+ PerformanceEvent.timedExec(
2740
+ this.mc.logger,
2741
+ { eventName: "CreateIdCompressorOnDelayedLoad" },
2742
+ (event) => {
2743
+ this._idCompressor = this.createIdCompressorFn();
2744
+ // Finalize any ranges we received while the compressor was turned off.
2745
+ const ops = this.pendingIdCompressorOps;
2746
+ this.pendingIdCompressorOps = [];
2747
+ const trace = Trace.start();
2748
+ for (const range of ops) {
2749
+ this._idCompressor.finalizeCreationRange(range);
2750
+ }
2751
+ event.end({
2752
+ details: {
2753
+ finalizeCreationRangeDuration: trace.trace().duration,
2754
+ idCompressorMode: this.sessionSchema.idCompressorMode,
2755
+ pendingIdCompressorOps: ops.length,
2756
+ },
2757
+ });
2758
+ },
2759
+ );
2681
2760
  assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
2682
2761
  }
2683
2762
  }
@@ -2685,7 +2764,7 @@ export class ContainerRuntime
2685
2764
  private readonly notifyReadOnlyState = (readonly: boolean): void =>
2686
2765
  this.channelCollection.notifyReadOnlyState(readonly);
2687
2766
 
2688
- public setConnectionState(connected: boolean, clientId?: string): void {
2767
+ public setConnectionState(canSendOps: boolean, clientId?: string): void {
2689
2768
  // Validate we have consistent state
2690
2769
  const currentClientId = this._audience.getSelf()?.clientId;
2691
2770
  assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
@@ -2694,10 +2773,10 @@ export class ContainerRuntime
2694
2773
  0x978 /* this.clientId does not match Audience */,
2695
2774
  );
2696
2775
 
2697
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
2776
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
2698
2777
  this.loadIdCompressor();
2699
2778
  }
2700
- if (connected === false && this.delayConnectClientId !== undefined) {
2779
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
2701
2780
  this.delayConnectClientId = undefined;
2702
2781
  this.mc.logger.sendTelemetryEvent({
2703
2782
  eventName: "UnsuccessfulConnectedTransition",
@@ -2706,14 +2785,10 @@ export class ContainerRuntime
2706
2785
  return;
2707
2786
  }
2708
2787
 
2709
- if (!connected) {
2710
- this.documentsSchemaController.onDisconnect();
2711
- }
2712
-
2713
2788
  // If there are stashed blobs in the pending state, we need to delay
2714
2789
  // propagation of the "connected" event until we have uploaded them to
2715
2790
  // ensure we don't submit ops referencing a blob that has not been uploaded
2716
- const connecting = connected && !this._connected;
2791
+ const connecting = canSendOps && !this.canSendOps;
2717
2792
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
2718
2793
  assert(
2719
2794
  !this.delayConnectClientId,
@@ -2724,10 +2799,15 @@ export class ContainerRuntime
2724
2799
  return;
2725
2800
  }
2726
2801
 
2727
- this.setConnectionStateCore(connected, clientId);
2802
+ this.setConnectionStateCore(canSendOps, clientId);
2728
2803
  }
2729
2804
 
2730
- private setConnectionStateCore(connected: boolean, clientId?: string): void {
2805
+ /**
2806
+ * Raises and propagates connected events.
2807
+ * @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
2808
+ * @remarks The connection state from container context used here when raising connected events.
2809
+ */
2810
+ private setConnectionStateCore(canSendOps: boolean, clientId?: string): void {
2731
2811
  assert(
2732
2812
  !this.delayConnectClientId,
2733
2813
  0x394 /* connect event delay must be cleared before propagating connect event */,
@@ -2735,24 +2815,24 @@ export class ContainerRuntime
2735
2815
  this.verifyNotClosed();
2736
2816
 
2737
2817
  // There might be no change of state due to Container calling this API after loading runtime.
2738
- const changeOfState = this._connected !== connected;
2739
- const reconnection = changeOfState && !connected;
2818
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
2819
+ const reconnection = canSendOpsChanged && !canSendOps;
2740
2820
 
2741
2821
  // We need to flush the ops currently collected by Outbox to preserve original order.
2742
2822
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
2743
2823
  // We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
2744
- if (changeOfState && connected) {
2824
+ if (canSendOpsChanged && canSendOps) {
2745
2825
  this.flush();
2746
2826
  }
2747
2827
 
2748
- this._connected = connected;
2828
+ this.canSendOps = canSendOps;
2749
2829
 
2750
- if (connected) {
2830
+ if (canSendOps) {
2751
2831
  assert(
2752
2832
  this.attachState === AttachState.Attached,
2753
2833
  0x3cd /* Connection is possible only if container exists in storage */,
2754
2834
  );
2755
- if (changeOfState) {
2835
+ if (canSendOpsChanged) {
2756
2836
  this.signalTelemetryManager.resetTracking();
2757
2837
  }
2758
2838
  }
@@ -2778,14 +2858,14 @@ export class ContainerRuntime
2778
2858
  }
2779
2859
  }
2780
2860
 
2781
- if (changeOfState) {
2861
+ if (canSendOpsChanged) {
2782
2862
  this.replayPendingStates();
2783
2863
  }
2784
2864
 
2785
- this.channelCollection.setConnectionState(connected, clientId);
2786
- this.garbageCollector.setConnectionState(connected, clientId);
2865
+ this.channelCollection.setConnectionState(canSendOps, clientId);
2866
+ this.garbageCollector.setConnectionState(canSendOps, clientId);
2787
2867
 
2788
- raiseConnectedEvent(this.mc.logger, this, connected, clientId);
2868
+ raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
2789
2869
  }
2790
2870
 
2791
2871
  public async notifyOpReplay(message: ISequencedDocumentMessage): Promise<void> {
@@ -3161,7 +3241,7 @@ export class ContainerRuntime
3161
3241
  }
3162
3242
  case ContainerMessageType.DocumentSchemaChange: {
3163
3243
  this.documentsSchemaController.processDocumentSchemaMessages(
3164
- contents as IDocumentSchemaChangeMessage[],
3244
+ contents as IDocumentSchemaChangeMessageIncoming[],
3165
3245
  local,
3166
3246
  message.sequenceNumber,
3167
3247
  );
@@ -3334,7 +3414,8 @@ export class ContainerRuntime
3334
3414
  // This will throw and close the container if rollback fails
3335
3415
  try {
3336
3416
  checkpoint.rollback((message: LocalBatchMessage) =>
3337
- this.rollback(message.runtimeOp, message.localOpMetadata),
3417
+ // These changes are staged since we entered staging mode above
3418
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata),
3338
3419
  );
3339
3420
  this.updateDocumentDirtyState();
3340
3421
  stageControls?.discardChanges();
@@ -3400,25 +3481,35 @@ export class ContainerRuntime
3400
3481
  // eslint-disable-next-line import/no-deprecated
3401
3482
  public enterStagingMode = (): StageControlsExperimental => {
3402
3483
  if (this.stageControls !== undefined) {
3403
- throw new Error("already in staging mode");
3484
+ throw new UsageError("already in staging mode");
3485
+ }
3486
+ if (this.attachState === AttachState.Detached) {
3487
+ throw new UsageError("cannot enter staging mode while detached");
3404
3488
  }
3405
3489
 
3406
- // Make sure all BatchManagers are empty before entering staging mode,
3490
+ // Make sure Outbox is empty before entering staging mode,
3407
3491
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
3408
- this.outbox.flush();
3492
+ this.flush();
3409
3493
 
3410
3494
  const exitStagingMode = (discardOrCommit: () => void) => (): void => {
3411
- // Final flush of any last staged changes
3412
- this.outbox.flush();
3495
+ try {
3496
+ // Final flush of any last staged changes
3497
+ // NOTE: We can't use this.flush() here, because orderSequentially uses StagingMode and in the rollback case we'll hit assert 0x24c
3498
+ this.outbox.flush();
3413
3499
 
3414
- this.stageControls = undefined;
3500
+ this.stageControls = undefined;
3415
3501
 
3416
- // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
3417
- // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
3418
- this.submitIdAllocationOpIfNeeded({ staged: false });
3419
- discardOrCommit();
3502
+ // During Staging Mode, we avoid submitting any ID Allocation ops (apart from resubmitting pre-staging ops).
3503
+ // Now that we've exited, we need to submit an ID Allocation op for any IDs that were generated while in Staging Mode.
3504
+ this.submitIdAllocationOpIfNeeded({ staged: false });
3505
+ discardOrCommit();
3420
3506
 
3421
- this.channelCollection.notifyStagingMode(false);
3507
+ this.channelCollection.notifyStagingMode(false);
3508
+ } catch (error) {
3509
+ const normalizedError = normalizeError(error);
3510
+ this.closeFn(normalizedError);
3511
+ throw normalizedError;
3512
+ }
3422
3513
  };
3423
3514
 
3424
3515
  // eslint-disable-next-line import/no-deprecated
@@ -3430,7 +3521,7 @@ export class ContainerRuntime
3430
3521
  runtimeOp !== undefined,
3431
3522
  0xb82 /* Staged batches expected to have runtimeOp defined */,
3432
3523
  );
3433
- this.rollback(runtimeOp, localOpMetadata);
3524
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
3434
3525
  });
3435
3526
  this.updateDocumentDirtyState();
3436
3527
  }),
@@ -3514,7 +3605,7 @@ export class ContainerRuntime
3514
3605
  );
3515
3606
  }
3516
3607
 
3517
- private canSendOps(): boolean {
3608
+ private shouldSendOps(): boolean {
3518
3609
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
3519
3610
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
3520
3611
  return (
@@ -4479,6 +4570,11 @@ export class ContainerRuntime
4479
4570
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
4480
4571
  const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
4481
4572
 
4573
+ assert(
4574
+ !staged || canStageMessageOfType(type),
4575
+ 0xbba /* Unexpected message type submitted in Staging Mode */,
4576
+ );
4577
+
4482
4578
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
4483
4579
  if (!staged) {
4484
4580
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -4487,7 +4583,7 @@ export class ContainerRuntime
4487
4583
  // Allow document schema controller to send a message if it needs to propose change in document schema.
4488
4584
  // If it needs to send a message, it will call provided callback with payload of such message and rely
4489
4585
  // on this callback to do actual sending.
4490
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
4586
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
4491
4587
  if (schemaChangeMessage) {
4492
4588
  this.mc.logger.sendTelemetryEvent({
4493
4589
  eventName: "SchemaChangeProposal",
@@ -4496,8 +4592,9 @@ export class ContainerRuntime
4496
4592
  newRuntimeSchema: JSON.stringify(schemaChangeMessage.runtime),
4497
4593
  sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
4498
4594
  oldRuntimeSchema: JSON.stringify(this.metadata?.documentSchema?.runtime),
4595
+ minVersionForCollab: schemaChangeMessage.info?.minVersionForCollab,
4499
4596
  });
4500
- const msg: ContainerRuntimeDocumentSchemaMessage = {
4597
+ const msg: OutboundContainerRuntimeDocumentSchemaMessage = {
4501
4598
  type: ContainerMessageType.DocumentSchemaChange,
4502
4599
  contents: schemaChangeMessage,
4503
4600
  };
@@ -4601,14 +4698,25 @@ export class ContainerRuntime
4601
4698
 
4602
4699
  /**
4603
4700
  * Resubmits each message in the batch, and then flushes the outbox.
4701
+ * This typically happens when we reconnect and there are pending messages.
4702
+ *
4703
+ * @remarks
4704
+ * Attempting to resubmit a batch that has been successfully sequenced will not happen due to
4705
+ * checks in the ConnectionStateHandler (Loader layer)
4604
4706
  *
4605
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
4707
+ * The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
4708
+ * If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
4606
4709
  * for correlation to detect container forking.
4607
4710
  */
4608
4711
  private reSubmitBatch(
4609
4712
  batch: PendingMessageResubmitData[],
4610
4713
  { batchId, staged, squash }: PendingBatchResubmitMetadata,
4611
4714
  ): void {
4715
+ assert(
4716
+ this._summarizer === undefined,
4717
+ 0x8f2 /* Summarizer never reconnects so should never resubmit */,
4718
+ );
4719
+
4612
4720
  const resubmitInfo = {
4613
4721
  // Only include Batch ID if "Offline Load" feature is enabled
4614
4722
  // It's only needed to identify batches across container forks arising from misuse of offline load.
@@ -4616,36 +4724,61 @@ export class ContainerRuntime
4616
4724
  staged,
4617
4725
  };
4618
4726
 
4727
+ const resubmitFn = squash
4728
+ ? this.reSubmitWithSquashing.bind(this)
4729
+ : this.reSubmit.bind(this);
4730
+
4619
4731
  this.batchRunner.run(() => {
4620
4732
  for (const message of batch) {
4621
- this.reSubmit(message, squash);
4733
+ resubmitFn(message);
4622
4734
  }
4623
4735
  }, resubmitInfo);
4624
4736
 
4625
4737
  this.flush(resubmitInfo);
4626
4738
  }
4627
4739
 
4628
- private reSubmit(message: PendingMessageResubmitData, squash: boolean): void {
4629
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
4630
- }
4631
-
4632
4740
  /**
4633
- * Finds the right store and asks it to resubmit the message. This typically happens when we
4634
- * reconnect and there are pending messages.
4635
- * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
4636
- * @param message - The original LocalContainerRuntimeMessage.
4637
- * @param localOpMetadata - The local metadata associated with the original message.
4741
+ * Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
4742
+ * How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
4638
4743
  */
4639
- private reSubmitCore(
4640
- message: LocalContainerRuntimeMessage,
4641
- localOpMetadata: unknown,
4642
- opMetadata: Record<string, unknown> | undefined,
4643
- squash: boolean,
4644
- ): void {
4744
+ private reSubmitWithSquashing(resubmitData: PendingMessageResubmitData): void {
4745
+ const message = resubmitData.runtimeOp;
4645
4746
  assert(
4646
- this._summarizer === undefined,
4647
- 0x8f2 /* Summarizer never reconnects so should never resubmit */,
4747
+ canStageMessageOfType(message.type),
4748
+ 0xbbb /* Expected message type to be compatible with staging */,
4648
4749
  );
4750
+ switch (message.type) {
4751
+ case ContainerMessageType.FluidDataStoreOp: {
4752
+ this.channelCollection.reSubmit(
4753
+ message.type,
4754
+ message.contents,
4755
+ resubmitData.localOpMetadata,
4756
+ /* squash: */ true,
4757
+ );
4758
+ break;
4759
+ }
4760
+ // NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
4761
+ case ContainerMessageType.GC:
4762
+ case ContainerMessageType.DocumentSchemaChange: {
4763
+ this.reSubmit(resubmitData);
4764
+ break;
4765
+ }
4766
+ default: {
4767
+ unreachableCase(message.type);
4768
+ }
4769
+ }
4770
+ }
4771
+
4772
+ /**
4773
+ * Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
4774
+ * transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
4775
+ * How to resubmit is up to the subsystem that submitted the op to begin with
4776
+ */
4777
+ private reSubmit({
4778
+ runtimeOp: message,
4779
+ localOpMetadata,
4780
+ opMetadata,
4781
+ }: PendingMessageResubmitData): void {
4649
4782
  switch (message.type) {
4650
4783
  case ContainerMessageType.FluidDataStoreOp:
4651
4784
  case ContainerMessageType.Attach:
@@ -4656,7 +4789,7 @@ export class ContainerRuntime
4656
4789
  message.type,
4657
4790
  message.contents,
4658
4791
  localOpMetadata,
4659
- squash,
4792
+ /* squash: */ false,
4660
4793
  );
4661
4794
  break;
4662
4795
  }
@@ -4683,9 +4816,9 @@ export class ContainerRuntime
4683
4816
  break;
4684
4817
  }
4685
4818
  case ContainerMessageType.DocumentSchemaChange: {
4686
- // There is no need to resend this message. Document schema controller will properly resend it again (if needed)
4687
- // on a first occasion (any ops sent after reconnect). There is a good chance, though, that it will not want to
4688
- // send any ops, as some other client already changed schema.
4819
+ // We shouldn't directly resubmit due to Compare-And-Swap semantics.
4820
+ // If needed it will be generated from scratch before other ops are submitted.
4821
+ this.documentsSchemaController.pendingOpNotAcked();
4689
4822
  break;
4690
4823
  }
4691
4824
  default: {
@@ -4696,8 +4829,15 @@ export class ContainerRuntime
4696
4829
  }
4697
4830
  }
4698
4831
 
4699
- private rollback(runtimeOp: LocalContainerRuntimeMessage, localOpMetadata: unknown): void {
4700
- const { type, contents } = runtimeOp;
4832
+ /**
4833
+ * Rollback the given op which was only staged but not yet submitted.
4834
+ */
4835
+ private rollbackStagedChanges(
4836
+ { type, contents }: LocalContainerRuntimeMessage,
4837
+ localOpMetadata: unknown,
4838
+ ): void {
4839
+ assert(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
4840
+
4701
4841
  switch (type) {
4702
4842
  case ContainerMessageType.FluidDataStoreOp: {
4703
4843
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -4705,8 +4845,24 @@ export class ContainerRuntime
4705
4845
  this.channelCollection.rollback(type, contents, localOpMetadata);
4706
4846
  break;
4707
4847
  }
4848
+ case ContainerMessageType.GC: {
4849
+ // Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
4850
+ // Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
4851
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
4852
+ this.mc.logger.sendErrorEvent({
4853
+ eventName: "GC_OpDiscarded",
4854
+ details: { subType: contents.type },
4855
+ });
4856
+ break;
4857
+ }
4858
+ case ContainerMessageType.DocumentSchemaChange: {
4859
+ // Notify the document schema controller that the pending op was not acked.
4860
+ // This will allow it to propose the schema change again if needed.
4861
+ this.documentsSchemaController.pendingOpNotAcked();
4862
+ break;
4863
+ }
4708
4864
  default: {
4709
- throw new Error(`Can't rollback ${type}`);
4865
+ unreachableCase(type);
4710
4866
  }
4711
4867
  }
4712
4868
  }
@@ -4986,6 +5142,7 @@ export class ContainerRuntime
4986
5142
  },
4987
5143
  getQuorum: this.getQuorum.bind(this),
4988
5144
  getAudience: this.getAudience.bind(this),
5145
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
4989
5146
  } satisfies ExtensionHost<TRuntimeProperties>;
4990
5147
  entry = new factory(runtime, ...useContext);
4991
5148
  this.extensions.set(id, entry);