@fluidframework/container-runtime 2.41.0 → 2.42.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 (134) 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 +22 -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 +34 -13
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +158 -59
  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 +1 -1
  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.js +1 -1
  48. package/dist/packageVersion.js.map +1 -1
  49. package/dist/pendingStateManager.d.ts +18 -5
  50. package/dist/pendingStateManager.d.ts.map +1 -1
  51. package/dist/pendingStateManager.js +20 -13
  52. package/dist/pendingStateManager.js.map +1 -1
  53. package/dist/summary/documentSchema.d.ts +42 -18
  54. package/dist/summary/documentSchema.d.ts.map +1 -1
  55. package/dist/summary/documentSchema.js +62 -52
  56. package/dist/summary/documentSchema.js.map +1 -1
  57. package/dist/summary/index.d.ts +1 -1
  58. package/dist/summary/index.d.ts.map +1 -1
  59. package/dist/summary/index.js.map +1 -1
  60. package/lib/channelCollection.d.ts +1 -1
  61. package/lib/channelCollection.d.ts.map +1 -1
  62. package/lib/channelCollection.js +4 -4
  63. package/lib/channelCollection.js.map +1 -1
  64. package/lib/compatUtils.d.ts +22 -1
  65. package/lib/compatUtils.d.ts.map +1 -1
  66. package/lib/compatUtils.js +102 -3
  67. package/lib/compatUtils.js.map +1 -1
  68. package/lib/containerRuntime.d.ts +34 -13
  69. package/lib/containerRuntime.d.ts.map +1 -1
  70. package/lib/containerRuntime.js +160 -61
  71. package/lib/containerRuntime.js.map +1 -1
  72. package/lib/dataStore.d.ts.map +1 -1
  73. package/lib/dataStore.js +5 -0
  74. package/lib/dataStore.js.map +1 -1
  75. package/lib/gc/garbageCollection.d.ts.map +1 -1
  76. package/lib/gc/garbageCollection.js +2 -0
  77. package/lib/gc/garbageCollection.js.map +1 -1
  78. package/lib/gc/gcDefinitions.d.ts +1 -1
  79. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  80. package/lib/gc/gcDefinitions.js.map +1 -1
  81. package/lib/index.d.ts +1 -1
  82. package/lib/index.d.ts.map +1 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/messageTypes.d.ts +5 -4
  85. package/lib/messageTypes.d.ts.map +1 -1
  86. package/lib/messageTypes.js.map +1 -1
  87. package/lib/metadata.d.ts +1 -1
  88. package/lib/metadata.d.ts.map +1 -1
  89. package/lib/metadata.js.map +1 -1
  90. package/lib/opLifecycle/definitions.d.ts +6 -5
  91. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  92. package/lib/opLifecycle/definitions.js.map +1 -1
  93. package/lib/opLifecycle/index.d.ts +1 -1
  94. package/lib/opLifecycle/index.d.ts.map +1 -1
  95. package/lib/opLifecycle/index.js.map +1 -1
  96. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  97. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  98. package/lib/opLifecycle/opGroupingManager.js +6 -4
  99. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  100. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  101. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  102. package/lib/opLifecycle/opSerialization.js.map +1 -1
  103. package/lib/packageVersion.d.ts +1 -1
  104. package/lib/packageVersion.js +1 -1
  105. package/lib/packageVersion.js.map +1 -1
  106. package/lib/pendingStateManager.d.ts +18 -5
  107. package/lib/pendingStateManager.d.ts.map +1 -1
  108. package/lib/pendingStateManager.js +20 -13
  109. package/lib/pendingStateManager.js.map +1 -1
  110. package/lib/summary/documentSchema.d.ts +42 -18
  111. package/lib/summary/documentSchema.d.ts.map +1 -1
  112. package/lib/summary/documentSchema.js +62 -52
  113. package/lib/summary/documentSchema.js.map +1 -1
  114. package/lib/summary/index.d.ts +1 -1
  115. package/lib/summary/index.d.ts.map +1 -1
  116. package/lib/summary/index.js.map +1 -1
  117. package/package.json +18 -18
  118. package/src/channelCollection.ts +4 -4
  119. package/src/compatUtils.ts +145 -10
  120. package/src/containerRuntime.ts +209 -73
  121. package/src/dataStore.ts +7 -0
  122. package/src/gc/garbageCollection.ts +2 -0
  123. package/src/gc/gcDefinitions.ts +1 -1
  124. package/src/index.ts +2 -1
  125. package/src/messageTypes.ts +12 -5
  126. package/src/metadata.ts +1 -1
  127. package/src/opLifecycle/definitions.ts +7 -3
  128. package/src/opLifecycle/index.ts +1 -0
  129. package/src/opLifecycle/opGroupingManager.ts +17 -4
  130. package/src/opLifecycle/opSerialization.ts +6 -1
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +49 -22
  133. package/src/summary/documentSchema.ts +111 -86
  134. package/src/summary/index.ts +2 -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,
@@ -180,6 +183,7 @@ import {
180
183
  isValidMinVersionForCollab,
181
184
  type RuntimeOptionsAffectingDocSchema,
182
185
  type MinimumVersionForCollab,
186
+ validateRuntimeOptions,
183
187
  } from "./compatUtils.js";
184
188
  import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
185
189
  import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
@@ -206,7 +210,7 @@ import {
206
210
  import { InboundBatchAggregator } from "./inboundBatchAggregator.js";
207
211
  import {
208
212
  ContainerMessageType,
209
- type ContainerRuntimeDocumentSchemaMessage,
213
+ type OutboundContainerRuntimeDocumentSchemaMessage,
210
214
  ContainerRuntimeGCMessage,
211
215
  type ContainerRuntimeIdAllocationMessage,
212
216
  type InboundSequencedContainerRuntimeMessage,
@@ -244,7 +248,7 @@ import {
244
248
  import { SignalTelemetryManager } from "./signalTelemetryProcessing.js";
245
249
  // These types are imported as types here because they are present in summaryDelayLoadedModule, which is loaded dynamically when required.
246
250
  import type {
247
- IDocumentSchemaChangeMessage,
251
+ IDocumentSchemaChangeMessageIncoming,
248
252
  IDocumentSchemaCurrent,
249
253
  Summarizer,
250
254
  IDocumentSchemaFeatures,
@@ -772,6 +776,27 @@ export async function loadContainerRuntime(
772
776
 
773
777
  const defaultMaxConsecutiveReconnects = 7;
774
778
 
779
+ /**
780
+ * These are the ONLY message types that are allowed to be submitted while in staging mode
781
+ * (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
782
+ */
783
+ function canStageMessageOfType(
784
+ type: LocalContainerRuntimeMessage["type"],
785
+ ): type is
786
+ | ContainerMessageType.FluidDataStoreOp
787
+ | ContainerMessageType.GC
788
+ | ContainerMessageType.DocumentSchemaChange {
789
+ return (
790
+ // These are user changes coming up from the runtime's DataStores
791
+ type === ContainerMessageType.FluidDataStoreOp ||
792
+ // GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
793
+ // These can be submitted at any time, including while in Staging Mode.
794
+ type === ContainerMessageType.GC ||
795
+ // These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
796
+ type === ContainerMessageType.DocumentSchemaChange
797
+ );
798
+ }
799
+
775
800
  /**
776
801
  * Represents the runtime of the container. Contains helper functions/state of the container.
777
802
  * It will define the store level mappings.
@@ -784,6 +809,8 @@ export class ContainerRuntime
784
809
  IContainerRuntimeInternal,
785
810
  // eslint-disable-next-line import/no-deprecated
786
811
  IContainerRuntimeBaseExperimental,
812
+ // eslint-disable-next-line import/no-deprecated
813
+ IContainerRuntimeWithResolveHandle_Deprecated,
787
814
  IRuntime,
788
815
  IGarbageCollectionRuntime,
789
816
  ISummarizerRuntime,
@@ -862,6 +889,10 @@ export class ContainerRuntime
862
889
  `Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`,
863
890
  );
864
891
  }
892
+ // We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
893
+ // were manually set.
894
+ validateRuntimeOptions(minVersionForCollab, runtimeOptions);
895
+
865
896
  const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);
866
897
 
867
898
  // The following are the default values for the options that do not affect the DocumentSchema.
@@ -1160,6 +1191,8 @@ export class ContainerRuntime
1160
1191
 
1161
1192
  public readonly clientDetails: IClientDetails;
1162
1193
 
1194
+ private readonly isSummarizerClient: boolean;
1195
+
1163
1196
  public get storage(): IDocumentStorageService {
1164
1197
  return this._storage;
1165
1198
  }
@@ -1292,7 +1325,7 @@ export class ContainerRuntime
1292
1325
  private readonly offlineEnabled: boolean;
1293
1326
  private flushScheduled = false;
1294
1327
 
1295
- private _connected: boolean;
1328
+ private canSendOps: boolean;
1296
1329
 
1297
1330
  private consecutiveReconnects = 0;
1298
1331
 
@@ -1314,8 +1347,12 @@ export class ContainerRuntime
1314
1347
  return this.dataModelChangeRunner.run(callback);
1315
1348
  }
1316
1349
 
1350
+ /**
1351
+ * Indicates whether the container is in a state where it is able to send
1352
+ * ops (connected to op stream and not in readonly mode).
1353
+ */
1317
1354
  public get connected(): boolean {
1318
- return this._connected;
1355
+ return this.canSendOps;
1319
1356
  }
1320
1357
 
1321
1358
  /**
@@ -1502,6 +1539,11 @@ export class ContainerRuntime
1502
1539
  this.mc = createChildMonitoringContext({
1503
1540
  logger: this.baseLogger,
1504
1541
  namespace: "ContainerRuntime",
1542
+ properties: {
1543
+ all: {
1544
+ inStagingMode: this.inStagingMode,
1545
+ },
1546
+ },
1505
1547
  });
1506
1548
 
1507
1549
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
@@ -1558,7 +1600,7 @@ export class ContainerRuntime
1558
1600
  // Values are generally expected to be set from the runtime side.
1559
1601
  this.options = options ?? {};
1560
1602
  this.clientDetails = clientDetails;
1561
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
1603
+ this.isSummarizerClient = this.clientDetails.type === summarizerClientType;
1562
1604
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
1563
1605
  // eslint-disable-next-line unicorn/consistent-destructuring
1564
1606
  this._getClientId = () => context.clientId;
@@ -1599,7 +1641,7 @@ export class ContainerRuntime
1599
1641
  );
1600
1642
 
1601
1643
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
1602
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
1644
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
1603
1645
 
1604
1646
  let loadSummaryNumber: number;
1605
1647
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
@@ -1625,7 +1667,7 @@ export class ContainerRuntime
1625
1667
 
1626
1668
  // Note that we only need to pull the *initial* connected state from the context.
1627
1669
  // Later updates come through calls to setConnectionState.
1628
- this._connected = connected;
1670
+ this.canSendOps = connected;
1629
1671
 
1630
1672
  this.mc.logger.sendTelemetryEvent({
1631
1673
  eventName: "GCFeatureMatrix",
@@ -1760,7 +1802,7 @@ export class ContainerRuntime
1760
1802
  existing,
1761
1803
  metadata,
1762
1804
  createContainerMetadata: this.createContainerMetadata,
1763
- isSummarizerClient,
1805
+ isSummarizerClient: this.isSummarizerClient,
1764
1806
  getNodePackagePath: async (nodePath: string) => this.getGCNodePackagePath(nodePath),
1765
1807
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
1766
1808
  readAndParseBlob: async <T>(id: string) => readAndParse<T>(this.storage, id),
@@ -1887,7 +1929,7 @@ export class ContainerRuntime
1887
1929
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
1888
1930
 
1889
1931
  this.outbox = new Outbox({
1890
- shouldSend: () => this.canSendOps(),
1932
+ shouldSend: () => this.shouldSendOps(),
1891
1933
  pendingStateManager: this.pendingStateManager,
1892
1934
  submitBatchFn,
1893
1935
  legacySendBatchFn,
@@ -2068,7 +2110,18 @@ export class ContainerRuntime
2068
2110
  this.sessionSchema.idCompressorMode === "on" ||
2069
2111
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)
2070
2112
  ) {
2071
- this._idCompressor = this.createIdCompressorFn();
2113
+ PerformanceEvent.timedExec(
2114
+ this.mc.logger,
2115
+ { eventName: "CreateIdCompressorOnBoot" },
2116
+ (event) => {
2117
+ this._idCompressor = this.createIdCompressorFn();
2118
+ event.end({
2119
+ details: {
2120
+ idCompressorMode: this.sessionSchema.idCompressorMode,
2121
+ },
2122
+ });
2123
+ },
2124
+ );
2072
2125
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
2073
2126
  assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
2074
2127
  }
@@ -2125,8 +2178,7 @@ export class ContainerRuntime
2125
2178
  maxOpsSinceLastSummary,
2126
2179
  );
2127
2180
 
2128
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
2129
- if (isSummarizerClient) {
2181
+ if (this.isSummarizerClient) {
2130
2182
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
2131
2183
  // so that all non summarizer clients don't have to load the code inside this module.
2132
2184
  const module = await import(
@@ -2575,7 +2627,7 @@ export class ContainerRuntime
2575
2627
 
2576
2628
  private replayPendingStates(): void {
2577
2629
  // We need to be able to send ops to replay states
2578
- if (!this.canSendOps()) {
2630
+ if (!this.shouldSendOps()) {
2579
2631
  return;
2580
2632
  }
2581
2633
 
@@ -2671,13 +2723,27 @@ export class ContainerRuntime
2671
2723
  this._idCompressor === undefined &&
2672
2724
  this.sessionSchema.idCompressorMode !== undefined
2673
2725
  ) {
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
- }
2726
+ PerformanceEvent.timedExec(
2727
+ this.mc.logger,
2728
+ { eventName: "CreateIdCompressorOnDelayedLoad" },
2729
+ (event) => {
2730
+ this._idCompressor = this.createIdCompressorFn();
2731
+ // Finalize any ranges we received while the compressor was turned off.
2732
+ const ops = this.pendingIdCompressorOps;
2733
+ this.pendingIdCompressorOps = [];
2734
+ const trace = Trace.start();
2735
+ for (const range of ops) {
2736
+ this._idCompressor.finalizeCreationRange(range);
2737
+ }
2738
+ event.end({
2739
+ details: {
2740
+ finalizeCreationRangeDuration: trace.trace().duration,
2741
+ idCompressorMode: this.sessionSchema.idCompressorMode,
2742
+ pendingIdCompressorOps: ops.length,
2743
+ },
2744
+ });
2745
+ },
2746
+ );
2681
2747
  assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
2682
2748
  }
2683
2749
  }
@@ -2685,7 +2751,7 @@ export class ContainerRuntime
2685
2751
  private readonly notifyReadOnlyState = (readonly: boolean): void =>
2686
2752
  this.channelCollection.notifyReadOnlyState(readonly);
2687
2753
 
2688
- public setConnectionState(connected: boolean, clientId?: string): void {
2754
+ public setConnectionState(canSendOps: boolean, clientId?: string): void {
2689
2755
  // Validate we have consistent state
2690
2756
  const currentClientId = this._audience.getSelf()?.clientId;
2691
2757
  assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
@@ -2694,10 +2760,10 @@ export class ContainerRuntime
2694
2760
  0x978 /* this.clientId does not match Audience */,
2695
2761
  );
2696
2762
 
2697
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
2763
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
2698
2764
  this.loadIdCompressor();
2699
2765
  }
2700
- if (connected === false && this.delayConnectClientId !== undefined) {
2766
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
2701
2767
  this.delayConnectClientId = undefined;
2702
2768
  this.mc.logger.sendTelemetryEvent({
2703
2769
  eventName: "UnsuccessfulConnectedTransition",
@@ -2706,14 +2772,10 @@ export class ContainerRuntime
2706
2772
  return;
2707
2773
  }
2708
2774
 
2709
- if (!connected) {
2710
- this.documentsSchemaController.onDisconnect();
2711
- }
2712
-
2713
2775
  // If there are stashed blobs in the pending state, we need to delay
2714
2776
  // propagation of the "connected" event until we have uploaded them to
2715
2777
  // ensure we don't submit ops referencing a blob that has not been uploaded
2716
- const connecting = connected && !this._connected;
2778
+ const connecting = canSendOps && !this.canSendOps;
2717
2779
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
2718
2780
  assert(
2719
2781
  !this.delayConnectClientId,
@@ -2724,10 +2786,15 @@ export class ContainerRuntime
2724
2786
  return;
2725
2787
  }
2726
2788
 
2727
- this.setConnectionStateCore(connected, clientId);
2789
+ this.setConnectionStateCore(canSendOps, clientId);
2728
2790
  }
2729
2791
 
2730
- private setConnectionStateCore(connected: boolean, clientId?: string): void {
2792
+ /**
2793
+ * Raises and propagates connected events.
2794
+ * @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
2795
+ * @remarks The connection state from container context used here when raising connected events.
2796
+ */
2797
+ private setConnectionStateCore(canSendOps: boolean, clientId?: string): void {
2731
2798
  assert(
2732
2799
  !this.delayConnectClientId,
2733
2800
  0x394 /* connect event delay must be cleared before propagating connect event */,
@@ -2735,24 +2802,24 @@ export class ContainerRuntime
2735
2802
  this.verifyNotClosed();
2736
2803
 
2737
2804
  // 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;
2805
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
2806
+ const reconnection = canSendOpsChanged && !canSendOps;
2740
2807
 
2741
2808
  // We need to flush the ops currently collected by Outbox to preserve original order.
2742
2809
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
2743
2810
  // 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) {
2811
+ if (canSendOpsChanged && canSendOps) {
2745
2812
  this.flush();
2746
2813
  }
2747
2814
 
2748
- this._connected = connected;
2815
+ this.canSendOps = canSendOps;
2749
2816
 
2750
- if (connected) {
2817
+ if (canSendOps) {
2751
2818
  assert(
2752
2819
  this.attachState === AttachState.Attached,
2753
2820
  0x3cd /* Connection is possible only if container exists in storage */,
2754
2821
  );
2755
- if (changeOfState) {
2822
+ if (canSendOpsChanged) {
2756
2823
  this.signalTelemetryManager.resetTracking();
2757
2824
  }
2758
2825
  }
@@ -2778,14 +2845,14 @@ export class ContainerRuntime
2778
2845
  }
2779
2846
  }
2780
2847
 
2781
- if (changeOfState) {
2848
+ if (canSendOpsChanged) {
2782
2849
  this.replayPendingStates();
2783
2850
  }
2784
2851
 
2785
- this.channelCollection.setConnectionState(connected, clientId);
2786
- this.garbageCollector.setConnectionState(connected, clientId);
2852
+ this.channelCollection.setConnectionState(canSendOps, clientId);
2853
+ this.garbageCollector.setConnectionState(canSendOps, clientId);
2787
2854
 
2788
- raiseConnectedEvent(this.mc.logger, this, connected, clientId);
2855
+ raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
2789
2856
  }
2790
2857
 
2791
2858
  public async notifyOpReplay(message: ISequencedDocumentMessage): Promise<void> {
@@ -3161,7 +3228,7 @@ export class ContainerRuntime
3161
3228
  }
3162
3229
  case ContainerMessageType.DocumentSchemaChange: {
3163
3230
  this.documentsSchemaController.processDocumentSchemaMessages(
3164
- contents as IDocumentSchemaChangeMessage[],
3231
+ contents as IDocumentSchemaChangeMessageIncoming[],
3165
3232
  local,
3166
3233
  message.sequenceNumber,
3167
3234
  );
@@ -3334,7 +3401,8 @@ export class ContainerRuntime
3334
3401
  // This will throw and close the container if rollback fails
3335
3402
  try {
3336
3403
  checkpoint.rollback((message: LocalBatchMessage) =>
3337
- this.rollback(message.runtimeOp, message.localOpMetadata),
3404
+ // These changes are staged since we entered staging mode above
3405
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata),
3338
3406
  );
3339
3407
  this.updateDocumentDirtyState();
3340
3408
  stageControls?.discardChanges();
@@ -3400,7 +3468,10 @@ export class ContainerRuntime
3400
3468
  // eslint-disable-next-line import/no-deprecated
3401
3469
  public enterStagingMode = (): StageControlsExperimental => {
3402
3470
  if (this.stageControls !== undefined) {
3403
- throw new Error("already in staging mode");
3471
+ throw new UsageError("already in staging mode");
3472
+ }
3473
+ if (this.attachState === AttachState.Detached) {
3474
+ throw new UsageError("cannot enter staging mode while detached");
3404
3475
  }
3405
3476
 
3406
3477
  // Make sure all BatchManagers are empty before entering staging mode,
@@ -3430,7 +3501,7 @@ export class ContainerRuntime
3430
3501
  runtimeOp !== undefined,
3431
3502
  0xb82 /* Staged batches expected to have runtimeOp defined */,
3432
3503
  );
3433
- this.rollback(runtimeOp, localOpMetadata);
3504
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
3434
3505
  });
3435
3506
  this.updateDocumentDirtyState();
3436
3507
  }),
@@ -3514,7 +3585,7 @@ export class ContainerRuntime
3514
3585
  );
3515
3586
  }
3516
3587
 
3517
- private canSendOps(): boolean {
3588
+ private shouldSendOps(): boolean {
3518
3589
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
3519
3590
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
3520
3591
  return (
@@ -4479,6 +4550,11 @@ export class ContainerRuntime
4479
4550
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
4480
4551
  const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
4481
4552
 
4553
+ assert(
4554
+ !staged || canStageMessageOfType(type),
4555
+ 0xbba /* Unexpected message type submitted in Staging Mode */,
4556
+ );
4557
+
4482
4558
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
4483
4559
  if (!staged) {
4484
4560
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -4487,7 +4563,7 @@ export class ContainerRuntime
4487
4563
  // Allow document schema controller to send a message if it needs to propose change in document schema.
4488
4564
  // If it needs to send a message, it will call provided callback with payload of such message and rely
4489
4565
  // on this callback to do actual sending.
4490
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
4566
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
4491
4567
  if (schemaChangeMessage) {
4492
4568
  this.mc.logger.sendTelemetryEvent({
4493
4569
  eventName: "SchemaChangeProposal",
@@ -4497,7 +4573,7 @@ export class ContainerRuntime
4497
4573
  sessionRuntimeSchema: JSON.stringify(this.sessionSchema),
4498
4574
  oldRuntimeSchema: JSON.stringify(this.metadata?.documentSchema?.runtime),
4499
4575
  });
4500
- const msg: ContainerRuntimeDocumentSchemaMessage = {
4576
+ const msg: OutboundContainerRuntimeDocumentSchemaMessage = {
4501
4577
  type: ContainerMessageType.DocumentSchemaChange,
4502
4578
  contents: schemaChangeMessage,
4503
4579
  };
@@ -4601,14 +4677,25 @@ export class ContainerRuntime
4601
4677
 
4602
4678
  /**
4603
4679
  * Resubmits each message in the batch, and then flushes the outbox.
4680
+ * This typically happens when we reconnect and there are pending messages.
4604
4681
  *
4605
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
4682
+ * @remarks
4683
+ * Attempting to resubmit a batch that has been successfully sequenced will not happen due to
4684
+ * checks in the ConnectionStateHandler (Loader layer)
4685
+ *
4686
+ * The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
4687
+ * If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
4606
4688
  * for correlation to detect container forking.
4607
4689
  */
4608
4690
  private reSubmitBatch(
4609
4691
  batch: PendingMessageResubmitData[],
4610
4692
  { batchId, staged, squash }: PendingBatchResubmitMetadata,
4611
4693
  ): void {
4694
+ assert(
4695
+ this._summarizer === undefined,
4696
+ 0x8f2 /* Summarizer never reconnects so should never resubmit */,
4697
+ );
4698
+
4612
4699
  const resubmitInfo = {
4613
4700
  // Only include Batch ID if "Offline Load" feature is enabled
4614
4701
  // It's only needed to identify batches across container forks arising from misuse of offline load.
@@ -4616,36 +4703,61 @@ export class ContainerRuntime
4616
4703
  staged,
4617
4704
  };
4618
4705
 
4706
+ const resubmitFn = squash
4707
+ ? this.reSubmitWithSquashing.bind(this)
4708
+ : this.reSubmit.bind(this);
4709
+
4619
4710
  this.batchRunner.run(() => {
4620
4711
  for (const message of batch) {
4621
- this.reSubmit(message, squash);
4712
+ resubmitFn(message);
4622
4713
  }
4623
4714
  }, resubmitInfo);
4624
4715
 
4625
4716
  this.flush(resubmitInfo);
4626
4717
  }
4627
4718
 
4628
- private reSubmit(message: PendingMessageResubmitData, squash: boolean): void {
4629
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
4630
- }
4631
-
4632
4719
  /**
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.
4720
+ * Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
4721
+ * How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
4638
4722
  */
4639
- private reSubmitCore(
4640
- message: LocalContainerRuntimeMessage,
4641
- localOpMetadata: unknown,
4642
- opMetadata: Record<string, unknown> | undefined,
4643
- squash: boolean,
4644
- ): void {
4723
+ private reSubmitWithSquashing(resubmitData: PendingMessageResubmitData): void {
4724
+ const message = resubmitData.runtimeOp;
4645
4725
  assert(
4646
- this._summarizer === undefined,
4647
- 0x8f2 /* Summarizer never reconnects so should never resubmit */,
4726
+ canStageMessageOfType(message.type),
4727
+ 0xbbb /* Expected message type to be compatible with staging */,
4648
4728
  );
4729
+ switch (message.type) {
4730
+ case ContainerMessageType.FluidDataStoreOp: {
4731
+ this.channelCollection.reSubmit(
4732
+ message.type,
4733
+ message.contents,
4734
+ resubmitData.localOpMetadata,
4735
+ /* squash: */ true,
4736
+ );
4737
+ break;
4738
+ }
4739
+ // NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
4740
+ case ContainerMessageType.GC:
4741
+ case ContainerMessageType.DocumentSchemaChange: {
4742
+ this.reSubmit(resubmitData);
4743
+ break;
4744
+ }
4745
+ default: {
4746
+ unreachableCase(message.type);
4747
+ }
4748
+ }
4749
+ }
4750
+
4751
+ /**
4752
+ * Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
4753
+ * transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
4754
+ * How to resubmit is up to the subsystem that submitted the op to begin with
4755
+ */
4756
+ private reSubmit({
4757
+ runtimeOp: message,
4758
+ localOpMetadata,
4759
+ opMetadata,
4760
+ }: PendingMessageResubmitData): void {
4649
4761
  switch (message.type) {
4650
4762
  case ContainerMessageType.FluidDataStoreOp:
4651
4763
  case ContainerMessageType.Attach:
@@ -4656,7 +4768,7 @@ export class ContainerRuntime
4656
4768
  message.type,
4657
4769
  message.contents,
4658
4770
  localOpMetadata,
4659
- squash,
4771
+ /* squash: */ false,
4660
4772
  );
4661
4773
  break;
4662
4774
  }
@@ -4683,9 +4795,9 @@ export class ContainerRuntime
4683
4795
  break;
4684
4796
  }
4685
4797
  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.
4798
+ // We shouldn't directly resubmit due to Compare-And-Swap semantics.
4799
+ // If needed it will be generated from scratch before other ops are submitted.
4800
+ this.documentsSchemaController.pendingOpNotAcked();
4689
4801
  break;
4690
4802
  }
4691
4803
  default: {
@@ -4696,8 +4808,15 @@ export class ContainerRuntime
4696
4808
  }
4697
4809
  }
4698
4810
 
4699
- private rollback(runtimeOp: LocalContainerRuntimeMessage, localOpMetadata: unknown): void {
4700
- const { type, contents } = runtimeOp;
4811
+ /**
4812
+ * Rollback the given op which was only staged but not yet submitted.
4813
+ */
4814
+ private rollbackStagedChanges(
4815
+ { type, contents }: LocalContainerRuntimeMessage,
4816
+ localOpMetadata: unknown,
4817
+ ): void {
4818
+ assert(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
4819
+
4701
4820
  switch (type) {
4702
4821
  case ContainerMessageType.FluidDataStoreOp: {
4703
4822
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -4705,8 +4824,24 @@ export class ContainerRuntime
4705
4824
  this.channelCollection.rollback(type, contents, localOpMetadata);
4706
4825
  break;
4707
4826
  }
4827
+ case ContainerMessageType.GC: {
4828
+ // Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
4829
+ // Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
4830
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
4831
+ this.mc.logger.sendErrorEvent({
4832
+ eventName: "GC_OpDiscarded",
4833
+ details: { subType: contents.type },
4834
+ });
4835
+ break;
4836
+ }
4837
+ case ContainerMessageType.DocumentSchemaChange: {
4838
+ // Notify the document schema controller that the pending op was not acked.
4839
+ // This will allow it to propose the schema change again if needed.
4840
+ this.documentsSchemaController.pendingOpNotAcked();
4841
+ break;
4842
+ }
4708
4843
  default: {
4709
- throw new Error(`Can't rollback ${type}`);
4844
+ unreachableCase(type);
4710
4845
  }
4711
4846
  }
4712
4847
  }
@@ -4986,6 +5121,7 @@ export class ContainerRuntime
4986
5121
  },
4987
5122
  getQuorum: this.getQuorum.bind(this),
4988
5123
  getAudience: this.getAudience.bind(this),
5124
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
4989
5125
  } satisfies ExtensionHost<TRuntimeProperties>;
4990
5126
  entry = new factory(runtime, ...useContext);
4991
5127
  this.extensions.set(id, entry);
package/src/dataStore.ts CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  AliasResult,
12
12
  IDataStore,
13
13
  IFluidDataStoreChannel,
14
+ // eslint-disable-next-line import/no-deprecated
15
+ type IContainerRuntimeBaseExperimental,
14
16
  } from "@fluidframework/runtime-definitions/internal";
15
17
  import {
16
18
  ITelemetryLoggerExt,
@@ -78,6 +80,11 @@ class DataStore implements IDataStore {
78
80
  if (alias.includes("/")) {
79
81
  throw new UsageError(`The alias cannot contain slashes: '${alias}'`);
80
82
  }
83
+ // eslint-disable-next-line import/no-deprecated
84
+ const runtime = this.parentContext.containerRuntime as IContainerRuntimeBaseExperimental;
85
+ if (runtime.inStagingMode === true) {
86
+ throw new UsageError("Cannot set aliases while in staging mode");
87
+ }
81
88
 
82
89
  switch (this.aliasState) {
83
90
  // If we're already aliasing, check if it's for the same value and return
@@ -1045,6 +1045,8 @@ export class GarbageCollector implements IGarbageCollector {
1045
1045
 
1046
1046
  // Any time we log a Tombstone Loaded error (via Telemetry Tracker),
1047
1047
  // we want to also trigger autorecovery to avoid the object being deleted
1048
+ // i.e. this will be preceded by one of these telemetry events;
1049
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
1048
1050
  // Note: We don't need to trigger on "Changed" because any change will cause the object
1049
1051
  // to be loaded by the Summarizer, and auto-recovery will be triggered then.
1050
1052
  if (isTombstoned && reason === "Loaded") {
@@ -448,7 +448,7 @@ export interface IGarbageCollector {
448
448
  * Returns true if this node has been deleted by GC during sweep phase.
449
449
  */
450
450
  isNodeDeleted(nodePath: string): boolean;
451
- setConnectionState(connected: boolean, clientId?: string): void;
451
+ setConnectionState(canSendOps: boolean, clientId?: string): void;
452
452
  dispose(): void;
453
453
  }
454
454
 
package/src/index.ts CHANGED
@@ -95,7 +95,8 @@ export {
95
95
  IDocumentSchemaCurrent,
96
96
  currentDocumentVersionSchema,
97
97
  DocumentsSchemaController,
98
- IDocumentSchemaChangeMessage,
98
+ IDocumentSchemaChangeMessageIncoming,
99
+ IDocumentSchemaChangeMessageOutgoing,
99
100
  IDocumentSchemaFeatures,
100
101
  ReadFluidDataStoreAttributes,
101
102
  IFluidDataStoreAttributes0,