@fluidframework/container-runtime 2.90.0 → 2.92.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 (135) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/api-report/container-runtime.legacy.beta.api.md +2 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/containerCompatibility.d.ts +1 -1
  5. package/dist/containerCompatibility.d.ts.map +1 -1
  6. package/dist/containerCompatibility.js.map +1 -1
  7. package/dist/containerRuntime.d.ts +37 -10
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +105 -77
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/gc/garbageCollection.d.ts +1 -0
  12. package/dist/gc/garbageCollection.d.ts.map +1 -1
  13. package/dist/gc/garbageCollection.js +3 -8
  14. package/dist/gc/garbageCollection.js.map +1 -1
  15. package/dist/gc/gcDefinitions.d.ts +4 -0
  16. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  17. package/dist/gc/gcDefinitions.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/legacy.d.ts +1 -1
  23. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  24. package/dist/opLifecycle/batchManager.js +2 -1
  25. package/dist/opLifecycle/batchManager.js.map +1 -1
  26. package/dist/opLifecycle/index.d.ts +1 -1
  27. package/dist/opLifecycle/index.d.ts.map +1 -1
  28. package/dist/opLifecycle/index.js +2 -1
  29. package/dist/opLifecycle/index.js.map +1 -1
  30. package/dist/opLifecycle/opGroupingManager.d.ts +6 -0
  31. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  32. package/dist/opLifecycle/opGroupingManager.js +11 -2
  33. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  34. package/dist/opLifecycle/opSerialization.d.ts +3 -1
  35. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  36. package/dist/opLifecycle/opSerialization.js +11 -9
  37. package/dist/opLifecycle/opSerialization.js.map +1 -1
  38. package/dist/opLifecycle/outbox.d.ts +0 -6
  39. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  40. package/dist/opLifecycle/outbox.js +2 -9
  41. package/dist/opLifecycle/outbox.js.map +1 -1
  42. package/dist/packageVersion.d.ts +1 -1
  43. package/dist/packageVersion.js +1 -1
  44. package/dist/packageVersion.js.map +1 -1
  45. package/dist/pendingStateManager.d.ts +7 -3
  46. package/dist/pendingStateManager.d.ts.map +1 -1
  47. package/dist/pendingStateManager.js +19 -7
  48. package/dist/pendingStateManager.js.map +1 -1
  49. package/dist/public.d.ts +1 -1
  50. package/dist/runtimeLayerCompatState.d.ts +1 -1
  51. package/dist/summary/documentSchema.d.ts +9 -3
  52. package/dist/summary/documentSchema.d.ts.map +1 -1
  53. package/dist/summary/documentSchema.js +19 -3
  54. package/dist/summary/documentSchema.js.map +1 -1
  55. package/dist/summary/orderedClientElection.js +2 -2
  56. package/dist/summary/orderedClientElection.js.map +1 -1
  57. package/dist/summary/summaryManager.d.ts +9 -0
  58. package/dist/summary/summaryManager.d.ts.map +1 -1
  59. package/dist/summary/summaryManager.js +29 -0
  60. package/dist/summary/summaryManager.js.map +1 -1
  61. package/internal.d.ts +1 -1
  62. package/legacy.d.ts +1 -1
  63. package/lib/containerCompatibility.d.ts +1 -1
  64. package/lib/containerCompatibility.d.ts.map +1 -1
  65. package/lib/containerCompatibility.js.map +1 -1
  66. package/lib/containerRuntime.d.ts +37 -10
  67. package/lib/containerRuntime.d.ts.map +1 -1
  68. package/lib/containerRuntime.js +106 -79
  69. package/lib/containerRuntime.js.map +1 -1
  70. package/lib/gc/garbageCollection.d.ts +1 -0
  71. package/lib/gc/garbageCollection.d.ts.map +1 -1
  72. package/lib/gc/garbageCollection.js +3 -8
  73. package/lib/gc/garbageCollection.js.map +1 -1
  74. package/lib/gc/gcDefinitions.d.ts +4 -0
  75. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  76. package/lib/gc/gcDefinitions.js.map +1 -1
  77. package/lib/index.d.ts +1 -1
  78. package/lib/index.d.ts.map +1 -1
  79. package/lib/index.js +1 -1
  80. package/lib/index.js.map +1 -1
  81. package/lib/legacy.d.ts +1 -1
  82. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  83. package/lib/opLifecycle/batchManager.js +2 -1
  84. package/lib/opLifecycle/batchManager.js.map +1 -1
  85. package/lib/opLifecycle/index.d.ts +1 -1
  86. package/lib/opLifecycle/index.d.ts.map +1 -1
  87. package/lib/opLifecycle/index.js +1 -1
  88. package/lib/opLifecycle/index.js.map +1 -1
  89. package/lib/opLifecycle/opGroupingManager.d.ts +6 -0
  90. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  91. package/lib/opLifecycle/opGroupingManager.js +10 -1
  92. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  93. package/lib/opLifecycle/opSerialization.d.ts +3 -1
  94. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  95. package/lib/opLifecycle/opSerialization.js +11 -9
  96. package/lib/opLifecycle/opSerialization.js.map +1 -1
  97. package/lib/opLifecycle/outbox.d.ts +0 -6
  98. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  99. package/lib/opLifecycle/outbox.js +2 -9
  100. package/lib/opLifecycle/outbox.js.map +1 -1
  101. package/lib/packageVersion.d.ts +1 -1
  102. package/lib/packageVersion.js +1 -1
  103. package/lib/packageVersion.js.map +1 -1
  104. package/lib/pendingStateManager.d.ts +7 -3
  105. package/lib/pendingStateManager.d.ts.map +1 -1
  106. package/lib/pendingStateManager.js +19 -7
  107. package/lib/pendingStateManager.js.map +1 -1
  108. package/lib/public.d.ts +1 -1
  109. package/lib/runtimeLayerCompatState.d.ts +1 -1
  110. package/lib/summary/documentSchema.d.ts +9 -3
  111. package/lib/summary/documentSchema.d.ts.map +1 -1
  112. package/lib/summary/documentSchema.js +19 -3
  113. package/lib/summary/documentSchema.js.map +1 -1
  114. package/lib/summary/orderedClientElection.js +2 -2
  115. package/lib/summary/orderedClientElection.js.map +1 -1
  116. package/lib/summary/summaryManager.d.ts +9 -0
  117. package/lib/summary/summaryManager.d.ts.map +1 -1
  118. package/lib/summary/summaryManager.js +29 -0
  119. package/lib/summary/summaryManager.js.map +1 -1
  120. package/package.json +28 -24
  121. package/src/containerCompatibility.ts +2 -0
  122. package/src/containerRuntime.ts +153 -93
  123. package/src/gc/garbageCollection.ts +4 -9
  124. package/src/gc/gcDefinitions.ts +4 -0
  125. package/src/index.ts +1 -0
  126. package/src/opLifecycle/batchManager.ts +2 -1
  127. package/src/opLifecycle/index.ts +1 -0
  128. package/src/opLifecycle/opGroupingManager.ts +11 -1
  129. package/src/opLifecycle/opSerialization.ts +14 -12
  130. package/src/opLifecycle/outbox.ts +2 -17
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +27 -11
  133. package/src/summary/documentSchema.ts +25 -2
  134. package/src/summary/orderedClientElection.ts +2 -2
  135. package/src/summary/summaryManager.ts +32 -0
@@ -48,12 +48,6 @@ export interface IOutboxConfig {
48
48
  * The maximum size of a batch that we can send over the wire.
49
49
  */
50
50
  readonly maxBatchSizeInBytes: number;
51
- /**
52
- * If true, maybeFlushPartialBatch will flush the batch if the reference sequence number changed
53
- * since the batch started. Otherwise, it will throw in this case (apart from reentrancy which is handled elsewhere).
54
- * Once the new throw-based flow is proved in a production environment, this option will be removed.
55
- */
56
- readonly flushPartialBatches: boolean;
57
51
  }
58
52
 
59
53
  export interface IOutboxParameters {
@@ -142,7 +136,7 @@ export function localBatchToOutboundBatch({
142
136
  // Shallow copy each message as we switch types
143
137
  const outboundMessages = localBatch.messages.map<OutboundBatchMessage>(
144
138
  ({ runtimeOp, ...message }) => ({
145
- contents: serializeOp(runtimeOp),
139
+ contents: serializeOp(runtimeOp).content,
146
140
  ...message,
147
141
  }),
148
142
  );
@@ -295,10 +289,7 @@ export class Outbox {
295
289
  this.logger.sendTelemetryEvent(
296
290
  {
297
291
  // Only log error if this is truly unexpected
298
- category:
299
- expectedDueToReentrancy || this.params.config.flushPartialBatches
300
- ? "generic"
301
- : "error",
292
+ category: expectedDueToReentrancy ? "generic" : "error",
302
293
  eventName: "ReferenceSequenceNumberMismatch",
303
294
  details: {
304
295
  expectedDueToReentrancy,
@@ -314,12 +305,6 @@ export class Outbox {
314
305
  );
315
306
  }
316
307
 
317
- // If we're configured to flush partial batches, do that now and return (don't throw)
318
- if (this.params.config.flushPartialBatches) {
319
- this.flushAll();
320
- return;
321
- }
322
-
323
308
  // If we are in a reentrant context, we know this can happen without causing any harm.
324
309
  if (expectedDueToReentrancy) {
325
310
  return;
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.90.0";
9
+ export const pkgVersion = "2.92.0";
@@ -335,7 +335,9 @@ export class PendingStateManager implements IDisposable {
335
335
  return this.pendingMessagesCount !== 0;
336
336
  }
337
337
 
338
- public getLocalState(snapshotSequenceNumber?: number): IPendingLocalState {
338
+ public getLocalState(snapshotSequenceNumber?: number): {
339
+ pending: IPendingLocalState;
340
+ } {
339
341
  assert(
340
342
  this.initialMessages.isEmpty(),
341
343
  0x2e9 /* "Must call getLocalState() after applying initial states" */,
@@ -359,10 +361,12 @@ export class PendingStateManager implements IDisposable {
359
361
  }
360
362
  }
361
363
  return {
362
- pendingStates: [
363
- ...newSavedOps,
364
- ...this.pendingMessages.toArray().map((message) => toSerializableForm(message)),
365
- ],
364
+ pending: {
365
+ pendingStates: [
366
+ ...newSavedOps,
367
+ ...this.pendingMessages.toArray().map((message) => toSerializableForm(message)),
368
+ ],
369
+ },
366
370
  };
367
371
  }
368
372
 
@@ -427,7 +431,7 @@ export class PendingStateManager implements IDisposable {
427
431
  clientId !== undefined,
428
432
  0xa33 /* clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case */,
429
433
  );
430
-
434
+ const batchInfo = { clientId, batchStartCsn, length: batch.length, ignoreBatchId, staged };
431
435
  for (const message of batch) {
432
436
  const {
433
437
  runtimeOp,
@@ -438,12 +442,11 @@ export class PendingStateManager implements IDisposable {
438
442
  const pendingMessage: IPendingMessage = {
439
443
  type: "message",
440
444
  referenceSequenceNumber,
441
- content: serializeOp(runtimeOp),
445
+ content: serializeOp(runtimeOp).content,
442
446
  runtimeOp,
443
447
  localOpMetadata,
444
448
  opMetadata,
445
- // Note: We only will read this off the first message, but put it on all for simplicity
446
- batchInfo: { clientId, batchStartCsn, length: batch.length, ignoreBatchId, staged },
449
+ batchInfo,
447
450
  };
448
451
  this.pendingMessages.push(pendingMessage);
449
452
  }
@@ -747,8 +750,12 @@ export class PendingStateManager implements IDisposable {
747
750
  * Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
748
751
  * states in its queue. This includes triggering resubmission of unacked ops.
749
752
  * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
753
+ *
754
+ * @returns The unique batch infos for all batches that were replayed.
750
755
  */
751
- public replayPendingStates(options?: ReplayPendingStateOptions): void {
756
+ public replayPendingStates(
757
+ options?: ReplayPendingStateOptions,
758
+ ): IPendingMessage["batchInfo"][] {
752
759
  const { committingStagedBatches, squash } = {
753
760
  ...defaultReplayPendingStatesOptions,
754
761
  ...options,
@@ -775,6 +782,7 @@ export class PendingStateManager implements IDisposable {
775
782
 
776
783
  const initialPendingMessagesCount = this.pendingMessages.length;
777
784
  let remainingPendingMessagesCount = this.pendingMessages.length;
785
+ const replayedBatchSet = new Set<IPendingMessage["batchInfo"]>();
778
786
 
779
787
  let seenStagedBatch = false;
780
788
 
@@ -812,6 +820,7 @@ export class PendingStateManager implements IDisposable {
812
820
  if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
813
821
  // Resubmit no messages, with the batchId. Will result in another empty batch marker.
814
822
  this.stateHandler.reSubmitBatch([], { batchId, staged, squash });
823
+ replayedBatchSet.add(pendingMessage.batchInfo);
815
824
  continue;
816
825
  }
817
826
 
@@ -838,6 +847,7 @@ export class PendingStateManager implements IDisposable {
838
847
  ],
839
848
  { batchId, staged, squash },
840
849
  );
850
+ replayedBatchSet.add(pendingMessage.batchInfo);
841
851
  continue;
842
852
  }
843
853
  // else: batchMetadataFlag === true (It's a typical multi-message batch)
@@ -877,6 +887,7 @@ export class PendingStateManager implements IDisposable {
877
887
  }
878
888
 
879
889
  this.stateHandler.reSubmitBatch(batch, { batchId, staged, squash });
890
+ replayedBatchSet.add(pendingMessage.batchInfo);
880
891
  }
881
892
 
882
893
  if (!committingStagedBatches) {
@@ -894,6 +905,8 @@ export class PendingStateManager implements IDisposable {
894
905
  clientId: this.stateHandler.clientId(),
895
906
  });
896
907
  }
908
+
909
+ return [...replayedBatchSet];
897
910
  }
898
911
 
899
912
  /**
@@ -904,11 +917,13 @@ export class PendingStateManager implements IDisposable {
904
917
  // callback will only be given staged messages with a valid runtime op (i.e. not empty batch and not an initial message with only serialized content)
905
918
  stagedMessage: IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage },
906
919
  ) => void,
907
- ): void {
920
+ ): IPendingMessage["batchInfo"][] {
921
+ const batchSet = new Set<IPendingMessage["batchInfo"]>();
908
922
  while (!this.pendingMessages.isEmpty()) {
909
923
  const stagedMessage = this.pendingMessages.peekBack();
910
924
  if (stagedMessage?.batchInfo.staged === true) {
911
925
  this.pendingMessages.pop();
926
+ batchSet.add(stagedMessage.batchInfo);
912
927
 
913
928
  if (hasTypicalRuntimeOp(stagedMessage)) {
914
929
  callback(stagedMessage);
@@ -921,6 +936,7 @@ export class PendingStateManager implements IDisposable {
921
936
  this.pendingMessages.toArray().every((m) => m.batchInfo.staged !== true),
922
937
  0xb89 /* Shouldn't be any more staged messages */,
923
938
  );
939
+ return [...batchSet];
924
940
  }
925
941
  }
926
942
 
@@ -512,7 +512,7 @@ function arrayToProp(arr: string[]): string[] | undefined {
512
512
  *
513
513
  * Users of this class need to use DocumentsSchemaController.sessionSchema to determine what features can be used.
514
514
  *
515
- * There are two modes this class can operate:
515
+ * There are three modes this class can operate:
516
516
  * 1) Legacy mode. In such mode it does not issue any ops to change document schema. Any changes happen implicitly,
517
517
  * right away, and new features are available right away
518
518
  * 2) Non-legacy mode. In such mode any changes to schema require an op roundtrip. This class will manage such transitions.
@@ -523,6 +523,9 @@ function arrayToProp(arr: string[]): string[] | undefined {
523
523
  * then eventually all documents that are modified will have that feature reflected in their schema. It could require
524
524
  * multiple reloads / new sessions to get there (depends on if code reacts to schema changes right away, or only consults
525
525
  * schema on document load).
526
+ * 3) Schema upgrade disabled mode (disableSchemaUpgrade = true). In this mode the controller will never send DocumentSchemaChange ops
527
+ * and will throw an error if any incoming schema change ops are received. The document schema is effectively frozen at the schema
528
+ * loaded for this session (snapshot) and will not accept further schema-change ops.
526
529
  *
527
530
  * How schemas are changed (in non-legacy mode):
528
531
  * If a client needs to change a schema, it will attempt to do so as part of normal ops sending process.
@@ -569,6 +572,7 @@ export class DocumentsSchemaController {
569
572
  * @param onSchemaChange - callback that is called whenever schema is changed (not called on creation / load, only when processing document schema change ops)
570
573
  * @param info - Informational properties of the document that are not subject to strict schema enforcement
571
574
  * @param logger - telemetry logger from the runtime
575
+ * @param disableSchemaUpgrade - when true, the controller will never send or accept DocumentSchemaChange ops
572
576
  */
573
577
  constructor(
574
578
  existing: boolean,
@@ -578,6 +582,7 @@ export class DocumentsSchemaController {
578
582
  private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
579
583
  info: IDocumentSchemaInfo,
580
584
  logger: ITelemetryLoggerExt,
585
+ private readonly disableSchemaUpgrade: boolean,
581
586
  ) {
582
587
  // For simplicity, let's only support new schema features for explicit schema control mode
583
588
  assert(
@@ -704,9 +709,12 @@ export class DocumentsSchemaController {
704
709
  * Called by Container runtime whenever it is about to send some op.
705
710
  * It gives opportunity for controller to issue its own ops - we do not want to send ops if there are no local changes in document.
706
711
  * Please consider note above constructor about race conditions - current design is to generate op only once in a session lifetime.
707
- * @returns Optional message to send.
712
+ * @returns Optional message to send. Always returns undefined when disableSchemaUpgrade is true.
708
713
  */
709
714
  public maybeGenerateSchemaMessage(): IDocumentSchemaChangeMessageOutgoing | undefined {
715
+ if (this.disableSchemaUpgrade) {
716
+ return undefined;
717
+ }
710
718
  if (this.futureSchema !== undefined && !this.opPending) {
711
719
  this.opPending = true;
712
720
  assert(
@@ -739,6 +747,7 @@ export class DocumentsSchemaController {
739
747
  /**
740
748
  * Process document schema change messages
741
749
  * Called by ContainerRuntime whenever it sees document schema messages.
750
+ * When disableSchemaUpgrade is true, an error is thrown if any incoming schema change ops are received.
742
751
  * @param contents - contents of the messages
743
752
  * @param local - whether op is local
744
753
  * @param sequenceNumber - sequence number of the op
@@ -749,6 +758,20 @@ export class DocumentsSchemaController {
749
758
  local: boolean,
750
759
  sequenceNumber: number,
751
760
  ): boolean {
761
+ if (this.disableSchemaUpgrade) {
762
+ assert(
763
+ !local,
764
+ 0xceb /* local schema change messages should never be generated when disableSchemaUpgrade is enabled */,
765
+ );
766
+ // Clients with disableSchemaUpgrade enabled should never generate schema change messages, but they
767
+ // may receive them from misconfigured clients. In such case, throw on any incoming schema change ops
768
+ // to prevent unexpected schema upgrades.
769
+ throw DataProcessingError.create(
770
+ "DocSchema: Received schema change op while disableSchemaUpgrade is enabled",
771
+ "processDocumentSchemaMessages",
772
+ undefined,
773
+ );
774
+ }
752
775
  for (const content of contents) {
753
776
  this.validateSeqNumber(content.refSeq, this.documentSchema.refSeq, "content.refSeq");
754
777
  this.validateSeqNumber(this.documentSchema.refSeq, sequenceNumber, "refSeq");
@@ -515,7 +515,7 @@ export class OrderedClientElection
515
515
  "InteractiveClientElected",
516
516
  client,
517
517
  sequenceNumber,
518
- true /* forceSend */,
518
+ false /* forceSend */,
519
519
  reason,
520
520
  );
521
521
  // Changing the elected parent as well.
@@ -544,7 +544,7 @@ export class OrderedClientElection
544
544
  "ParentElected",
545
545
  client,
546
546
  sequenceNumber,
547
- true /* forceSend */,
547
+ false /* forceSend */,
548
548
  reason,
549
549
  );
550
550
  this._electedParent = client;
@@ -102,8 +102,17 @@ export class SummaryManager
102
102
  private latestClientId: string | undefined;
103
103
  private state = SummaryManagerState.Off;
104
104
  private summarizer?: ISummarizer;
105
+ private pendingStopReason?: SummarizerStopReason;
105
106
  private _disposed = false;
106
107
  private summarizerStopTimeout?: ReturnType<typeof setTimeout>;
108
+ /**
109
+ * Monotonically increasing counter that tracks summarizer lifecycle generations.
110
+ * Incremented each time {@link cleanupAfterSummarizerStop} runs. Used by the
111
+ * promise chain in {@link startSummarization} to detect that cleanup has already
112
+ * been performed by another path (e.g. the stop timeout), so it can skip
113
+ * redundant cleanup that would corrupt the state machine.
114
+ */
115
+ private summarizerGeneration = 0;
107
116
 
108
117
  public get disposed(): boolean {
109
118
  return this._disposed;
@@ -245,6 +254,8 @@ export class SummaryManager
245
254
 
246
255
  assert(this.summarizer === undefined, 0x262 /* "Old summarizer is still working!" */);
247
256
 
257
+ const generation = this.summarizerGeneration;
258
+
248
259
  this.delayBeforeCreatingSummarizer()
249
260
  .then(async (startWithInitialDelay: boolean) => {
250
261
  if (this.disposed) {
@@ -277,6 +288,13 @@ export class SummaryManager
277
288
  this.summarizer = summarizer;
278
289
  this.setupForwardedEvents(summarizer);
279
290
 
291
+ // A stop may have been requested while we were awaiting summarizer creation.
292
+ // Replay it now so the summarizer can observe the stop intent and move to exit.
293
+ if (this.pendingStopReason !== undefined) {
294
+ summarizer.stop(this.pendingStopReason);
295
+ this.pendingStopReason = undefined;
296
+ }
297
+
280
298
  // Re-validate that it need to be running. Due to asynchrony, it may be not the case anymore
281
299
  // If we can't run the LastSummary, simply return as to avoid paying the cost of launching
282
300
  // the summarizer at all.
@@ -347,13 +365,25 @@ export class SummaryManager
347
365
  }
348
366
  })
349
367
  .finally(() => {
368
+ if (generation !== this.summarizerGeneration) {
369
+ // Cleanup was already performed by another path (e.g. the stop timeout),
370
+ // and a new summarizer cycle may have started. Running cleanup again
371
+ // would corrupt the current state machine cycle.
372
+ this.logger.sendTelemetryEvent({
373
+ eventName: "SummarizerCleanupAlreadyDone",
374
+ currentState: this.state,
375
+ });
376
+ return;
377
+ }
350
378
  assert(this.state !== SummaryManagerState.Off, 0x264 /* "Expected: Not Off" */);
351
379
  this.cleanupAfterSummarizerStop();
352
380
  });
353
381
  }
354
382
 
355
383
  private cleanupAfterSummarizerStop(): void {
384
+ this.summarizerGeneration++;
356
385
  this.state = SummaryManagerState.Off;
386
+ this.pendingStopReason = undefined;
357
387
 
358
388
  // Clear any pending stop timeout to avoid it firing for a different summarizer
359
389
  if (this.summarizerStopTimeout !== undefined) {
@@ -375,6 +405,7 @@ export class SummaryManager
375
405
  return;
376
406
  }
377
407
  this.state = SummaryManagerState.Stopping;
408
+ this.pendingStopReason = reason;
378
409
 
379
410
  // Stopping the running summarizer client should trigger a change
380
411
  // in states when the running summarizer closes
@@ -481,6 +512,7 @@ export class SummaryManager
481
512
  this.connectedState.off("connected", this.handleConnected);
482
513
  this.connectedState.off("disconnected", this.handleDisconnected);
483
514
  this.cleanupForwardedEvents();
515
+ this.pendingStopReason = undefined;
484
516
  if (this.summarizerStopTimeout !== undefined) {
485
517
  clearTimeout(this.summarizerStopTimeout);
486
518
  }