@fluidframework/container-runtime 2.40.0-336023 → 2.40.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 (92) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +4 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/blobManager/blobManager.d.ts +31 -8
  5. package/dist/blobManager/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager/blobManager.js +90 -17
  7. package/dist/blobManager/blobManager.js.map +1 -1
  8. package/dist/channelCollection.d.ts +8 -2
  9. package/dist/channelCollection.d.ts.map +1 -1
  10. package/dist/channelCollection.js +29 -6
  11. package/dist/channelCollection.js.map +1 -1
  12. package/dist/compatUtils.d.ts +19 -10
  13. package/dist/compatUtils.d.ts.map +1 -1
  14. package/dist/compatUtils.js +39 -32
  15. package/dist/compatUtils.js.map +1 -1
  16. package/dist/containerRuntime.d.ts +29 -13
  17. package/dist/containerRuntime.d.ts.map +1 -1
  18. package/dist/containerRuntime.js +139 -149
  19. package/dist/containerRuntime.js.map +1 -1
  20. package/dist/dataStoreContext.d.ts +12 -4
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +37 -18
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/legacy.d.ts +1 -0
  28. package/dist/opLifecycle/index.d.ts +1 -1
  29. package/dist/opLifecycle/index.d.ts.map +1 -1
  30. package/dist/opLifecycle/index.js.map +1 -1
  31. package/dist/opLifecycle/outbox.d.ts +20 -7
  32. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  33. package/dist/opLifecycle/outbox.js +16 -20
  34. package/dist/opLifecycle/outbox.js.map +1 -1
  35. package/dist/packageVersion.d.ts +1 -1
  36. package/dist/packageVersion.d.ts.map +1 -1
  37. package/dist/packageVersion.js +1 -1
  38. package/dist/packageVersion.js.map +1 -1
  39. package/dist/pendingStateManager.d.ts +22 -8
  40. package/dist/pendingStateManager.d.ts.map +1 -1
  41. package/dist/pendingStateManager.js +11 -16
  42. package/dist/pendingStateManager.js.map +1 -1
  43. package/lib/blobManager/blobManager.d.ts +31 -8
  44. package/lib/blobManager/blobManager.d.ts.map +1 -1
  45. package/lib/blobManager/blobManager.js +91 -18
  46. package/lib/blobManager/blobManager.js.map +1 -1
  47. package/lib/channelCollection.d.ts +8 -2
  48. package/lib/channelCollection.d.ts.map +1 -1
  49. package/lib/channelCollection.js +29 -6
  50. package/lib/channelCollection.js.map +1 -1
  51. package/lib/compatUtils.d.ts +19 -10
  52. package/lib/compatUtils.d.ts.map +1 -1
  53. package/lib/compatUtils.js +36 -29
  54. package/lib/compatUtils.js.map +1 -1
  55. package/lib/containerRuntime.d.ts +29 -13
  56. package/lib/containerRuntime.d.ts.map +1 -1
  57. package/lib/containerRuntime.js +60 -70
  58. package/lib/containerRuntime.js.map +1 -1
  59. package/lib/dataStoreContext.d.ts +12 -4
  60. package/lib/dataStoreContext.d.ts.map +1 -1
  61. package/lib/dataStoreContext.js +38 -19
  62. package/lib/dataStoreContext.js.map +1 -1
  63. package/lib/index.d.ts +1 -0
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js.map +1 -1
  66. package/lib/legacy.d.ts +1 -0
  67. package/lib/opLifecycle/index.d.ts +1 -1
  68. package/lib/opLifecycle/index.d.ts.map +1 -1
  69. package/lib/opLifecycle/index.js.map +1 -1
  70. package/lib/opLifecycle/outbox.d.ts +20 -7
  71. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  72. package/lib/opLifecycle/outbox.js +16 -20
  73. package/lib/opLifecycle/outbox.js.map +1 -1
  74. package/lib/packageVersion.d.ts +1 -1
  75. package/lib/packageVersion.d.ts.map +1 -1
  76. package/lib/packageVersion.js +1 -1
  77. package/lib/packageVersion.js.map +1 -1
  78. package/lib/pendingStateManager.d.ts +22 -8
  79. package/lib/pendingStateManager.d.ts.map +1 -1
  80. package/lib/pendingStateManager.js +11 -16
  81. package/lib/pendingStateManager.js.map +1 -1
  82. package/package.json +18 -18
  83. package/src/blobManager/blobManager.ts +141 -33
  84. package/src/channelCollection.ts +42 -6
  85. package/src/compatUtils.ts +53 -30
  86. package/src/containerRuntime.ts +102 -81
  87. package/src/dataStoreContext.ts +44 -25
  88. package/src/index.ts +1 -0
  89. package/src/opLifecycle/index.ts +1 -0
  90. package/src/opLifecycle/outbox.ts +42 -33
  91. package/src/packageVersion.ts +1 -1
  92. package/src/pendingStateManager.ts +37 -20
@@ -74,6 +74,11 @@ import type {
74
74
  SerializedIdCompressorWithNoSession,
75
75
  SerializedIdCompressorWithOngoingSession,
76
76
  } from "@fluidframework/id-compressor/internal";
77
+ import {
78
+ createIdCompressor,
79
+ createSessionId,
80
+ deserializeIdCompressor,
81
+ } from "@fluidframework/id-compressor/internal";
77
82
  import type {
78
83
  ISummaryTreeWithStats,
79
84
  ITelemetryContext,
@@ -158,10 +163,11 @@ import {
158
163
  wrapContext,
159
164
  } from "./channelCollection.js";
160
165
  import {
161
- defaultCompatibilityVersion,
162
- getCompatibilityVersionDefaults,
163
- isValidCompatVersion,
166
+ defaultMinVersionForCollab,
167
+ getMinVersionForCollabDefaults,
168
+ isValidMinVersionForCollab,
164
169
  type RuntimeOptionsAffectingDocSchema,
170
+ type MinimumVersionForCollab,
165
171
  } from "./compatUtils.js";
166
172
  import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
167
173
  import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
@@ -197,7 +203,6 @@ import {
197
203
  } from "./messageTypes.js";
198
204
  import { ISavedOpMetadata } from "./metadata.js";
199
205
  import {
200
- BatchId,
201
206
  LocalBatchMessage,
202
207
  BatchStartInfo,
203
208
  DuplicateBatchDetector,
@@ -210,12 +215,14 @@ import {
210
215
  Outbox,
211
216
  RemoteMessageProcessor,
212
217
  type OutboundBatch,
218
+ type BatchResubmitInfo,
213
219
  } from "./opLifecycle/index.js";
214
220
  import { pkgVersion } from "./packageVersion.js";
215
221
  import {
216
222
  PendingMessageResubmitData,
217
223
  IPendingLocalState,
218
224
  PendingStateManager,
225
+ type PendingBatchResubmitMetadata,
219
226
  } from "./pendingStateManager.js";
220
227
  import { RunCounter } from "./runCounter.js";
221
228
  import {
@@ -504,6 +511,8 @@ export const defaultRuntimeHeaderData: Required<RuntimeHeaderData> = {
504
511
  allowTombstone: false,
505
512
  };
506
513
 
514
+ const defaultStagingCommitOptions = { squash: false };
515
+
507
516
  /**
508
517
  * @deprecated
509
518
  * Untagged logger is unsupported going forward. There are old loaders with old ContainerContexts that only
@@ -702,6 +711,27 @@ export interface LoadContainerRuntimeParams {
702
711
  * @deprecated Will be removed once Loader LTS version is "2.0.0-internal.7.0.0". Migrate all usage of IFluidRouter to the "entryPoint" pattern. Refer to Removing-IFluidRouter.md
703
712
  * */
704
713
  requestHandler?: (request: IRequest, runtime: IContainerRuntime) => Promise<IResponse>;
714
+
715
+ /**
716
+ * Minimum version of the FF runtime that is required to collaborate on new documents.
717
+ * The input should be a string that represents the minimum version of the FF runtime that should be
718
+ * supported for collaboration. The format of the string must be in valid semver format.
719
+ *
720
+ * The inputted version will be used to determine the default configuration for
721
+ * {@link IContainerRuntimeOptionsInternal} to ensure compatibility with the specified version.
722
+ *
723
+ * @example
724
+ * minVersionForCollab: "2.0.0"
725
+ *
726
+ * @privateRemarks
727
+ * Used to determine the default configuration for {@link IContainerRuntimeOptionsInternal} that affect the document schema.
728
+ * For example, let's say that feature `foo` was added in 2.0 which introduces a new op type. Additionally, option `bar`
729
+ * was added to `IContainerRuntimeOptionsInternal` in 2.0 to enable/disable `foo` since clients prior to 2.0 would not
730
+ * understand the new op type. If a customer were to set minVersionForCollab to 2.0.0, then `bar` would be set to
731
+ * enable `foo` by default. If a customer were to set minVersionForCollab to 1.0.0, then `bar` would be set to
732
+ * disable `foo` by default.
733
+ */
734
+ minVersionForCollab?: MinimumVersionForCollab;
705
735
  }
706
736
  /**
707
737
  * This is meant to be used by a {@link @fluidframework/container-definitions#IRuntimeFactory} to instantiate a container runtime.
@@ -751,6 +781,7 @@ export class ContainerRuntime
751
781
  * - containerRuntimeCtor - Constructor to use to create the ContainerRuntime instance.
752
782
  * This allows mixin classes to leverage this method to define their own async initializer.
753
783
  * - provideEntryPoint - Promise that resolves to an object which will act as entryPoint for the Container.
784
+ * - minVersionForCollab - Minimum version of the FF runtime that this runtime supports collaboration with.
754
785
  * This object should provide all the functionality that the Container is expected to provide to the loader layer.
755
786
  */
756
787
  public static async loadRuntime(params: {
@@ -765,6 +796,7 @@ export class ContainerRuntime
765
796
  */
766
797
  requestHandler?: (request: IRequest, runtime: IContainerRuntime) => Promise<IResponse>;
767
798
  provideEntryPoint: (containerRuntime: IContainerRuntime) => Promise<FluidObject>;
799
+ minVersionForCollab?: MinimumVersionForCollab;
768
800
  }): Promise<ContainerRuntime> {
769
801
  const {
770
802
  context,
@@ -775,6 +807,7 @@ export class ContainerRuntime
775
807
  runtimeOptions = {} satisfies IContainerRuntimeOptionsInternal,
776
808
  containerScope = {},
777
809
  containerRuntimeCtor = ContainerRuntime,
810
+ minVersionForCollab = defaultMinVersionForCollab,
778
811
  } = params;
779
812
 
780
813
  // If taggedLogger exists, use it. Otherwise, wrap the vanilla logger:
@@ -796,23 +829,21 @@ export class ContainerRuntime
796
829
  const mc = loggerToMonitoringContext(logger);
797
830
 
798
831
  // Some options require a minimum version of the FF runtime to operate, so the default configs will be generated
799
- // based on the compatibility mode.
800
- // For example, if compatibility mode is set to "1.0.0", the default configs will ensure compatibility with FF runtime
801
- // 1.0.0 or later. If the compatibility mode is set to "2.10.0", the default values will be generated to ensure compatibility
832
+ // based on the minVersionForCollab.
833
+ // For example, if minVersionForCollab is set to "1.0.0", the default configs will ensure compatibility with FF runtime
834
+ // 1.0.0 or later. If the minVersionForCollab is set to "2.10.0", the default values will be generated to ensure compatibility
802
835
  // with FF runtime 2.10.0 or later.
803
- // TODO: We will add in a way for users to pass in compatibilityVersion in a follow up PR.
804
- const compatibilityVersion = defaultCompatibilityVersion;
805
- if (!isValidCompatVersion(compatibilityVersion)) {
836
+ if (!isValidMinVersionForCollab(minVersionForCollab)) {
806
837
  throw new UsageError(
807
- `Invalid compatibility version: ${compatibilityVersion}. It must be an existing FF version (i.e. 2.22.1).`,
838
+ `Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`,
808
839
  );
809
840
  }
810
- const defaultVersionDependentConfigs =
811
- getCompatibilityVersionDefaults(compatibilityVersion);
841
+ const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);
812
842
 
813
843
  // The following are the default values for the options that do not affect the DocumentSchema.
814
- const defaultConfigsNonVersionDependent: Required<
815
- Omit<IContainerRuntimeOptionsInternal, keyof RuntimeOptionsAffectingDocSchema>
844
+ const defaultsNotAffectingDocSchema: Omit<
845
+ ContainerRuntimeOptionsInternal,
846
+ keyof RuntimeOptionsAffectingDocSchema
816
847
  > = {
817
848
  summaryOptions: {},
818
849
  loadSequenceNumberVerification: "close",
@@ -821,8 +852,8 @@ export class ContainerRuntime
821
852
  };
822
853
 
823
854
  const defaultConfigs = {
824
- ...defaultVersionDependentConfigs,
825
- ...defaultConfigsNonVersionDependent,
855
+ ...defaultsAffectingDocSchema,
856
+ ...defaultsNotAffectingDocSchema,
826
857
  };
827
858
 
828
859
  // Here we set each option to its corresponding default config value if it's not provided in runtimeOptions.
@@ -976,11 +1007,7 @@ export class ContainerRuntime
976
1007
  idCompressorMode = desiredIdCompressorMode;
977
1008
  }
978
1009
 
979
- const createIdCompressorFn = async (): Promise<IIdCompressor & IIdCompressorCore> => {
980
- const { createIdCompressor, deserializeIdCompressor, createSessionId } = await import(
981
- "@fluidframework/id-compressor/internal"
982
- );
983
-
1010
+ const createIdCompressorFn = (): IIdCompressor & IIdCompressorCore => {
984
1011
  /**
985
1012
  * Because the IdCompressor emits so much telemetry, this function is used to sample
986
1013
  * approximately 5% of all clients. Only the given percentage of sessions will emit telemetry.
@@ -1072,6 +1099,7 @@ export class ContainerRuntime
1072
1099
  documentSchemaController,
1073
1100
  featureGatesForTelemetry,
1074
1101
  provideEntryPoint,
1102
+ minVersionForCollab,
1075
1103
  requestHandler,
1076
1104
  undefined, // summaryConfiguration
1077
1105
  recentBatchInfo,
@@ -1190,12 +1218,6 @@ export class ContainerRuntime
1190
1218
  }
1191
1219
  }
1192
1220
 
1193
- /**
1194
- * True if we have ID compressor loading in-flight (async operation). Useful only for
1195
- * this.sessionSchema.idCompressorMode === "delayed" mode
1196
- */
1197
- protected _loadIdCompressor: Promise<void> | undefined;
1198
-
1199
1221
  /**
1200
1222
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.generateDocumentUniqueId}
1201
1223
  */
@@ -1401,11 +1423,12 @@ export class ContainerRuntime
1401
1423
 
1402
1424
  blobManagerLoadInfo: IBlobManagerLoadInfo,
1403
1425
  private readonly _storage: IDocumentStorageService,
1404
- private readonly createIdCompressor: () => Promise<IIdCompressor & IIdCompressorCore>,
1426
+ private readonly createIdCompressorFn: () => IIdCompressor & IIdCompressorCore,
1405
1427
 
1406
1428
  private readonly documentsSchemaController: DocumentsSchemaController,
1407
1429
  featureGatesForTelemetry: Record<string, boolean | number | undefined>,
1408
1430
  provideEntryPoint: (containerRuntime: IContainerRuntime) => Promise<FluidObject>,
1431
+ private readonly minVersionForCollab: MinimumVersionForCollab,
1409
1432
  private readonly requestHandler?: (
1410
1433
  request: IRequest,
1411
1434
  runtime: IContainerRuntime,
@@ -1914,6 +1937,7 @@ export class ContainerRuntime
1914
1937
  telemetryDocumentId: this.telemetryDocumentId,
1915
1938
  groupedBatchingEnabled: this.groupedBatchingEnabled,
1916
1939
  initialSequenceNumber: this.deltaManager.initialSequenceNumber,
1940
+ minVersionForCollab: this.minVersionForCollab,
1917
1941
  });
1918
1942
 
1919
1943
  ReportOpPerfTelemetry(this.clientId, this._deltaManager, this, this.baseLogger);
@@ -1946,7 +1970,6 @@ export class ContainerRuntime
1946
1970
  // As it's implemented right now (with async initialization), this will only work for "off" -> "delayed" transitions.
1947
1971
  // Anything else is too risky, and requires ability to initialize ID compressor synchronously!
1948
1972
  if (schema.runtime.idCompressorMode !== undefined) {
1949
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1950
1973
  this.loadIdCompressor();
1951
1974
  }
1952
1975
  }
@@ -1994,7 +2017,7 @@ export class ContainerRuntime
1994
2017
  this.sessionSchema.idCompressorMode === "on" ||
1995
2018
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)
1996
2019
  ) {
1997
- this._idCompressor = await this.createIdCompressor();
2020
+ this._idCompressor = this.createIdCompressorFn();
1998
2021
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
1999
2022
  assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
2000
2023
  }
@@ -2592,29 +2615,20 @@ export class ContainerRuntime
2592
2615
  }
2593
2616
  }
2594
2617
 
2595
- private async loadIdCompressor(): Promise<void | undefined> {
2618
+ private loadIdCompressor(): void {
2596
2619
  if (
2597
2620
  this._idCompressor === undefined &&
2598
- this.sessionSchema.idCompressorMode !== undefined &&
2599
- this._loadIdCompressor === undefined
2621
+ this.sessionSchema.idCompressorMode !== undefined
2600
2622
  ) {
2601
- this._loadIdCompressor = this.createIdCompressor()
2602
- .then((compressor) => {
2603
- // Finalize any ranges we received while the compressor was turned off.
2604
- const ops = this.pendingIdCompressorOps;
2605
- this.pendingIdCompressorOps = [];
2606
- for (const range of ops) {
2607
- compressor.finalizeCreationRange(range);
2608
- }
2609
- assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
2610
- this._idCompressor = compressor;
2611
- })
2612
- .catch((error) => {
2613
- this.mc.logger.sendErrorEvent({ eventName: "IdCompressorDelayedLoad" }, error);
2614
- throw error;
2615
- });
2623
+ this._idCompressor = this.createIdCompressorFn();
2624
+ // Finalize any ranges we received while the compressor was turned off.
2625
+ const ops = this.pendingIdCompressorOps;
2626
+ this.pendingIdCompressorOps = [];
2627
+ for (const range of ops) {
2628
+ this._idCompressor.finalizeCreationRange(range);
2629
+ }
2630
+ assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
2616
2631
  }
2617
- return this._loadIdCompressor;
2618
2632
  }
2619
2633
 
2620
2634
  private readonly notifyReadOnlyState = (readonly: boolean): void =>
@@ -2630,7 +2644,6 @@ export class ContainerRuntime
2630
2644
  );
2631
2645
 
2632
2646
  if (connected && this.sessionSchema.idCompressorMode === "delayed") {
2633
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2634
2647
  this.loadIdCompressor();
2635
2648
  }
2636
2649
  if (connected === false && this.delayConnectClientId !== undefined) {
@@ -3190,23 +3203,20 @@ export class ContainerRuntime
3190
3203
  }
3191
3204
 
3192
3205
  /**
3193
- * Flush the pending ops manually.
3194
- * This method is expected to be called at the end of a batch.
3206
+ * Flush the current batch of ops to the ordering service for sequencing
3207
+ * This method is not expected to be called in the middle of a batch.
3195
3208
  * @remarks - If it throws (e.g. if the batch is too large to send), the container will be closed.
3196
3209
  *
3197
- * @param resubmittingBatchId - If defined, indicates this is a resubmission of a batch
3198
- * with the given Batch ID, which must be preserved
3199
- * @param resubmittingStagedBatch - If defined, indicates this is a resubmission of a batch that is staged,
3200
- * meaning it should not be sent to the ordering service yet.
3210
+ * @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
3201
3211
  */
3202
- private flush(resubmittingBatchId?: BatchId, resubmittingStagedBatch?: boolean): void {
3212
+ private flush(resubmitInfo?: BatchResubmitInfo): void {
3203
3213
  try {
3204
3214
  assert(
3205
3215
  !this.batchRunner.running,
3206
3216
  0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */,
3207
3217
  );
3208
3218
 
3209
- this.outbox.flush(resubmittingBatchId, resubmittingStagedBatch);
3219
+ this.outbox.flush(resubmitInfo);
3210
3220
  assert(this.outbox.isEmpty, 0x3cf /* reentrancy */);
3211
3221
  } catch (error) {
3212
3222
  const error2 = normalizeError(error, {
@@ -3282,7 +3292,8 @@ export class ContainerRuntime
3282
3292
  throw error; // throw the original error for the consumer of the runtime
3283
3293
  }
3284
3294
  });
3285
- stageControls?.commitChanges();
3295
+
3296
+ stageControls?.commitChanges({ squash: false });
3286
3297
 
3287
3298
  // We don't flush on TurnBased since we expect all messages in the same JS turn to be part of the same batch
3288
3299
  if (this.flushMode !== FlushMode.TurnBased && !this.batchRunner.running) {
@@ -3318,16 +3329,19 @@ export class ContainerRuntime
3318
3329
  // Make sure all BatchManagers are empty before entering staging mode,
3319
3330
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
3320
3331
  this.outbox.flush();
3332
+
3321
3333
  const exitStagingMode = (discardOrCommit: () => void) => (): void => {
3322
3334
  // Final flush of any last staged changes
3323
- this.outbox.flush(undefined, true /* staged */);
3335
+ this.outbox.flush();
3324
3336
 
3325
3337
  this.stageControls = undefined;
3326
3338
 
3327
3339
  discardOrCommit();
3340
+ this.channelCollection.notifyStagingMode(false);
3328
3341
  };
3329
3342
 
3330
- const stageControls = {
3343
+ // eslint-disable-next-line import/no-deprecated
3344
+ const stageControls: StageControlsExperimental = {
3331
3345
  discardChanges: exitStagingMode(() => {
3332
3346
  // Pop all staged batches from the PSM and roll them back in LIFO order
3333
3347
  this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
@@ -3341,18 +3355,21 @@ export class ContainerRuntime
3341
3355
  this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
3342
3356
  }
3343
3357
  }),
3344
- commitChanges: exitStagingMode(() => {
3345
- // All staged changes are in the PSM, so just replay them (ignore pre-staging batches)
3346
- // FUTURE: Have this do squash-rebase instead of resubmitting all intermediate changes
3347
- if (this.connected) {
3348
- this.pendingStateManager.replayPendingStates(true /* onlyStagedBatched */);
3349
- } else {
3350
- this.pendingStateManager.clearStagingFlags();
3351
- }
3352
- }),
3358
+ commitChanges: (optionsParam) => {
3359
+ const options = { ...defaultStagingCommitOptions, ...optionsParam };
3360
+ return exitStagingMode(() => {
3361
+ this.pendingStateManager.replayPendingStates({
3362
+ onlyStagedBatches: true,
3363
+ squash: options.squash ?? false,
3364
+ });
3365
+ })();
3366
+ },
3353
3367
  };
3354
3368
 
3355
- return (this.stageControls = stageControls);
3369
+ this.stageControls = stageControls;
3370
+ this.channelCollection.notifyStagingMode(true);
3371
+
3372
+ return this.stageControls;
3356
3373
  };
3357
3374
 
3358
3375
  /**
@@ -3587,8 +3604,7 @@ export class ContainerRuntime
3587
3604
  wrapSummaryInChannelsTree(summarizeResult);
3588
3605
  const pathPartsForChildren = [channelsTreeName];
3589
3606
 
3590
- // Ensure that ID compressor had a chance to load, if we are using delayed mode.
3591
- await this.loadIdCompressor();
3607
+ this.loadIdCompressor();
3592
3608
 
3593
3609
  this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
3594
3610
  return {
@@ -4547,22 +4563,21 @@ export class ContainerRuntime
4547
4563
  */
4548
4564
  private reSubmitBatch(
4549
4565
  batch: PendingMessageResubmitData[],
4550
- batchId: BatchId,
4551
- staged: boolean,
4566
+ { batchId, staged, squash }: PendingBatchResubmitMetadata,
4552
4567
  ): void {
4553
4568
  this.batchRunner.run(() => {
4554
4569
  for (const message of batch) {
4555
- this.reSubmit(message);
4570
+ this.reSubmit(message, squash);
4556
4571
  }
4557
4572
  });
4558
4573
 
4559
4574
  // Only include Batch ID if "Offline Load" feature is enabled
4560
4575
  // It's only needed to identify batches across container forks arising from misuse of offline load.
4561
- this.flush(this.offlineEnabled ? batchId : undefined, staged);
4576
+ this.flush({ batchId: this.offlineEnabled ? batchId : undefined, staged });
4562
4577
  }
4563
4578
 
4564
- private reSubmit(message: PendingMessageResubmitData): void {
4565
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata);
4579
+ private reSubmit(message: PendingMessageResubmitData, squash: boolean): void {
4580
+ this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
4566
4581
  }
4567
4582
 
4568
4583
  /**
@@ -4576,6 +4591,7 @@ export class ContainerRuntime
4576
4591
  message: LocalContainerRuntimeMessage,
4577
4592
  localOpMetadata: unknown,
4578
4593
  opMetadata: Record<string, unknown> | undefined,
4594
+ squash: boolean,
4579
4595
  ): void {
4580
4596
  assert(
4581
4597
  this._summarizer === undefined,
@@ -4587,7 +4603,12 @@ export class ContainerRuntime
4587
4603
  case ContainerMessageType.Alias: {
4588
4604
  // For Operations, call resubmitDataStoreOp which will find the right store
4589
4605
  // and trigger resubmission on it.
4590
- this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata);
4606
+ this.channelCollection.reSubmit(
4607
+ message.type,
4608
+ message.contents,
4609
+ localOpMetadata,
4610
+ squash,
4611
+ );
4591
4612
  break;
4592
4613
  }
4593
4614
  case ContainerMessageType.IdAllocation: {
@@ -15,7 +15,6 @@ import {
15
15
  FluidObject,
16
16
  IDisposable,
17
17
  ITelemetryBaseProperties,
18
- type IErrorBase,
19
18
  type IEvent,
20
19
  } from "@fluidframework/core-interfaces";
21
20
  import {
@@ -95,7 +94,6 @@ import {
95
94
  getAttributesFormatVersion,
96
95
  getFluidDataStoreAttributes,
97
96
  hasIsolatedChannels,
98
- summarizerClientType,
99
97
  wrapSummaryInChannelsTree,
100
98
  } from "./summary/index.js";
101
99
 
@@ -225,17 +223,14 @@ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy {
225
223
  }
226
224
 
227
225
  /**
228
- * Called by the owning datastore context to configure the readonly
229
- * state of the delta manger that is project down to the datastore
226
+ * Called by the owning datastore context to emit the readonly
227
+ * event on the delta manger that is projected down to the datastore
230
228
  * runtime. This state may not align with that of the true delta
231
229
  * manager if the context wishes to control the read only state
232
230
  * differently than the delta manager itself.
233
231
  */
234
- public setReadonly(
235
- readonly: boolean,
236
- readonlyConnectionReason?: { reason: string; error?: IErrorBase },
237
- ): void {
238
- this.emit("readonly", readonly, readonlyConnectionReason);
232
+ public emitReadonly(): void {
233
+ this.emit("readonly", this.isReadOnly());
239
234
  }
240
235
  }
241
236
 
@@ -272,7 +267,10 @@ export abstract class FluidDataStoreContext
272
267
  return this._contextDeltaManagerProxy;
273
268
  }
274
269
 
275
- public isReadOnly = (): boolean => this.parentContext.isReadOnly();
270
+ private isStagingMode: boolean = false;
271
+ public isReadOnly = (): boolean =>
272
+ (this.isStagingMode && this.channel?.policies?.readonlyInStagingMode !== false) ||
273
+ this.parentContext.isReadOnly();
276
274
 
277
275
  public get connected(): boolean {
278
276
  return this.parentContext.connected;
@@ -679,11 +677,28 @@ export abstract class FluidDataStoreContext
679
677
  this.channel!.setConnectionState(connected, clientId);
680
678
  }
681
679
 
682
- public notifyReadOnlyState(readonly: boolean): void {
680
+ public notifyReadOnlyState(): void {
683
681
  this.verifyNotClosed("notifyReadOnlyState", false /* checkTombstone */);
684
682
 
685
- this.channel?.notifyReadOnlyState?.(readonly);
686
- this._contextDeltaManagerProxy.setReadonly(readonly);
683
+ // These two calls achieve the same purpose, and are both needed for a time for back compat
684
+ this.channel?.notifyReadOnlyState?.(this.isReadOnly());
685
+ this._contextDeltaManagerProxy.emitReadonly();
686
+ }
687
+
688
+ /**
689
+ * Updates the readonly state of the data store based on the staging mode.
690
+ *
691
+ * @param staging - A boolean indicating whether the container is in staging mode.
692
+ * If true, the data store is set to readonly unless explicitly allowed by its policies.
693
+ */
694
+ public notifyStagingMode(staging: boolean): void {
695
+ // If the `readonlyInStagingMode` policy is not explicitly set to `false`,
696
+ // the data store is treated as readonly in staging mode.
697
+ const oldReadOnlyState = this.isReadOnly();
698
+ this.isStagingMode = staging;
699
+ if (this.isReadOnly() !== oldReadOnlyState) {
700
+ this.notifyReadOnlyState();
701
+ }
687
702
  }
688
703
 
689
704
  /**
@@ -868,8 +883,8 @@ export abstract class FluidDataStoreContext
868
883
  public submitMessage(type: string, content: unknown, localOpMetadata: unknown): void {
869
884
  this.verifyNotClosed("submitMessage");
870
885
  assert(!!this.channel, 0x146 /* "Channel must exist when submitting message" */);
871
- // Summarizer clients should not submit messages.
872
- this.identifyLocalChangeInSummarizer("DataStoreMessageSubmittedInSummarizer", type);
886
+ // Readonly clients should not submit messages.
887
+ this.identifyLocalChangeIfReadonly("DataStoreMessageWhileReadonly", type);
873
888
 
874
889
  this.parentContext.submitMessage(type, content, localOpMetadata);
875
890
  }
@@ -1027,9 +1042,14 @@ export abstract class FluidDataStoreContext
1027
1042
  return {};
1028
1043
  }
1029
1044
 
1030
- public reSubmit(type: string, contents: unknown, localOpMetadata: unknown): void {
1045
+ public reSubmit(
1046
+ type: string,
1047
+ contents: unknown,
1048
+ localOpMetadata: unknown,
1049
+ squash: boolean,
1050
+ ): void {
1031
1051
  assert(!!this.channel, 0x14b /* "Channel must exist when resubmitting ops" */);
1032
- this.channel.reSubmit(type, contents, localOpMetadata);
1052
+ this.channel.reSubmit(type, contents, localOpMetadata, squash);
1033
1053
  }
1034
1054
 
1035
1055
  public rollback(type: string, contents: unknown, localOpMetadata: unknown): void {
@@ -1100,19 +1120,16 @@ export abstract class FluidDataStoreContext
1100
1120
  }
1101
1121
 
1102
1122
  /**
1103
- * Summarizer client should not have local changes. These changes can become part of the summary and can break
1123
+ * Readonly client, including summarizer, should not have local changes. These changes can become part of the summary and can break
1104
1124
  * eventual consistency. For example, the next summary (say at ref seq# 100) may contain these changes whereas
1105
1125
  * other clients that are up-to-date till seq# 100 may not have them yet.
1106
1126
  */
1107
- protected identifyLocalChangeInSummarizer(eventName: string, type?: string): void {
1108
- if (
1109
- this.clientDetails.type !== summarizerClientType ||
1110
- this.localChangesTelemetryCount <= 0
1111
- ) {
1127
+ protected identifyLocalChangeIfReadonly(eventName: string, type?: string): void {
1128
+ if (!this.isReadOnly() || this.localChangesTelemetryCount <= 0) {
1112
1129
  return;
1113
1130
  }
1114
1131
 
1115
- // Log a telemetry if there are local changes in the summarizer. This will give us data on how often
1132
+ // Log a telemetry if there are local changes in readonly. This will give us data on how often
1116
1133
  // this is happening and which data stores do this. The eventual goal is to disallow local changes
1117
1134
  // in the summarizer and the data will help us plan this.
1118
1135
  this.mc.logger.sendTelemetryEvent({
@@ -1120,6 +1137,8 @@ export abstract class FluidDataStoreContext
1120
1137
  type,
1121
1138
  isSummaryInProgress: this.summarizerNode.isSummaryInProgress?.(),
1122
1139
  stack: generateStack(30),
1140
+ readonly: this.isReadOnly(),
1141
+ isStagingMode: this.isStagingMode,
1123
1142
  });
1124
1143
  this.localChangesTelemetryCount--;
1125
1144
  }
@@ -1321,7 +1340,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
1321
1340
  );
1322
1341
 
1323
1342
  // Summarizer client should not create local data stores.
1324
- this.identifyLocalChangeInSummarizer("DataStoreCreatedInSummarizer");
1343
+ this.identifyLocalChangeIfReadonly("DataStoreCreatedWhileReadonly");
1325
1344
 
1326
1345
  this.snapshotTree = props.snapshotTree;
1327
1346
  }
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export {
31
31
  ChannelCollectionFactory,
32
32
  AllowTombstoneRequestHeaderKey,
33
33
  } from "./channelCollection.js";
34
+ export type { MinimumVersionForCollab } from "./compatUtils.js";
34
35
  export {
35
36
  GCNodeType,
36
37
  IGCMetadata,
@@ -27,6 +27,7 @@ export {
27
27
  ensureContentsDeserialized,
28
28
  } from "./opSerialization.js";
29
29
  export {
30
+ BatchResubmitInfo,
30
31
  estimateSocketSize,
31
32
  localBatchToOutboundBatch,
32
33
  Outbox,