@fluidframework/container-runtime 0.58.1000 → 0.58.2000

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 (94) hide show
  1. package/dist/batchTracker.d.ts +1 -1
  2. package/dist/batchTracker.d.ts.map +1 -1
  3. package/dist/batchTracker.js.map +1 -1
  4. package/dist/containerRuntime.d.ts.map +1 -1
  5. package/dist/containerRuntime.js +9 -4
  6. package/dist/containerRuntime.js.map +1 -1
  7. package/dist/dataStore.js.map +1 -1
  8. package/dist/dataStoreContext.d.ts +17 -14
  9. package/dist/dataStoreContext.d.ts.map +1 -1
  10. package/dist/dataStoreContext.js +41 -31
  11. package/dist/dataStoreContext.js.map +1 -1
  12. package/dist/dataStores.d.ts.map +1 -1
  13. package/dist/dataStores.js +4 -2
  14. package/dist/dataStores.js.map +1 -1
  15. package/dist/garbageCollection.d.ts +1 -1
  16. package/dist/garbageCollection.d.ts.map +1 -1
  17. package/dist/garbageCollection.js +3 -4
  18. package/dist/garbageCollection.js.map +1 -1
  19. package/dist/packageVersion.d.ts +1 -1
  20. package/dist/packageVersion.js +1 -1
  21. package/dist/packageVersion.js.map +1 -1
  22. package/dist/pendingStateManager.d.ts +15 -5
  23. package/dist/pendingStateManager.d.ts.map +1 -1
  24. package/dist/pendingStateManager.js +78 -72
  25. package/dist/pendingStateManager.js.map +1 -1
  26. package/dist/runningSummarizer.d.ts.map +1 -1
  27. package/dist/runningSummarizer.js +1 -1
  28. package/dist/runningSummarizer.js.map +1 -1
  29. package/dist/summarizerClientElection.d.ts.map +1 -1
  30. package/dist/summarizerClientElection.js +1 -0
  31. package/dist/summarizerClientElection.js.map +1 -1
  32. package/dist/summarizerTypes.d.ts +13 -1
  33. package/dist/summarizerTypes.d.ts.map +1 -1
  34. package/dist/summarizerTypes.js.map +1 -1
  35. package/dist/summaryGenerator.d.ts +3 -3
  36. package/dist/summaryGenerator.d.ts.map +1 -1
  37. package/dist/summaryGenerator.js +1 -0
  38. package/dist/summaryGenerator.js.map +1 -1
  39. package/dist/summaryManager.d.ts.map +1 -1
  40. package/dist/summaryManager.js.map +1 -1
  41. package/lib/batchTracker.d.ts +1 -1
  42. package/lib/batchTracker.d.ts.map +1 -1
  43. package/lib/batchTracker.js.map +1 -1
  44. package/lib/containerRuntime.d.ts.map +1 -1
  45. package/lib/containerRuntime.js +9 -4
  46. package/lib/containerRuntime.js.map +1 -1
  47. package/lib/dataStore.js.map +1 -1
  48. package/lib/dataStoreContext.d.ts +17 -14
  49. package/lib/dataStoreContext.d.ts.map +1 -1
  50. package/lib/dataStoreContext.js +41 -31
  51. package/lib/dataStoreContext.js.map +1 -1
  52. package/lib/dataStores.d.ts.map +1 -1
  53. package/lib/dataStores.js +4 -2
  54. package/lib/dataStores.js.map +1 -1
  55. package/lib/garbageCollection.d.ts +1 -1
  56. package/lib/garbageCollection.d.ts.map +1 -1
  57. package/lib/garbageCollection.js +1 -2
  58. package/lib/garbageCollection.js.map +1 -1
  59. package/lib/packageVersion.d.ts +1 -1
  60. package/lib/packageVersion.js +1 -1
  61. package/lib/packageVersion.js.map +1 -1
  62. package/lib/pendingStateManager.d.ts +15 -5
  63. package/lib/pendingStateManager.d.ts.map +1 -1
  64. package/lib/pendingStateManager.js +78 -72
  65. package/lib/pendingStateManager.js.map +1 -1
  66. package/lib/runningSummarizer.d.ts.map +1 -1
  67. package/lib/runningSummarizer.js +1 -1
  68. package/lib/runningSummarizer.js.map +1 -1
  69. package/lib/summarizerClientElection.d.ts.map +1 -1
  70. package/lib/summarizerClientElection.js +1 -0
  71. package/lib/summarizerClientElection.js.map +1 -1
  72. package/lib/summarizerTypes.d.ts +13 -1
  73. package/lib/summarizerTypes.d.ts.map +1 -1
  74. package/lib/summarizerTypes.js.map +1 -1
  75. package/lib/summaryGenerator.d.ts +3 -3
  76. package/lib/summaryGenerator.d.ts.map +1 -1
  77. package/lib/summaryGenerator.js +1 -0
  78. package/lib/summaryGenerator.js.map +1 -1
  79. package/lib/summaryManager.d.ts.map +1 -1
  80. package/lib/summaryManager.js.map +1 -1
  81. package/package.json +17 -13
  82. package/src/batchTracker.ts +2 -2
  83. package/src/containerRuntime.ts +5 -1
  84. package/src/dataStore.ts +1 -1
  85. package/src/dataStoreContext.ts +38 -36
  86. package/src/dataStores.ts +2 -1
  87. package/src/garbageCollection.ts +5 -6
  88. package/src/packageVersion.ts +1 -1
  89. package/src/pendingStateManager.ts +102 -86
  90. package/src/runningSummarizer.ts +5 -4
  91. package/src/summarizerClientElection.ts +1 -0
  92. package/src/summarizerTypes.ts +18 -0
  93. package/src/summaryGenerator.ts +41 -5
  94. package/src/summaryManager.ts +0 -1
@@ -3,11 +3,11 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import EventEmitter from "events";
6
7
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
8
  import { assert, performance } from "@fluidframework/common-utils";
8
9
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
9
10
  import { ChildLogger } from "@fluidframework/telemetry-utils";
10
- import EventEmitter from "events";
11
11
 
12
12
  export class BatchTracker {
13
13
  private readonly logger: ITelemetryLogger;
@@ -77,4 +77,4 @@ export const BindBatchTracker = (
77
77
  logger: ITelemetryLogger,
78
78
  batchLengthThreshold: number = 128,
79
79
  batchCountSamplingRate: number = 1000,
80
- ) => new BatchTracker(batchEventEmitter, logger, batchLengthThreshold, batchCountSamplingRate)
80
+ ) => new BatchTracker(batchEventEmitter, logger, batchLengthThreshold, batchCountSamplingRate);
@@ -780,6 +780,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
780
780
  if (loadSequenceNumberVerification !== "bypass" && runtimeSequenceNumber !== protocolSequenceNumber) {
781
781
  // "Load from summary, runtime metadata sequenceNumber !== initialSequenceNumber"
782
782
  const error = new DataCorruptionError(
783
+ // pre-0.58 error message: SummaryMetadataMismatch
783
784
  "Summary metadata mismatch",
784
785
  { runtimeSequenceNumber, protocolSequenceNumber },
785
786
  );
@@ -1128,6 +1129,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1128
1129
  this.pendingStateManager = new PendingStateManager(
1129
1130
  this,
1130
1131
  async (type, content) => this.applyStashedOp(type, content),
1132
+ this._flushMode,
1131
1133
  context.pendingLocalState as IPendingLocalState);
1132
1134
 
1133
1135
  this.context.quorum.on("removeMember", (clientId: string) => {
@@ -1579,6 +1581,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1579
1581
  this.context.pendingLocalState = undefined;
1580
1582
  if (!this.shouldContinueReconnecting()) {
1581
1583
  this.closeFn(new GenericError(
1584
+ // pre-0.58 error message: MaxReconnectsWithNoProgress
1582
1585
  "Runtime detected too many reconnects with no progress syncing local ops",
1583
1586
  undefined, // error
1584
1587
  { attempts: this.consecutiveReconnects }));
@@ -1762,6 +1765,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
1762
1765
  this._orderSequentiallyCalls++;
1763
1766
  callback();
1764
1767
  } catch (error) {
1768
+ // pre-0.58 error message: orderSequentiallyCallbackException
1765
1769
  this.closeFn(new GenericError("orderSequentially callback exception", error));
1766
1770
  throw error; // throw the original error for the consumer of the runtime
1767
1771
  } finally {
@@ -2147,7 +2151,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
2147
2151
  if (summaryRefSeqNum !== this.deltaManager.lastMessage?.sequenceNumber) {
2148
2152
  summaryLogger.sendErrorEvent({
2149
2153
  eventName: "LastSequenceMismatch",
2150
- message,
2154
+ error: message,
2151
2155
  });
2152
2156
  }
2153
2157
 
package/src/dataStore.ts CHANGED
@@ -123,7 +123,7 @@ class DataStore implements IDataStore {
123
123
  private readonly fluidDataStoreChannel: IFluidDataStoreChannel,
124
124
  private readonly internalId: string,
125
125
  private readonly runtime: ContainerRuntime,
126
- private datastores: DataStores,
126
+ private readonly datastores: DataStores,
127
127
  private readonly logger: ITelemetryLogger,
128
128
  ) { }
129
129
  public get IFluidRouter() { return this.fluidDataStoreChannel; }
@@ -106,6 +106,7 @@ export function createAttributesBlob(
106
106
 
107
107
  interface ISnapshotDetails {
108
108
  pkg: readonly string[];
109
+ isRootDataStore: boolean;
109
110
  snapshot?: ISnapshotTree;
110
111
  }
111
112
 
@@ -206,11 +207,25 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
206
207
  return this.registry;
207
208
  }
208
209
 
210
+ /**
211
+ * A datastore is considered as root if it
212
+ * 1. is root in memory - see isInMemoryRoot
213
+ * 2. is root as part of the base snapshot that the datastore loaded from
214
+ * @returns whether a datastore is root
215
+ */
209
216
  public async isRoot(): Promise<boolean> {
210
- // This call updates this.isRootDataStore if it has not yet been updated
211
- // The initial value is stored in the initial snapshot of the data store
212
- await this.getInitialSnapshotDetails();
213
- return this.isRootDataStore;
217
+ return this.isInMemoryRoot() || (await this.getInitialSnapshotDetails()).isRootDataStore;
218
+ }
219
+
220
+ /**
221
+ * There are 3 states where isInMemoryRoot needs to be true
222
+ * 1. when a datastore becomes aliased. This can happen for both remote and local datastores
223
+ * 2. when a datastore is created locally as root
224
+ * 3. when a datastore is created locally as root and is rehydrated
225
+ * @returns whether a datastore is root in memory
226
+ */
227
+ protected isInMemoryRoot(): boolean {
228
+ return this._isInMemoryRoot;
214
229
  }
215
230
 
216
231
  protected registry: IFluidDataStoreRegistry | undefined;
@@ -223,7 +238,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
223
238
  protected channelDeferred: Deferred<IFluidDataStoreChannel> | undefined;
224
239
  private _baseSnapshot: ISnapshotTree | undefined;
225
240
  protected _attachState: AttachState;
226
- protected isRootDataStore: boolean = false;
241
+ private _isInMemoryRoot: boolean = false;
227
242
  protected readonly summarizerNode: ISummarizerNodeWithGC;
228
243
  private readonly subLogger: ITelemetryLogger;
229
244
  private readonly thresholdOpsCounter: ThresholdCounter;
@@ -450,7 +465,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
450
465
 
451
466
  // Add data store's attributes to the summary.
452
467
  const { pkg } = await this.getInitialSnapshotDetails();
453
- const attributes = createAttributes(pkg, this.isRootDataStore, this.disableIsolatedChannels);
468
+ const isRoot = await this.isRoot();
469
+ const attributes = createAttributes(pkg, isRoot, this.disableIsolatedChannels);
454
470
  addBlobToSummary(summarizeResult, dataStoreAttributesBlobName, JSON.stringify(attributes));
455
471
 
456
472
  // Add GC data to the summary if it's not written at the root.
@@ -676,7 +692,9 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
676
692
  * This method should not be used outside of the aliasing context.
677
693
  * It will be removed, as the source of truth for this flag will be the aliasing blob.
678
694
  */
679
- public abstract setRoot(): void;
695
+ public setInMemoryRoot(): void {
696
+ this._isInMemoryRoot = true;
697
+ }
680
698
 
681
699
  /**
682
700
  * @deprecated - Renamed to getBaseGCDetails().
@@ -793,7 +811,7 @@ export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
793
811
  * data stores in older documents are not garbage collected incorrectly. This may lead to additional
794
812
  * roots in the document but they won't break.
795
813
  */
796
- isRootDataStore = this.isRootDataStore === true || (attributes.isRootDataStore ?? true);
814
+ isRootDataStore = attributes.isRootDataStore ?? true;
797
815
 
798
816
  if (hasIsolatedChannels(attributes)) {
799
817
  tree = tree.trees[channelsTreeName];
@@ -802,11 +820,10 @@ export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
802
820
  }
803
821
  }
804
822
 
805
- this.isRootDataStore = isRootDataStore;
806
-
807
823
  return {
808
824
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
809
825
  pkg: this.pkg!,
826
+ isRootDataStore,
810
827
  snapshot: tree,
811
828
  };
812
829
  });
@@ -829,15 +846,6 @@ export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
829
846
  public generateAttachMessage(): IAttachMessage {
830
847
  throw new Error("Cannot attach remote store");
831
848
  }
832
-
833
- /**
834
- * @deprecated - Sets the datastore as root, for aliasing purposes: #7948
835
- * This method should not be used outside of the aliasing context.
836
- * It will be removed, as the source of truth for this flag will be the aliasing blob.
837
- */
838
- public setRoot(): void {
839
- this.isRootDataStore = true;
840
- }
841
849
  }
842
850
 
843
851
  /**
@@ -860,7 +868,9 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
860
868
  );
861
869
 
862
870
  this.snapshotTree = props.snapshotTree;
863
- this.isRootDataStore = props.isRootDataStore ?? false;
871
+ if (props.isRootDataStore === true) {
872
+ this.setInMemoryRoot();
873
+ }
864
874
  this.createProps = props.createProps;
865
875
  this.attachListeners();
866
876
  }
@@ -879,8 +889,6 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
879
889
  public generateAttachMessage(): IAttachMessage {
880
890
  assert(this.channel !== undefined, 0x14f /* "There should be a channel when generating attach message" */);
881
891
  assert(this.pkg !== undefined, 0x150 /* "pkg should be available in local data store context" */);
882
- assert(this.isRootDataStore !== undefined,
883
- 0x151 /* "isRootDataStore should be available in local data store context" */);
884
892
 
885
893
  const summarizeResult = this.channel.getAttachSummary();
886
894
 
@@ -892,7 +900,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
892
900
  // Add data store's attributes to the summary.
893
901
  const attributes = createAttributes(
894
902
  this.pkg,
895
- this.isRootDataStore,
903
+ this.isInMemoryRoot(),
896
904
  this.disableIsolatedChannels,
897
905
  );
898
906
  addBlobToSummary(summarizeResult, dataStoreAttributesBlobName, JSON.stringify(attributes));
@@ -912,6 +920,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
912
920
  protected async getInitialSnapshotDetails(): Promise<ISnapshotDetails> {
913
921
  let snapshot = this.snapshotTree;
914
922
  let attributes: ReadFluidDataStoreAttributes;
923
+ let isRootDataStore = false;
915
924
  if (snapshot !== undefined) {
916
925
  // Get the dataStore attributes.
917
926
  // Note: storage can be undefined in special case while detached.
@@ -926,15 +935,17 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
926
935
  // If there is no isRootDataStore in the attributes blob, set it to true. This ensures that data
927
936
  // stores in older documents are not garbage collected incorrectly. This may lead to additional
928
937
  // roots in the document but they won't break.
929
- this.isRootDataStore = this.isRootDataStore || (attributes.isRootDataStore ?? true);
938
+ if (attributes.isRootDataStore ?? true) {
939
+ isRootDataStore = true;
940
+ this.setInMemoryRoot();
941
+ }
930
942
  }
931
943
  }
932
944
  assert(this.pkg !== undefined, 0x152 /* "pkg should be available in local data store" */);
933
- assert(this.isRootDataStore !== undefined,
934
- 0x153 /* "isRootDataStore should be available in local data store" */);
935
945
 
936
946
  return {
937
947
  pkg: this.pkg,
948
+ isRootDataStore,
938
949
  snapshot,
939
950
  };
940
951
  }
@@ -951,15 +962,6 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
951
962
  // Local data store does not have initial summary.
952
963
  return {};
953
964
  }
954
-
955
- /**
956
- * @deprecated - Sets the datastore as root, for aliasing purposes: #7948
957
- * This method should not be used outside of the aliasing context.
958
- * It will be removed, as the source of truth for this flag will be the aliasing blob.
959
- */
960
- public setRoot(): void {
961
- this.isRootDataStore = true;
962
- }
963
965
  }
964
966
 
965
967
  /**
@@ -1009,7 +1011,7 @@ export class LocalDetachedFluidDataStoreContext
1009
1011
 
1010
1012
  super.bindRuntime(dataStoreRuntime);
1011
1013
 
1012
- if (this.isRootDataStore) {
1014
+ if (await this.isRoot()) {
1013
1015
  dataStoreRuntime.bindToContext();
1014
1016
  }
1015
1017
  }
package/src/dataStores.ts CHANGED
@@ -194,6 +194,7 @@ export class DataStores implements IDisposable {
194
194
  if (this.alreadyProcessed(attachMessage.id)) {
195
195
  // TODO: dataStoreId may require a different tag from PackageData #7488
196
196
  const error = new DataCorruptionError(
197
+ // pre-0.58 error message: duplicateDataStoreCreatedWithExistingId
197
198
  "Duplicate DataStore created with existing id",
198
199
  {
199
200
  ...extractSafePropertiesFromMessage(message),
@@ -282,7 +283,7 @@ export class DataStores implements IDisposable {
282
283
  }
283
284
 
284
285
  this.aliasMap.set(aliasMessage.alias, currentContext.id);
285
- currentContext.setRoot();
286
+ currentContext.setInMemoryRoot();
286
287
  return true;
287
288
  }
288
289
 
@@ -35,10 +35,9 @@ import {
35
35
  MonitoringContext,
36
36
  PerformanceEvent,
37
37
  TelemetryDataTag,
38
- } from "@fluidframework/telemetry-utils";
39
- import { RuntimeHeaders } from ".";
38
+ } from "@fluidframework/telemetry-utils";
40
39
 
41
- import { IGCRuntimeOptions } from "./containerRuntime";
40
+ import { IGCRuntimeOptions, RuntimeHeaders } from "./containerRuntime";
42
41
  import { getSummaryForDatastores } from "./dataStores";
43
42
  import {
44
43
  getGCVersion,
@@ -96,7 +95,7 @@ interface IUnreferencedEvent {
96
95
  lastSummaryTime?: number;
97
96
  externalRequest?: boolean;
98
97
  viaHandle?: boolean;
99
- };
98
+ }
100
99
 
101
100
  /** Defines the APIs for the runtime object to be passed to the garbage collector. */
102
101
  export interface IGarbageCollectionRuntime {
@@ -186,7 +185,7 @@ class UnreferencedStateTracker {
186
185
  }
187
186
 
188
187
  // The node isn't inactive yet. Restart a timer for the duration remaining for it to become inactive.
189
- const remainingDurationMs = this.inactiveTimeoutMs - unreferencedDurationMs;
188
+ const remainingDurationMs = this.inactiveTimeoutMs - unreferencedDurationMs;
190
189
  if (this.timer === undefined) {
191
190
  this.timer = new Timer(remainingDurationMs, () => { this._inactive = true; });
192
191
  }
@@ -322,7 +321,7 @@ export class GarbageCollector implements IGarbageCollector {
322
321
  // per event per node.
323
322
  private readonly loggedUnreferencedEvents: Set<string> = new Set();
324
323
  // Queue for unreferenced events that should be logged the next time GC runs.
325
- private pendingEventsQueue: IUnreferencedEvent[] = [];
324
+ private readonly pendingEventsQueue: IUnreferencedEvent[] = [];
326
325
 
327
326
  protected constructor(
328
327
  private readonly provider: IGarbageCollectionRuntime,
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "0.58.1000";
9
+ export const pkgVersion = "0.58.2000";
@@ -37,8 +37,8 @@ export interface IPendingFlushMode {
37
37
  }
38
38
 
39
39
  /**
40
- * This represents a manual flush and is added to the pending queue when `flush` is called on the ContainerRuntime to
41
- * flush any pending messages. This is applicable only when the FlushMode is Manual.
40
+ * This represents an explicit flush call and is added to the pending queue when flush is called on the ContainerRuntime
41
+ * to flush pending messages.
42
42
  */
43
43
  export interface IPendingFlush {
44
44
  type: "flush";
@@ -86,6 +86,13 @@ export class PendingStateManager implements IDisposable {
86
86
  // the correct batch metadata.
87
87
  private pendingBatchBeginMessage: ISequencedDocumentMessage | undefined;
88
88
 
89
+ /**
90
+ * This tracks the flush mode for the next message in the pending state queue. When replaying messages, we need to
91
+ * first set the flush mode to this value and then send ops. It is important to do this info because the flush
92
+ * mode could have been updated.
93
+ */
94
+ private flushModeForNextMessage: FlushMode;
95
+
89
96
  private clientId: string | undefined;
90
97
 
91
98
  private get connected(): boolean {
@@ -115,19 +122,23 @@ export class PendingStateManager implements IDisposable {
115
122
  constructor(
116
123
  private readonly containerRuntime: ContainerRuntime,
117
124
  private readonly applyStashedOp: (type, content) => Promise<unknown>,
118
- initialState: IPendingLocalState | undefined,
125
+ initialFlushMode: FlushMode,
126
+ initialLocalState: IPendingLocalState | undefined,
119
127
  ) {
120
- this.initialStates = new Deque<IPendingState>(initialState?.pendingStates ?? []);
128
+ this.initialStates = new Deque<IPendingState>(initialLocalState?.pendingStates ?? []);
121
129
 
122
- if (initialState) {
123
- if (initialState?.clientId) {
124
- this.previousClientIds.add(initialState.clientId);
130
+ if (initialLocalState) {
131
+ if (initialLocalState?.clientId) {
132
+ this.previousClientIds.add(initialLocalState.clientId);
125
133
  }
126
134
  // get stashed op count and client sequence number of first op
127
- const messages = initialState.pendingStates
135
+ const messages = initialLocalState.pendingStates
128
136
  .filter((state) => state.type === "message") as IPendingMessage[];
129
137
  this.firstStashedCSN = messages[0].clientSequenceNumber;
130
138
  }
139
+
140
+ this.flushModeForNextMessage = initialFlushMode;
141
+ this.onFlushModeUpdated(initialFlushMode);
131
142
  }
132
143
 
133
144
  public get disposed() { return this.disposeOnce.evaluated; }
@@ -169,54 +180,27 @@ export class PendingStateManager implements IDisposable {
169
180
  * @param flushMode - The flushMode that was updated.
170
181
  */
171
182
  public onFlushModeUpdated(flushMode: FlushMode) {
172
- if (flushMode === FlushMode.Immediate) {
173
- const previousState = this.pendingStates.peekBack();
174
-
175
- // We don't have to track a previous "flush" state because FlushMode.Immediate flushes the messages. So,
176
- // just tracking this FlushMode.Immediate is enough.
177
- if (previousState?.type === "flush") {
178
- this.pendingStates.removeBack();
179
- }
180
-
181
- // If no messages were sent between FlushMode.TurnBased and FlushMode.Immediate,
182
- // then we do not have to track both these states.
183
- // Remove FlushMode.TurnBased from the pending queue and return.
184
- if (previousState?.type === "flushMode" && previousState.flushMode === FlushMode.TurnBased) {
185
- this.pendingStates.removeBack();
186
- return;
187
- }
188
- }
189
-
190
- const pendingFlushMode: IPendingFlushMode = {
191
- type: "flushMode",
192
- flushMode,
193
- };
194
- this.pendingStates.push(pendingFlushMode);
183
+ this.pendingStates.push({ type: "flushMode", flushMode });
195
184
  }
196
185
 
197
186
  /**
198
187
  * Called when flush() is called on the ContainerRuntime to manually flush messages.
199
188
  */
200
189
  public onFlush() {
201
- // If the FlushMode is Immediate, we should not track this flush call as it is only applicable when FlushMode
202
- // is TurnBased.
190
+ // If the FlushMode is Immediate, we don't need to track an explicit flush call because every message is
191
+ // automatically flushed. So, flush is a no-op.
203
192
  if (this.containerRuntime.flushMode === FlushMode.Immediate) {
204
193
  return;
205
194
  }
206
195
 
207
- // If the previous state is not a message, we don't have to track this flush call as there is nothing to flush.
196
+ // If the previous state is not a message, flush is a no-op.
208
197
  const previousState = this.pendingStates.peekBack();
209
198
  if (previousState?.type !== "message") {
210
199
  return;
211
200
  }
212
201
 
213
- // Note that because of the checks above and the checks in onFlushModeUpdated(), we can be sure that a "flush"
214
- // state always has a "message" before and after it. So, it marks the end of a batch and the beginning of a
215
- // new one.
216
- const pendingFlush: IPendingFlush = {
217
- type: "flush",
218
- };
219
- this.pendingStates.push(pendingFlush);
202
+ // An explicit flush is interesting and is tracked only if there are messages sent in TurnBased mode.
203
+ this.pendingStates.push({ type: "flush" });
220
204
  }
221
205
 
222
206
  /**
@@ -249,7 +233,7 @@ export class PendingStateManager implements IDisposable {
249
233
  * Processes a local message once it's ack'd by the server to verify that there was no data corruption and that
250
234
  * the batch information was preserved for batch messages. Also process remote messages that might have been
251
235
  * sent from a previous container.
252
- * @param message - The messsage that got ack'd and needs to be processed.
236
+ * @param message - The message that got ack'd and needs to be processed.
253
237
  */
254
238
  public processMessage(message: ISequencedDocumentMessage, local: boolean) {
255
239
  // Do not process chunked ops until all pieces are available.
@@ -324,7 +308,7 @@ export class PendingStateManager implements IDisposable {
324
308
  /**
325
309
  * Processes a local message once its ack'd by the server. It verifies that there was no data corruption and that
326
310
  * the batch information was preserved for batch messages.
327
- * @param message - The messsage that got ack'd and needs to be processed.
311
+ * @param message - The message that got ack'd and needs to be processed.
328
312
  */
329
313
  private processPendingLocalMessage(message: ISequencedDocumentMessage): unknown {
330
314
  // Pre-processing part - This may be the start of a batch.
@@ -353,9 +337,7 @@ export class PendingStateManager implements IDisposable {
353
337
  this.pendingMessagesCount--;
354
338
 
355
339
  // Post-processing part - If we are processing a batch then this could be the last message in the batch.
356
- if (this.isProcessingBatch) {
357
- this.maybeProcessBatchEnd(message);
358
- }
340
+ this.maybeProcessBatchEnd(message);
359
341
 
360
342
  return pendingState.localOpMetadata;
361
343
  }
@@ -365,46 +347,82 @@ export class PendingStateManager implements IDisposable {
365
347
  * @param message - The message that is being processed.
366
348
  */
367
349
  private maybeProcessBatchBegin(message: ISequencedDocumentMessage) {
368
- const pendingState = this.peekNextPendingState();
369
- if (pendingState.type !== "flush" && pendingState.type !== "flushMode") {
370
- return;
350
+ // Tracks the last FlushMode that was set before this message was sent.
351
+ let pendingFlushMode: FlushMode | undefined;
352
+ // Tracks whether a flush was called before this message was sent.
353
+ let pendingFlush: boolean = false;
354
+
355
+ /**
356
+ * We are checking if the next message is the start of a batch. It can happen in the following scenarios:
357
+ * 1. The FlushMode was set to TurnBased before this message was sent.
358
+ * 2. The FlushMode was already TurnBased and a flush was called before this message was sent. This essentially
359
+ * means that the flush marked the end of a previous batch and beginning of a new batch.
360
+ *
361
+ * Keep reading pending states from the queue until we encounter a message. It's possible that the FlushMode was
362
+ * updated a bunch of times without sending any messages.
363
+ */
364
+ let nextPendingState = this.peekNextPendingState();
365
+ while (nextPendingState.type !== "message") {
366
+ if (nextPendingState.type === "flushMode") {
367
+ pendingFlushMode = nextPendingState.flushMode;
368
+ }
369
+ if (nextPendingState.type === "flush") {
370
+ pendingFlush = true;
371
+ }
372
+ this.pendingStates.shift();
373
+ nextPendingState = this.peekNextPendingState();
371
374
  }
372
375
 
373
- // If the pending state is of type "flushMode", it must be Manual since Automatic flush mode is processed
374
- // after a message is processed and not before.
375
- if (pendingState.type === "flushMode") {
376
- assert(pendingState.flushMode === FlushMode.TurnBased,
377
- 0x16a /* "Flush mode should be manual when processing batch begin" */);
376
+ if (pendingFlushMode !== undefined) {
377
+ this.flushModeForNextMessage = pendingFlushMode;
378
378
  }
379
379
 
380
- // We should not already be processing a batch and there should be no pending batch begin message.
381
- assert(!this.isProcessingBatch && this.pendingBatchBeginMessage === undefined,
382
- 0x16b /* "The pending batch state indicates we are already processing a batch" */);
383
-
384
- // Set the pending batch state indicating we have started processing a batch.
385
- this.pendingBatchBeginMessage = message;
386
- this.isProcessingBatch = true;
380
+ // If the FlushMode was set to Immediate before this message was sent, this message won't be a batch message
381
+ // because in Immediate mode, every message is flushed individually.
382
+ if (pendingFlushMode === FlushMode.Immediate) {
383
+ return;
384
+ }
387
385
 
388
- // Remove this pending state from the queue as we have processed it.
389
- this.pendingStates.shift();
386
+ /**
387
+ * This message is the first in a batch if before it was sent either the FlushMode was set to TurnBased or there
388
+ * was an explicit flush call. Note that a flush call is tracked only in TurnBased mode and it indicates the end
389
+ * of one batch and beginning of another.
390
+ */
391
+ if (pendingFlushMode === FlushMode.TurnBased || pendingFlush) {
392
+ // We should not already be processing a batch and there should be no pending batch begin message.
393
+ assert(!this.isProcessingBatch && this.pendingBatchBeginMessage === undefined,
394
+ 0x16b /* "The pending batch state indicates we are already processing a batch" */);
395
+
396
+ // Set the pending batch state indicating we have started processing a batch.
397
+ this.pendingBatchBeginMessage = message;
398
+ this.isProcessingBatch = true;
399
+ }
390
400
  }
391
401
 
402
+ /**
403
+ * This message could be the last message in batch. If so, clear batch state since the batch is complete.
404
+ * @param message - The message that is being processed.
405
+ */
392
406
  private maybeProcessBatchEnd(message: ISequencedDocumentMessage) {
393
- const nextPendingState = this.peekNextPendingState();
394
- if (nextPendingState.type !== "flush" && nextPendingState.type !== "flushMode") {
407
+ if (!this.isProcessingBatch) {
395
408
  return;
396
409
  }
397
410
 
398
- // If the next pending state is of type "flushMode", it must be Immediate and if so, we need to remove it from
399
- // the queue.
400
- // Note that we do not remove the type "flush" from the queue because it indicates the end of one batch and the
401
- // beginning of a new one. So, it will removed when the next batch begin is processed.
402
- if (nextPendingState.type === "flushMode") {
403
- assert(nextPendingState.flushMode === FlushMode.Immediate,
404
- 0x16c /* "Flush mode is set to TurnBased in the middle of processing a batch" */);
405
- this.pendingStates.shift();
411
+ const nextPendingState = this.peekNextPendingState();
412
+ if (nextPendingState.type === "message") {
413
+ return;
406
414
  }
407
415
 
416
+ /**
417
+ * We are in the middle of processing a batch. The batch ends when we see an explicit flush. We should never see
418
+ * a FlushMode before flush. This is true because we track batches only when FlushMode is TurnBased and in this
419
+ * mode, a batch ends either by calling flush or by changing the mode to Immediate which also triggers a flush.
420
+ */
421
+ assert(
422
+ nextPendingState.type !== "flushMode",
423
+ 0x2bd /* "We should not see a pending FlushMode until we see a flush when processing a batch" */,
424
+ );
425
+
408
426
  // There should be a pending batch begin message.
409
427
  assert(this.pendingBatchBeginMessage !== undefined, 0x16d /* "There is no pending batch begin message" */);
410
428
 
@@ -462,6 +480,10 @@ export class PendingStateManager implements IDisposable {
462
480
  // Save the current FlushMode so that we can revert it back after replaying the states.
463
481
  const savedFlushMode = this.containerRuntime.flushMode;
464
482
 
483
+ // Set the flush mode for the next message. This step is important because the flush mode may have been changed
484
+ // after the next pending message was sent.
485
+ this.containerRuntime.setFlushMode(this.flushModeForNextMessage);
486
+
465
487
  // Process exactly `pendingStatesCount` items in the queue as it represents the number of states that were
466
488
  // pending when we connected. This is important because the `reSubmitFn` might add more items in the queue
467
489
  // which must not be replayed.
@@ -470,23 +492,17 @@ export class PendingStateManager implements IDisposable {
470
492
  const pendingState = this.pendingStates.shift()!;
471
493
  switch (pendingState.type) {
472
494
  case "message":
473
- {
474
- this.containerRuntime.reSubmitFn(
475
- pendingState.messageType,
476
- pendingState.content,
477
- pendingState.localOpMetadata,
478
- pendingState.opMetadata);
479
- }
495
+ this.containerRuntime.reSubmitFn(
496
+ pendingState.messageType,
497
+ pendingState.content,
498
+ pendingState.localOpMetadata,
499
+ pendingState.opMetadata);
480
500
  break;
481
501
  case "flushMode":
482
- {
483
- this.containerRuntime.setFlushMode(pendingState.flushMode);
484
- }
502
+ this.containerRuntime.setFlushMode(pendingState.flushMode);
485
503
  break;
486
504
  case "flush":
487
- {
488
- this.containerRuntime.flush();
489
- }
505
+ this.containerRuntime.flush();
490
506
  break;
491
507
  default:
492
508
  break;