@fluidframework/container-runtime 2.33.2 → 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 +26 -10
  9. package/dist/channelCollection.d.ts.map +1 -1
  10. package/dist/channelCollection.js +42 -11
  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 +15 -16
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +37 -19
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/index.d.ts +1 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -9
  27. package/dist/index.js.map +1 -1
  28. package/dist/legacy.d.ts +1 -0
  29. package/dist/opLifecycle/index.d.ts +1 -1
  30. package/dist/opLifecycle/index.d.ts.map +1 -1
  31. package/dist/opLifecycle/index.js.map +1 -1
  32. package/dist/opLifecycle/outbox.d.ts +20 -7
  33. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  34. package/dist/opLifecycle/outbox.js +16 -20
  35. package/dist/opLifecycle/outbox.js.map +1 -1
  36. package/dist/packageVersion.d.ts +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 +26 -10
  48. package/lib/channelCollection.d.ts.map +1 -1
  49. package/lib/channelCollection.js +43 -12
  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 +15 -16
  60. package/lib/dataStoreContext.d.ts.map +1 -1
  61. package/lib/dataStoreContext.js +38 -20
  62. package/lib/dataStoreContext.js.map +1 -1
  63. package/lib/index.d.ts +1 -3
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js +0 -3
  66. package/lib/index.js.map +1 -1
  67. package/lib/legacy.d.ts +1 -0
  68. package/lib/opLifecycle/index.d.ts +1 -1
  69. package/lib/opLifecycle/index.d.ts.map +1 -1
  70. package/lib/opLifecycle/index.js.map +1 -1
  71. package/lib/opLifecycle/outbox.d.ts +20 -7
  72. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  73. package/lib/opLifecycle/outbox.js +16 -20
  74. package/lib/opLifecycle/outbox.js.map +1 -1
  75. package/lib/packageVersion.d.ts +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 +77 -19
  85. package/src/compatUtils.ts +53 -30
  86. package/src/containerRuntime.ts +102 -81
  87. package/src/dataStoreContext.ts +48 -38
  88. package/src/index.ts +1 -13
  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
@@ -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 {
@@ -52,7 +51,6 @@ import {
52
51
  IFluidDataStoreContext,
53
52
  IFluidDataStoreContextDetached,
54
53
  IFluidDataStoreRegistry,
55
- IFluidParentContext,
56
54
  IGarbageCollectionDetailsBase,
57
55
  IProvideFluidDataStoreFactory,
58
56
  ISummarizeInternalResult,
@@ -64,6 +62,7 @@ import {
64
62
  type IPendingMessagesState,
65
63
  type IRuntimeMessageCollection,
66
64
  type IFluidDataStoreFactory,
65
+ type IFluidParentContext,
67
66
  } from "@fluidframework/runtime-definitions/internal";
68
67
  import {
69
68
  addBlobToSummary,
@@ -81,6 +80,7 @@ import {
81
80
  tagCodeArtifacts,
82
81
  } from "@fluidframework/telemetry-utils/internal";
83
82
 
83
+ import type { IFluidParentContextPrivate } from "./channelCollection.js";
84
84
  import { BaseDeltaManagerProxy } from "./deltaManagerProxies.js";
85
85
  import {
86
86
  runtimeCompatDetailsForDataStore,
@@ -94,7 +94,6 @@ import {
94
94
  getAttributesFormatVersion,
95
95
  getFluidDataStoreAttributes,
96
96
  hasIsolatedChannels,
97
- summarizerClientType,
98
97
  wrapSummaryInChannelsTree,
99
98
  } from "./summary/index.js";
100
99
 
@@ -117,9 +116,6 @@ export function createAttributesBlob(
117
116
  return new BlobTreeEntry(dataStoreAttributesBlobName, JSON.stringify(attributes));
118
117
  }
119
118
 
120
- /**
121
- * @internal
122
- */
123
119
  export interface ISnapshotDetails {
124
120
  pkg: readonly string[];
125
121
  isRootDataStore: boolean;
@@ -131,7 +127,6 @@ export interface ISnapshotDetails {
131
127
  * This is interface that every context should implement.
132
128
  * This interface is used for context's parent - ChannelCollection.
133
129
  * It should not be exposed to any other users of context.
134
- * @internal
135
130
  */
136
131
  export interface IFluidDataStoreContextInternal extends IFluidDataStoreContext {
137
132
  getAttachSummary(telemetryContext?: ITelemetryContext): ISummaryTreeWithStats;
@@ -147,11 +142,10 @@ export interface IFluidDataStoreContextInternal extends IFluidDataStoreContext {
147
142
 
148
143
  /**
149
144
  * Properties necessary for creating a FluidDataStoreContext
150
- * @internal
151
145
  */
152
146
  export interface IFluidDataStoreContextProps {
153
147
  readonly id: string;
154
- readonly parentContext: IFluidParentContext;
148
+ readonly parentContext: IFluidParentContextPrivate;
155
149
  readonly storage: IDocumentStorageService;
156
150
  readonly scope: FluidObject;
157
151
  readonly createSummarizerNodeFn: CreateChildSummarizerNodeFn;
@@ -161,7 +155,6 @@ export interface IFluidDataStoreContextProps {
161
155
 
162
156
  /**
163
157
  * Properties necessary for creating a local FluidDataStoreContext
164
- * @internal
165
158
  */
166
159
  export interface ILocalFluidDataStoreContextProps extends IFluidDataStoreContextProps {
167
160
  readonly pkg: Readonly<string[]> | undefined;
@@ -171,7 +164,6 @@ export interface ILocalFluidDataStoreContextProps extends IFluidDataStoreContext
171
164
 
172
165
  /**
173
166
  * Properties necessary for creating a local FluidDataStoreContext
174
- * @internal
175
167
  */
176
168
  export interface ILocalDetachedFluidDataStoreContextProps
177
169
  extends ILocalFluidDataStoreContextProps {
@@ -180,7 +172,6 @@ export interface ILocalDetachedFluidDataStoreContextProps
180
172
 
181
173
  /**
182
174
  * Properties necessary for creating a remote FluidDataStoreContext
183
- * @internal
184
175
  */
185
176
  export interface IRemoteFluidDataStoreContextProps extends IFluidDataStoreContextProps {
186
177
  readonly snapshot: ISnapshotTree | ISnapshot | undefined;
@@ -189,7 +180,6 @@ export interface IRemoteFluidDataStoreContextProps extends IFluidDataStoreContex
189
180
  // back-compat: To be removed in the future.
190
181
  // Added in "2.0.0-rc.2.0.0" timeframe (to support older builds).
191
182
  /**
192
- * @internal
193
183
  */
194
184
  export interface IFluidDataStoreContextEvents extends IEvent {
195
185
  (event: "attaching" | "attached", listener: () => void);
@@ -233,23 +223,19 @@ class ContextDeltaManagerProxy extends BaseDeltaManagerProxy {
233
223
  }
234
224
 
235
225
  /**
236
- * Called by the owning datastore context to configure the readonly
237
- * 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
238
228
  * runtime. This state may not align with that of the true delta
239
229
  * manager if the context wishes to control the read only state
240
230
  * differently than the delta manager itself.
241
231
  */
242
- public setReadonly(
243
- readonly: boolean,
244
- readonlyConnectionReason?: { reason: string; error?: IErrorBase },
245
- ): void {
246
- this.emit("readonly", readonly, readonlyConnectionReason);
232
+ public emitReadonly(): void {
233
+ this.emit("readonly", this.isReadOnly());
247
234
  }
248
235
  }
249
236
 
250
237
  /**
251
238
  * Represents the context for the store. This context is passed to the store runtime.
252
- * @internal
253
239
  */
254
240
  export abstract class FluidDataStoreContext
255
241
  extends TypedEventEmitter<IFluidDataStoreContextEvents>
@@ -281,7 +267,10 @@ export abstract class FluidDataStoreContext
281
267
  return this._contextDeltaManagerProxy;
282
268
  }
283
269
 
284
- public isReadOnly = (): boolean => this.parentContext.isReadOnly?.() ?? false;
270
+ private isStagingMode: boolean = false;
271
+ public isReadOnly = (): boolean =>
272
+ (this.isStagingMode && this.channel?.policies?.readonlyInStagingMode !== false) ||
273
+ this.parentContext.isReadOnly();
285
274
 
286
275
  public get connected(): boolean {
287
276
  return this.parentContext.connected;
@@ -424,7 +413,7 @@ export abstract class FluidDataStoreContext
424
413
 
425
414
  public readonly id: string;
426
415
  private readonly _containerRuntime: IContainerRuntimeBase;
427
- private readonly parentContext: IFluidParentContext;
416
+ private readonly parentContext: IFluidParentContextPrivate;
428
417
  public readonly storage: IDocumentStorageService;
429
418
  public readonly scope: FluidObject;
430
419
  // Represents the group to which the data store belongs too.
@@ -688,11 +677,28 @@ export abstract class FluidDataStoreContext
688
677
  this.channel!.setConnectionState(connected, clientId);
689
678
  }
690
679
 
691
- public notifyReadOnlyState(readonly: boolean): void {
680
+ public notifyReadOnlyState(): void {
692
681
  this.verifyNotClosed("notifyReadOnlyState", false /* checkTombstone */);
693
682
 
694
- this.channel?.notifyReadOnlyState?.(readonly);
695
- 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
+ }
696
702
  }
697
703
 
698
704
  /**
@@ -877,8 +883,8 @@ export abstract class FluidDataStoreContext
877
883
  public submitMessage(type: string, content: unknown, localOpMetadata: unknown): void {
878
884
  this.verifyNotClosed("submitMessage");
879
885
  assert(!!this.channel, 0x146 /* "Channel must exist when submitting message" */);
880
- // Summarizer clients should not submit messages.
881
- this.identifyLocalChangeInSummarizer("DataStoreMessageSubmittedInSummarizer", type);
886
+ // Readonly clients should not submit messages.
887
+ this.identifyLocalChangeIfReadonly("DataStoreMessageWhileReadonly", type);
882
888
 
883
889
  this.parentContext.submitMessage(type, content, localOpMetadata);
884
890
  }
@@ -1036,9 +1042,14 @@ export abstract class FluidDataStoreContext
1036
1042
  return {};
1037
1043
  }
1038
1044
 
1039
- 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 {
1040
1051
  assert(!!this.channel, 0x14b /* "Channel must exist when resubmitting ops" */);
1041
- this.channel.reSubmit(type, contents, localOpMetadata);
1052
+ this.channel.reSubmit(type, contents, localOpMetadata, squash);
1042
1053
  }
1043
1054
 
1044
1055
  public rollback(type: string, contents: unknown, localOpMetadata: unknown): void {
@@ -1109,19 +1120,16 @@ export abstract class FluidDataStoreContext
1109
1120
  }
1110
1121
 
1111
1122
  /**
1112
- * 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
1113
1124
  * eventual consistency. For example, the next summary (say at ref seq# 100) may contain these changes whereas
1114
1125
  * other clients that are up-to-date till seq# 100 may not have them yet.
1115
1126
  */
1116
- protected identifyLocalChangeInSummarizer(eventName: string, type?: string): void {
1117
- if (
1118
- this.clientDetails.type !== summarizerClientType ||
1119
- this.localChangesTelemetryCount <= 0
1120
- ) {
1127
+ protected identifyLocalChangeIfReadonly(eventName: string, type?: string): void {
1128
+ if (!this.isReadOnly() || this.localChangesTelemetryCount <= 0) {
1121
1129
  return;
1122
1130
  }
1123
1131
 
1124
- // 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
1125
1133
  // this is happening and which data stores do this. The eventual goal is to disallow local changes
1126
1134
  // in the summarizer and the data will help us plan this.
1127
1135
  this.mc.logger.sendTelemetryEvent({
@@ -1129,6 +1137,8 @@ export abstract class FluidDataStoreContext
1129
1137
  type,
1130
1138
  isSummaryInProgress: this.summarizerNode.isSummaryInProgress?.(),
1131
1139
  stack: generateStack(30),
1140
+ readonly: this.isReadOnly(),
1141
+ isStagingMode: this.isStagingMode,
1132
1142
  });
1133
1143
  this.localChangesTelemetryCount--;
1134
1144
  }
@@ -1330,7 +1340,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
1330
1340
  );
1331
1341
 
1332
1342
  // Summarizer client should not create local data stores.
1333
- this.identifyLocalChangeInSummarizer("DataStoreCreatedInSummarizer");
1343
+ this.identifyLocalChangeIfReadonly("DataStoreCreatedWhileReadonly");
1334
1344
 
1335
1345
  this.snapshotTree = props.snapshotTree;
1336
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,
@@ -109,16 +110,3 @@ export {
109
110
  DefaultSummaryConfiguration,
110
111
  } from "./summary/index.js";
111
112
  export { IChunkedOp, unpackRuntimeMessage } from "./opLifecycle/index.js";
112
- export { ChannelCollection } from "./channelCollection.js";
113
- export {
114
- IFluidDataStoreContextInternal,
115
- ISnapshotDetails,
116
- LocalFluidDataStoreContext,
117
- LocalFluidDataStoreContextBase,
118
- FluidDataStoreContext,
119
- IFluidDataStoreContextProps,
120
- ILocalFluidDataStoreContextProps,
121
- ILocalDetachedFluidDataStoreContextProps,
122
- IFluidDataStoreContextEvents,
123
- } from "./dataStoreContext.js";
124
- export { DataStoreContexts } from "./dataStoreContexts.js";
@@ -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,
@@ -66,10 +66,27 @@ export interface IOutboxParameters {
66
66
  readonly logger: ITelemetryBaseLogger;
67
67
  readonly groupingManager: OpGroupingManager;
68
68
  readonly getCurrentSequenceNumbers: () => BatchSequenceNumbers;
69
- readonly reSubmit: (message: PendingMessageResubmitData) => void;
69
+ readonly reSubmit: (message: PendingMessageResubmitData, squash: boolean) => void;
70
70
  readonly opReentrancy: () => boolean;
71
71
  }
72
72
 
73
+ /**
74
+ * Info needed to correctly resubmit a batch
75
+ */
76
+ export interface BatchResubmitInfo {
77
+ /**
78
+ * If defined, indicates the Batch ID of the batch being resubmitted.
79
+ * This must be preserved on the new batch about to be submitted so they can be correlated/deduped in case both are sent.
80
+ */
81
+ batchId?: string;
82
+ /**
83
+ * Indicates whether or not this batch is "staged", meaning it should not be sent to the ordering service yet
84
+ * This is important on resubmit because we may be in Staging Mode for new changes,
85
+ * but resubmitting a non-staged change from before entering Staging Mode
86
+ */
87
+ staged: boolean;
88
+ }
89
+
73
90
  /**
74
91
  * Temporarily increase the stack limit while executing the provided action.
75
92
  * If a negative value is provided for `length`, no stack frames will be collected.
@@ -335,37 +352,33 @@ export class Outbox {
335
352
  * This method is expected to be called at the end of a batch.
336
353
  *
337
354
  * @throws If called from a reentrant context, or if the batch being flushed is too large.
338
- * @param resubmittingBatchId - If defined, indicates this is a resubmission of a batch
339
- * with the given Batch ID, which must be preserved
340
- * @param resubmittingStagedBatch - If defined, indicates this is a resubmission of a batch that is staged,
341
- * meaning it should not be sent to the ordering service yet.
355
+ * @param resubmitInfo - Key information when flushing a resubmitted batch. Undefined means this is not resubmit.
342
356
  */
343
- public flush(resubmittingBatchId?: BatchId, resubmittingStagedBatch?: boolean): void {
357
+ public flush(resubmitInfo?: BatchResubmitInfo): void {
344
358
  assert(
345
359
  !this.isContextReentrant(),
346
360
  0xb7b /* Flushing must not happen while incoming changes are being processed */,
347
361
  );
348
-
349
- this.flushAll(resubmittingBatchId, resubmittingStagedBatch);
362
+ this.flushAll(resubmitInfo);
350
363
  }
351
364
 
352
- private flushAll(resubmittingBatchId?: BatchId, resubmittingStagedBatch?: boolean): void {
365
+ private flushAll(resubmitInfo?: BatchResubmitInfo): void {
353
366
  const allBatchesEmpty =
354
367
  this.idAllocationBatch.empty && this.blobAttachBatch.empty && this.mainBatch.empty;
355
368
  if (allBatchesEmpty) {
356
- // If we're resubmitting and all batches are empty, we need to flush an empty batch.
357
- // Note that we currently resubmit one batch at a time, so on resubmit, 2 of the 3 batches will *always* be empty.
369
+ // If we're resubmitting with a batchId and all batches are empty, we need to flush an empty batch.
370
+ // Note that we currently resubmit one batch at a time, so on resubmit, 1 of the 2 batches will *always* be empty.
358
371
  // It's theoretically possible that we don't *need* to resubmit this empty batch, and in those cases, it'll safely be ignored
359
372
  // by the rest of the system, including remote clients.
360
373
  // In some cases we *must* resubmit the empty batch (to match up with a non-empty version tracked locally by a container fork), so we do it always.
361
- if (resubmittingBatchId) {
362
- this.flushEmptyBatch(resubmittingBatchId, resubmittingStagedBatch === true);
374
+ if (resubmitInfo?.batchId !== undefined) {
375
+ this.flushEmptyBatch(resubmitInfo.batchId, resubmitInfo.staged);
363
376
  }
364
377
  return;
365
378
  }
366
379
 
367
380
  // Don't use resubmittingBatchId for idAllocationBatch.
368
- // ID Allocation messages are not directly resubmitted so we don't want to reuse the batch ID.
381
+ // ID Allocation messages are not directly resubmitted so don't pass the resubmitInfo
369
382
  this.flushInternal({
370
383
  batchManager: this.idAllocationBatch,
371
384
  // Note: For now, we will never stage ID Allocation messages.
@@ -374,13 +387,11 @@ export class Outbox {
374
387
  this.flushInternal({
375
388
  batchManager: this.blobAttachBatch,
376
389
  disableGroupedBatching: true,
377
- resubmittingBatchId,
378
- resubmittingStagedBatch,
390
+ resubmitInfo,
379
391
  });
380
392
  this.flushInternal({
381
393
  batchManager: this.mainBatch,
382
- resubmittingBatchId,
383
- resubmittingStagedBatch,
394
+ resubmitInfo,
384
395
  });
385
396
  }
386
397
 
@@ -416,25 +427,19 @@ export class Outbox {
416
427
  private flushInternal(params: {
417
428
  batchManager: BatchManager;
418
429
  disableGroupedBatching?: boolean;
419
- resubmittingBatchId?: BatchId; // undefined if not resubmitting
420
- resubmittingStagedBatch?: boolean; // undefined if not resubmitting
430
+ resubmitInfo?: BatchResubmitInfo; // undefined if not resubmitting
421
431
  }): void {
422
- const {
423
- batchManager,
424
- disableGroupedBatching = false,
425
- resubmittingBatchId,
426
- resubmittingStagedBatch,
427
- } = params;
432
+ const { batchManager, disableGroupedBatching = false, resubmitInfo } = params;
428
433
  if (batchManager.empty) {
429
434
  return;
430
435
  }
431
436
 
432
- const rawBatch = batchManager.popBatch(resubmittingBatchId);
437
+ const rawBatch = batchManager.popBatch(resubmitInfo?.batchId);
433
438
 
434
439
  // When resubmitting, we respect the staged state of the original batch.
435
440
  // In this case rawBatch.staged will match the state of inStagingMode when
436
441
  // the resubmit occurred, which is not relevant.
437
- const staged = resubmittingStagedBatch ?? rawBatch.staged === true;
442
+ const staged = resubmitInfo?.staged ?? rawBatch.staged === true;
438
443
 
439
444
  const groupingEnabled =
440
445
  !disableGroupedBatching && this.params.groupingManager.groupedBatchingEnabled();
@@ -490,12 +495,16 @@ export class Outbox {
490
495
  assert(batchManager.options.canRebase, 0x9a7 /* BatchManager does not support rebase */);
491
496
 
492
497
  this.rebasing = true;
498
+ const squash = false;
493
499
  for (const message of rawBatch.messages) {
494
- this.params.reSubmit({
495
- runtimeOp: message.runtimeOp,
496
- localOpMetadata: message.localOpMetadata,
497
- opMetadata: message.metadata,
498
- });
500
+ this.params.reSubmit(
501
+ {
502
+ runtimeOp: message.runtimeOp,
503
+ localOpMetadata: message.localOpMetadata,
504
+ opMetadata: message.metadata,
505
+ },
506
+ squash,
507
+ );
499
508
  }
500
509
 
501
510
  if (this.batchRebasesToReport > 0) {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.33.2";
9
+ export const pkgVersion = "2.40.0";
@@ -22,13 +22,13 @@ import {
22
22
  } from "./messageTypes.js";
23
23
  import { asBatchMetadata, asEmptyBatchLocalOpMetadata } from "./metadata.js";
24
24
  import {
25
- BatchId,
26
25
  LocalBatchMessage,
27
26
  getEffectiveBatchId,
28
27
  BatchStartInfo,
29
28
  InboundMessageResult,
30
29
  serializeOp,
31
30
  type LocalEmptyBatchPlaceholder,
31
+ type BatchResubmitInfo,
32
32
  } from "./opLifecycle/index.js";
33
33
 
34
34
  /**
@@ -114,11 +114,21 @@ export type PendingMessageResubmitData = Pick<
114
114
  runtimeOp: LocalContainerRuntimeMessage;
115
115
  };
116
116
 
117
+ export interface PendingBatchResubmitMetadata extends BatchResubmitInfo {
118
+ /**
119
+ * Whether changes in this batch should be squashed when resubmitting.
120
+ */
121
+ squash: boolean;
122
+ }
123
+
117
124
  export interface IRuntimeStateHandler {
118
125
  connected(): boolean;
119
126
  clientId(): string | undefined;
120
127
  applyStashedOp(serializedOp: string): Promise<unknown>;
121
- reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId, staged: boolean): void;
128
+ reSubmitBatch(
129
+ batch: PendingMessageResubmitData[],
130
+ metadata: PendingBatchResubmitMetadata,
131
+ ): void;
122
132
  isActiveConnection: () => boolean;
123
133
  isAttached: () => boolean;
124
134
  }
@@ -211,6 +221,24 @@ function toSerializableForm(
211
221
  };
212
222
  }
213
223
 
224
+ interface ReplayPendingStateOptions {
225
+ /**
226
+ * If true, only replay staged batches. This is used when we are exiting staging mode and want to rebase and submit the staged batches.
227
+ * Default: false
228
+ */
229
+ onlyStagedBatches: boolean;
230
+ /**
231
+ * @param squash - If true, edits should be squashed when resubmitting.
232
+ * Default: false
233
+ */
234
+ squash: boolean;
235
+ }
236
+
237
+ const defaultReplayPendingStatesOptions: ReplayPendingStateOptions = {
238
+ onlyStagedBatches: false,
239
+ squash: false,
240
+ };
241
+
214
242
  /**
215
243
  * PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
216
244
  * acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
@@ -683,11 +711,12 @@ export class PendingStateManager implements IDisposable {
683
711
  * Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
684
712
  * states in its queue. This includes triggering resubmission of unacked ops.
685
713
  * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
686
- * @param onlyStagedBatches - If true, only replay staged batches. This is used when we are exiting staging mode and want to rebase and submit the staged batches.
687
714
  */
688
- public replayPendingStates(onlyStagedBatches?: boolean): void {
715
+ public replayPendingStates(optionsParam?: ReplayPendingStateOptions): void {
716
+ const options = { ...defaultReplayPendingStatesOptions, ...optionsParam };
717
+ const { onlyStagedBatches, squash } = options;
689
718
  assert(
690
- this.stateHandler.connected(),
719
+ this.stateHandler.connected() || onlyStagedBatches === true,
691
720
  0x172 /* "The connection state is not consistent with the runtime" */,
692
721
  );
693
722
 
@@ -741,7 +770,7 @@ export class PendingStateManager implements IDisposable {
741
770
 
742
771
  if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
743
772
  // Resubmit no messages, with the batchId. Will result in another empty batch marker.
744
- this.stateHandler.reSubmitBatch([], batchId, staged);
773
+ this.stateHandler.reSubmitBatch([], { batchId, staged, squash });
745
774
  continue;
746
775
  }
747
776
 
@@ -766,8 +795,7 @@ export class PendingStateManager implements IDisposable {
766
795
  opMetadata: pendingMessage.opMetadata,
767
796
  },
768
797
  ],
769
- batchId,
770
- staged,
798
+ { batchId, staged, squash },
771
799
  );
772
800
  continue;
773
801
  }
@@ -807,7 +835,7 @@ export class PendingStateManager implements IDisposable {
807
835
  );
808
836
  }
809
837
 
810
- this.stateHandler.reSubmitBatch(batch, batchId, staged);
838
+ this.stateHandler.reSubmitBatch(batch, { batchId, staged, squash });
811
839
  }
812
840
 
813
841
  // pending ops should no longer depend on previous sequenced local ops after resubmit
@@ -825,17 +853,6 @@ export class PendingStateManager implements IDisposable {
825
853
  }
826
854
  }
827
855
 
828
- /**
829
- * Clears the 'staged' flag off all pending messages.
830
- */
831
- public clearStagingFlags(): void {
832
- for (const message of this.pendingMessages.toArray()) {
833
- if (message.batchInfo.staged) {
834
- message.batchInfo.staged = false;
835
- }
836
- }
837
- }
838
-
839
856
  /**
840
857
  * Pops all staged batches, invoking the callback on each one in order (LIFO)
841
858
  */