@fluidframework/container-runtime 0.59.4001 → 1.0.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 (137) hide show
  1. package/.eslintrc.js +1 -1
  2. package/dist/blobManager.d.ts +2 -2
  3. package/dist/blobManager.d.ts.map +1 -1
  4. package/dist/blobManager.js +12 -11
  5. package/dist/blobManager.js.map +1 -1
  6. package/dist/connectionTelemetry.js +3 -3
  7. package/dist/connectionTelemetry.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +125 -29
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +242 -110
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/dataStoreContext.d.ts +4 -2
  13. package/dist/dataStoreContext.d.ts.map +1 -1
  14. package/dist/dataStoreContext.js +16 -5
  15. package/dist/dataStoreContext.js.map +1 -1
  16. package/dist/dataStores.d.ts +4 -3
  17. package/dist/dataStores.d.ts.map +1 -1
  18. package/dist/dataStores.js +9 -3
  19. package/dist/dataStores.js.map +1 -1
  20. package/dist/garbageCollection.d.ts +3 -3
  21. package/dist/garbageCollection.d.ts.map +1 -1
  22. package/dist/garbageCollection.js +10 -8
  23. package/dist/garbageCollection.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/orderedClientElection.js +0 -4
  29. package/dist/orderedClientElection.js.map +1 -1
  30. package/dist/packageVersion.d.ts +1 -1
  31. package/dist/packageVersion.d.ts.map +1 -1
  32. package/dist/packageVersion.js +1 -1
  33. package/dist/packageVersion.js.map +1 -1
  34. package/dist/pendingStateManager.d.ts +30 -29
  35. package/dist/pendingStateManager.d.ts.map +1 -1
  36. package/dist/pendingStateManager.js +72 -109
  37. package/dist/pendingStateManager.js.map +1 -1
  38. package/dist/runningSummarizer.d.ts +4 -3
  39. package/dist/runningSummarizer.d.ts.map +1 -1
  40. package/dist/runningSummarizer.js +11 -6
  41. package/dist/runningSummarizer.js.map +1 -1
  42. package/dist/serializedSnapshotStorage.d.ts +58 -0
  43. package/dist/serializedSnapshotStorage.d.ts.map +1 -0
  44. package/dist/serializedSnapshotStorage.js +108 -0
  45. package/dist/serializedSnapshotStorage.js.map +1 -0
  46. package/dist/summarizer.d.ts +11 -4
  47. package/dist/summarizer.d.ts.map +1 -1
  48. package/dist/summarizer.js +18 -9
  49. package/dist/summarizer.js.map +1 -1
  50. package/dist/summarizerHeuristics.d.ts +5 -3
  51. package/dist/summarizerHeuristics.d.ts.map +1 -1
  52. package/dist/summarizerHeuristics.js +10 -3
  53. package/dist/summarizerHeuristics.js.map +1 -1
  54. package/dist/summarizerTypes.d.ts +4 -2
  55. package/dist/summarizerTypes.d.ts.map +1 -1
  56. package/dist/summarizerTypes.js.map +1 -1
  57. package/dist/summaryManager.d.ts +3 -3
  58. package/dist/summaryManager.d.ts.map +1 -1
  59. package/dist/summaryManager.js +7 -7
  60. package/dist/summaryManager.js.map +1 -1
  61. package/garbageCollection.md +9 -1
  62. package/lib/blobManager.d.ts +2 -2
  63. package/lib/blobManager.d.ts.map +1 -1
  64. package/lib/blobManager.js +12 -11
  65. package/lib/blobManager.js.map +1 -1
  66. package/lib/connectionTelemetry.js +3 -3
  67. package/lib/connectionTelemetry.js.map +1 -1
  68. package/lib/containerRuntime.d.ts +125 -29
  69. package/lib/containerRuntime.d.ts.map +1 -1
  70. package/lib/containerRuntime.js +243 -111
  71. package/lib/containerRuntime.js.map +1 -1
  72. package/lib/dataStoreContext.d.ts +4 -2
  73. package/lib/dataStoreContext.d.ts.map +1 -1
  74. package/lib/dataStoreContext.js +16 -5
  75. package/lib/dataStoreContext.js.map +1 -1
  76. package/lib/dataStores.d.ts +4 -3
  77. package/lib/dataStores.d.ts.map +1 -1
  78. package/lib/dataStores.js +9 -3
  79. package/lib/dataStores.js.map +1 -1
  80. package/lib/garbageCollection.d.ts +3 -3
  81. package/lib/garbageCollection.d.ts.map +1 -1
  82. package/lib/garbageCollection.js +10 -8
  83. package/lib/garbageCollection.js.map +1 -1
  84. package/lib/index.d.ts +2 -2
  85. package/lib/index.d.ts.map +1 -1
  86. package/lib/index.js +1 -1
  87. package/lib/index.js.map +1 -1
  88. package/lib/orderedClientElection.js +0 -4
  89. package/lib/orderedClientElection.js.map +1 -1
  90. package/lib/packageVersion.d.ts +1 -1
  91. package/lib/packageVersion.d.ts.map +1 -1
  92. package/lib/packageVersion.js +1 -1
  93. package/lib/packageVersion.js.map +1 -1
  94. package/lib/pendingStateManager.d.ts +30 -29
  95. package/lib/pendingStateManager.d.ts.map +1 -1
  96. package/lib/pendingStateManager.js +72 -109
  97. package/lib/pendingStateManager.js.map +1 -1
  98. package/lib/runningSummarizer.d.ts +4 -3
  99. package/lib/runningSummarizer.d.ts.map +1 -1
  100. package/lib/runningSummarizer.js +11 -6
  101. package/lib/runningSummarizer.js.map +1 -1
  102. package/lib/serializedSnapshotStorage.d.ts +58 -0
  103. package/lib/serializedSnapshotStorage.d.ts.map +1 -0
  104. package/lib/serializedSnapshotStorage.js +104 -0
  105. package/lib/serializedSnapshotStorage.js.map +1 -0
  106. package/lib/summarizer.d.ts +11 -4
  107. package/lib/summarizer.d.ts.map +1 -1
  108. package/lib/summarizer.js +18 -9
  109. package/lib/summarizer.js.map +1 -1
  110. package/lib/summarizerHeuristics.d.ts +5 -3
  111. package/lib/summarizerHeuristics.d.ts.map +1 -1
  112. package/lib/summarizerHeuristics.js +10 -3
  113. package/lib/summarizerHeuristics.js.map +1 -1
  114. package/lib/summarizerTypes.d.ts +4 -2
  115. package/lib/summarizerTypes.d.ts.map +1 -1
  116. package/lib/summarizerTypes.js.map +1 -1
  117. package/lib/summaryManager.d.ts +3 -3
  118. package/lib/summaryManager.d.ts.map +1 -1
  119. package/lib/summaryManager.js +7 -7
  120. package/lib/summaryManager.js.map +1 -1
  121. package/package.json +46 -31
  122. package/src/blobManager.ts +29 -15
  123. package/src/connectionTelemetry.ts +3 -3
  124. package/src/containerRuntime.ts +388 -135
  125. package/src/dataStoreContext.ts +27 -5
  126. package/src/dataStores.ts +15 -3
  127. package/src/garbageCollection.ts +21 -14
  128. package/src/index.ts +7 -1
  129. package/src/orderedClientElection.ts +1 -1
  130. package/src/packageVersion.ts +1 -1
  131. package/src/pendingStateManager.ts +104 -123
  132. package/src/runningSummarizer.ts +20 -10
  133. package/src/serializedSnapshotStorage.ts +146 -0
  134. package/src/summarizer.ts +20 -16
  135. package/src/summarizerHeuristics.ts +21 -5
  136. package/src/summarizerTypes.ts +4 -2
  137. package/src/summaryManager.ts +5 -6
@@ -58,6 +58,7 @@ import {
58
58
  ISummarizeResult,
59
59
  ISummarizerNodeWithGC,
60
60
  SummarizeInternalFn,
61
+ ITelemetryContext,
61
62
  } from "@fluidframework/runtime-definitions";
62
63
  import { addBlobToSummary, convertSummaryTreeToITree } from "@fluidframework/runtime-utils";
63
64
  import {
@@ -289,7 +290,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
289
290
  };
290
291
 
291
292
  const thisSummarizeInternal =
292
- async (fullTree: boolean, trackState: boolean) => this.summarizeInternal(fullTree, trackState);
293
+ async (fullTree: boolean, trackState: boolean, telemetryContext?: ITelemetryContext) =>
294
+ this.summarizeInternal(fullTree, trackState, telemetryContext);
293
295
 
294
296
  this.summarizerNode = props.createSummarizerNodeFn(
295
297
  thisSummarizeInternal,
@@ -445,16 +447,25 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
445
447
  * Returns a summary at the current sequence number.
446
448
  * @param fullTree - true to bypass optimizations and force a full summary tree
447
449
  * @param trackState - This tells whether we should track state from this summary.
450
+ * @param telemetryContext - summary data passed through the layers for telemetry purposes
448
451
  */
449
- public async summarize(fullTree: boolean = false, trackState: boolean = true): Promise<ISummarizeResult> {
450
- return this.summarizerNode.summarize(fullTree, trackState);
452
+ public async summarize(
453
+ fullTree: boolean = false,
454
+ trackState: boolean = true,
455
+ telemetryContext?: ITelemetryContext,
456
+ ): Promise<ISummarizeResult> {
457
+ return this.summarizerNode.summarize(fullTree, trackState, telemetryContext);
451
458
  }
452
459
 
453
- private async summarizeInternal(fullTree: boolean, trackState: boolean): Promise<ISummarizeInternalResult> {
460
+ private async summarizeInternal(
461
+ fullTree: boolean,
462
+ trackState: boolean,
463
+ telemetryContext?: ITelemetryContext,
464
+ ): Promise<ISummarizeInternalResult> {
454
465
  await this.realize();
455
466
 
456
467
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
457
- const summarizeResult = await this.channel!.summarize(fullTree, trackState);
468
+ const summarizeResult = await this.channel!.summarize(fullTree, trackState, telemetryContext);
458
469
  let pathPartsForChildren: string[] | undefined;
459
470
 
460
471
  if (!this.disableIsolatedChannels) {
@@ -717,6 +728,17 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
717
728
  this.channel.reSubmit(innerContents.type, innerContents.content, localOpMetadata);
718
729
  }
719
730
 
731
+ public rollback(contents: any, localOpMetadata: unknown) {
732
+ if (!this.channel) {
733
+ throw new Error("Channel must exist when rolling back ops");
734
+ }
735
+ if (!this.channel.rollback) {
736
+ throw new Error("Channel doesn't support rollback");
737
+ }
738
+ const innerContents = contents as FluidDataStoreMessage;
739
+ this.channel.rollback(innerContents.type, innerContents.content, localOpMetadata);
740
+ }
741
+
720
742
  public async applyStashedOp(contents: any): Promise<unknown> {
721
743
  if (!this.channel) {
722
744
  await this.realize();
package/src/dataStores.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  InboundAttachMessage,
26
26
  ISummarizeResult,
27
27
  ISummaryTreeWithStats,
28
+ ITelemetryContext,
28
29
  } from "@fluidframework/runtime-definitions";
29
30
  import {
30
31
  convertSnapshotTreeToSummaryTree,
@@ -380,6 +381,13 @@ export class DataStores implements IDisposable {
380
381
  context.reSubmit(envelope.contents, localOpMetadata);
381
382
  }
382
383
 
384
+ public rollbackDataStoreOp(content: any, localOpMetadata: unknown) {
385
+ const envelope = content as IEnvelope;
386
+ const context = this.contexts.get(envelope.address);
387
+ assert(!!context, 0x2e8 /* "There should be a store context for the op" */);
388
+ context.rollback(envelope.contents, localOpMetadata);
389
+ }
390
+
383
391
  public async applyStashedOp(content: any): Promise<unknown> {
384
392
  const envelope = content as IEnvelope;
385
393
  const context = this.contexts.get(envelope.address);
@@ -473,7 +481,11 @@ export class DataStores implements IDisposable {
473
481
  return this.contexts.size;
474
482
  }
475
483
 
476
- public async summarize(fullTree: boolean, trackState: boolean): Promise<ISummaryTreeWithStats> {
484
+ public async summarize(
485
+ fullTree: boolean,
486
+ trackState: boolean,
487
+ telemetryContext?: ITelemetryContext,
488
+ ): Promise<ISummaryTreeWithStats> {
477
489
  const summaryBuilder = new SummaryTreeBuilder();
478
490
 
479
491
  // Iterate over each store and ask it to snapshot
@@ -484,14 +496,14 @@ export class DataStores implements IDisposable {
484
496
  0x165 /* "Summarizer cannot work if client has local changes" */);
485
497
  return context.attachState === AttachState.Attached;
486
498
  }).map(async ([contextId, context]) => {
487
- const contextSummary = await context.summarize(fullTree, trackState);
499
+ const contextSummary = await context.summarize(fullTree, trackState, telemetryContext);
488
500
  summaryBuilder.addWithStats(contextId, contextSummary);
489
501
  }));
490
502
 
491
503
  return summaryBuilder.getSummaryTree();
492
504
  }
493
505
 
494
- public createSummary(): ISummaryTreeWithStats {
506
+ public createSummary(telemetryContext?: ITelemetryContext): ISummaryTreeWithStats {
495
507
  const builder = new SummaryTreeBuilder();
496
508
  // Attaching graph of some stores can cause other stores to get bound too.
497
509
  // So keep taking summary until no new stores get bound.
@@ -22,8 +22,9 @@ import {
22
22
  IGarbageCollectionData,
23
23
  IGarbageCollectionState,
24
24
  IGarbageCollectionDetailsBase,
25
- IGarbageCollectionNodeData,
26
25
  ISummarizeResult,
26
+ ITelemetryContext,
27
+ IGarbageCollectionNodeData,
27
28
  } from "@fluidframework/runtime-definitions";
28
29
  import {
29
30
  mergeStats,
@@ -162,7 +163,11 @@ export interface IGarbageCollector {
162
163
  options: { logger?: ITelemetryLogger; runGC?: boolean; runSweep?: boolean; fullGC?: boolean; },
163
164
  ): Promise<IGCStats>;
164
165
  /** Summarizes the GC data and returns it as a summary tree. */
165
- summarize(fullTree: boolean, trackState: boolean): ISummarizeResult | undefined;
166
+ summarize(
167
+ fullTree: boolean,
168
+ trackState: boolean,
169
+ telemetryContext?: ITelemetryContext,
170
+ ): ISummarizeResult | undefined;
166
171
  /** Returns the garbage collector specific metadata to be written into the summary. */
167
172
  getMetadata(): IGCMetadata;
168
173
  /** Returns a map of each node id to its base GC details in the base summary. */
@@ -330,7 +335,7 @@ export class GarbageCollector implements IGarbageCollector {
330
335
  private _writeDataAtRoot: boolean = false;
331
336
  public get writeDataAtRoot(): boolean {
332
337
  return this._writeDataAtRoot;
333
- }
338
+ }
334
339
 
335
340
  /**
336
341
  * Tells whether the initial GC state needs to be reset. This can happen under 2 conditions:
@@ -420,12 +425,14 @@ export class GarbageCollector implements IGarbageCollector {
420
425
  } else {
421
426
  // Sweep should not be enabled without enabling GC mark phase. We could silently disable sweep in this
422
427
  // scenario but explicitly failing makes it clearer and promotes correct usage.
423
- if (gcOptions.sweepAllowed && !gcOptions.gcAllowed) {
428
+ if (gcOptions.sweepAllowed && gcOptions.gcAllowed === false) {
424
429
  throw new UsageError("GC sweep phase cannot be enabled without enabling GC mark phase");
425
430
  }
426
431
 
427
- // For new documents, GC has to be explicitly enabled via the flags in GC options.
428
- this.gcEnabled = gcOptions.gcAllowed === true;
432
+ // For new documents, GC is enabled by default. It can be explicitly disabled by setting the gcAllowed
433
+ // flag in GC options to false.
434
+ this.gcEnabled = gcOptions.gcAllowed !== false;
435
+ // The sweep phase has to be explicitly enabled by setting the sweepAllowed flag in GC options to true.
429
436
  this.sweepEnabled = gcOptions.sweepAllowed === true;
430
437
 
431
438
  // Set the Session Expiry only if the flag is enabled or the test option is set.
@@ -588,7 +595,7 @@ export class GarbageCollector implements IGarbageCollector {
588
595
  return;
589
596
  }
590
597
 
591
- const gcNodes: { [ id: string ]: string[]; } = {};
598
+ const gcNodes: { [id: string]: string[]; } = {};
592
599
  for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
593
600
  if (nodeData.unreferencedTimestampMs !== undefined) {
594
601
  this.unreferencedNodesState.set(
@@ -613,7 +620,7 @@ export class GarbageCollector implements IGarbageCollector {
613
620
  return new Map();
614
621
  }
615
622
 
616
- const gcNodes: { [ id: string ]: string[]; } = {};
623
+ const gcNodes: { [id: string]: string[]; } = {};
617
624
  for (const [nodeId, nodeData] of Object.entries(baseState.gcNodes)) {
618
625
  gcNodes[nodeId] = Array.from(nodeData.outboundRoutes);
619
626
  }
@@ -737,8 +744,7 @@ export class GarbageCollector implements IGarbageCollector {
737
744
  this.completedRuns++;
738
745
 
739
746
  return gcStats;
740
- },
741
- { end: true, cancel: "error" });
747
+ }, { end: true, cancel: "error" });
742
748
  }
743
749
 
744
750
  /**
@@ -749,6 +755,7 @@ export class GarbageCollector implements IGarbageCollector {
749
755
  public summarize(
750
756
  fullTree: boolean,
751
757
  trackState: boolean,
758
+ telemetryContext?: ITelemetryContext,
752
759
  ): ISummarizeResult | undefined {
753
760
  if (!this.shouldRunGC || this.previousGCDataFromLastRun === undefined) {
754
761
  return;
@@ -1338,16 +1345,16 @@ function meetsMinimumVersionRequirement(currentVersion: string, minimumVersion:
1338
1345
  */
1339
1346
  export function semverCompare(currentVersion: string, minimumVersion: string): number {
1340
1347
  const minimumValues = minimumVersion.split(".").map((value): number => {
1341
- assert(isNaN(+value) === false, 0x2fa /* Expected real numbers in minimum version! */);
1348
+ assert(isNaN(+value) === false, "Expected real numbers in minimum version!");
1342
1349
  return Number.parseInt(value, 10);
1343
1350
  });
1344
- assert(minimumValues.length === 3, 0x2fb /* Expected minimumVersion to be [major].[minor].[patch] */);
1351
+ assert(minimumValues.length === 3, "Expected minimumVersion to be [major].[minor].[patch]");
1345
1352
  const [minMajor, minMinor, minPatch] = minimumValues;
1346
1353
 
1347
1354
  const currentValuesString = currentVersion.split(/\W/);
1348
- assert(currentValuesString.length >= 3, 0x2fc /* Expected version to match semver rules! */);
1355
+ assert(currentValuesString.length >= 3, "Expected version to match semver rules!");
1349
1356
  const currentValues = currentValuesString.slice(0, 3).map((value) => {
1350
- assert(isNaN(+value) === false, 0x2fd /* Expected real numbers in minimum version! */);
1357
+ assert(isNaN(+value) === false, "Expected real numbers in minimum version!");
1351
1358
  return Number.parseInt(value, 10);
1352
1359
  });
1353
1360
  const [cMajor, cMinor, cPatch] = currentValues;
package/src/index.ts CHANGED
@@ -9,7 +9,12 @@ export {
9
9
  ContainerRuntimeMessage,
10
10
  IGCRuntimeOptions,
11
11
  ISummaryRuntimeOptions,
12
+ ISummaryBaseConfiguration,
13
+ ISummaryConfigurationHeuristics,
14
+ ISummaryConfigurationDisableSummarizer,
15
+ ISummaryConfigurationDisableHeuristics,
12
16
  IContainerRuntimeOptions,
17
+ IPendingRuntimeState,
13
18
  IRootSummaryTreeWithStats,
14
19
  isRuntimeMessage,
15
20
  RuntimeMessage,
@@ -18,6 +23,8 @@ export {
18
23
  agentSchedulerId,
19
24
  ContainerRuntime,
20
25
  RuntimeHeaders,
26
+ ISummaryConfiguration,
27
+ DefaultSummaryConfiguration,
21
28
  } from "./containerRuntime";
22
29
  export { DeltaScheduler } from "./deltaScheduler";
23
30
  export { FluidDataStoreRegistry } from "./dataStoreRegistry";
@@ -55,7 +62,6 @@ export {
55
62
  ISummarizer,
56
63
  ISummarizerEvents,
57
64
  ISummarizerInternalsProvider,
58
- ISummarizerOptions,
59
65
  ISummarizerRuntime,
60
66
  ISummarizingWarning,
61
67
  ISummaryCancellationToken,
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
-
5
+ /* eslint-disable @rushstack/no-new-null */
6
6
  import { IEvent, IEventProvider, ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
8
8
  import { IDeltaManager } from "@fluidframework/container-definitions";
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "0.59.4001";
9
+ export const pkgVersion = "1.0.0";
@@ -5,13 +5,15 @@
5
5
 
6
6
  import { IDisposable } from "@fluidframework/common-definitions";
7
7
  import { assert, Lazy } from "@fluidframework/common-utils";
8
+ import { ICriticalContainerError } from "@fluidframework/container-definitions";
8
9
  import { DataProcessingError } from "@fluidframework/container-utils";
9
10
  import {
10
11
  ISequencedDocumentMessage,
11
12
  } from "@fluidframework/protocol-definitions";
12
13
  import { FlushMode } from "@fluidframework/runtime-definitions";
14
+ import { wrapError } from "@fluidframework/telemetry-utils";
13
15
  import Deque from "double-ended-queue";
14
- import { ContainerRuntime, ContainerMessageType, isRuntimeMessage } from "./containerRuntime";
16
+ import { ContainerMessageType } from "./containerRuntime";
15
17
 
16
18
  /**
17
19
  * This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
@@ -47,16 +49,31 @@ export interface IPendingFlush {
47
49
  export type IPendingState = IPendingMessage | IPendingFlushMode | IPendingFlush;
48
50
 
49
51
  export interface IPendingLocalState {
50
- /**
51
- * client ID we most recently connected with, or undefined if we never connected
52
- */
53
- clientId?: string;
54
52
  /**
55
53
  * list of pending states, including ops and batch information
56
54
  */
57
55
  pendingStates: IPendingState[];
58
56
  }
59
57
 
58
+ export interface IRuntimeStateHandler{
59
+ connected(): boolean;
60
+ clientId(): string | undefined;
61
+ flushMode(): FlushMode;
62
+ setFlushMode(mode: FlushMode): void;
63
+ close(error?: ICriticalContainerError): void;
64
+ applyStashedOp: (type: ContainerMessageType, content: ISequencedDocumentMessage) => Promise<unknown>;
65
+ flush(): void;
66
+ reSubmit(
67
+ type: ContainerMessageType,
68
+ content: any,
69
+ localOpMetadata: unknown,
70
+ opMetadata: Record<string, unknown> | undefined): void;
71
+ rollback(
72
+ type: ContainerMessageType,
73
+ content: any,
74
+ localOpMetadata: unknown): void;
75
+ }
76
+
60
77
  /**
61
78
  * PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
62
79
  * acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
@@ -69,15 +86,16 @@ export interface IPendingLocalState {
69
86
  export class PendingStateManager implements IDisposable {
70
87
  private readonly pendingStates = new Deque<IPendingState>();
71
88
  private readonly initialStates: Deque<IPendingState>;
72
- private readonly previousClientIds = new Set<string>();
73
- private readonly firstStashedCSN: number = -1;
74
89
  private readonly disposeOnce = new Lazy<void>(() => {
75
90
  this.initialStates.clear();
76
91
  this.pendingStates.clear();
77
92
  });
78
93
 
79
94
  // Maintains the count of messages that are currently unacked.
80
- private pendingMessagesCount: number = 0;
95
+ private _pendingMessagesCount: number = 0;
96
+ public get pendingMessagesCount(): number {
97
+ return this._pendingMessagesCount;
98
+ }
81
99
 
82
100
  // Indicates whether we are processing a batch.
83
101
  private isProcessingBatch: boolean = false;
@@ -95,22 +113,18 @@ export class PendingStateManager implements IDisposable {
95
113
 
96
114
  private clientId: string | undefined;
97
115
 
98
- private get connected(): boolean {
99
- return this.containerRuntime.connected;
100
- }
101
-
102
116
  /**
103
117
  * Called to check if there are any pending messages in the pending state queue.
104
118
  * @returns A boolean indicating whether there are messages or not.
105
119
  */
106
120
  public hasPendingMessages(): boolean {
107
- return this.pendingMessagesCount !== 0;
121
+ return this._pendingMessagesCount !== 0 || !this.initialStates.isEmpty();
108
122
  }
109
123
 
110
124
  public getLocalState(): IPendingLocalState | undefined {
125
+ assert(this.initialStates.isEmpty(), 0x2e9 /* "Must call getLocalState() after applying initial states" */);
111
126
  if (this.hasPendingMessages()) {
112
127
  return {
113
- clientId: this.clientId,
114
128
  pendingStates: this.pendingStates.toArray().map(
115
129
  // delete localOpMetadata since it may not be serializable
116
130
  // and will be regenerated by applyStashedOp()
@@ -120,23 +134,12 @@ export class PendingStateManager implements IDisposable {
120
134
  }
121
135
 
122
136
  constructor(
123
- private readonly containerRuntime: ContainerRuntime,
124
- private readonly applyStashedOp: (type, content) => Promise<unknown>,
137
+ private readonly stateHandler: IRuntimeStateHandler,
125
138
  initialFlushMode: FlushMode,
126
139
  initialLocalState: IPendingLocalState | undefined,
127
140
  ) {
128
141
  this.initialStates = new Deque<IPendingState>(initialLocalState?.pendingStates ?? []);
129
142
 
130
- if (initialLocalState) {
131
- if (initialLocalState?.clientId) {
132
- this.previousClientIds.add(initialLocalState.clientId);
133
- }
134
- // get stashed op count and client sequence number of first op
135
- const messages = initialLocalState.pendingStates
136
- .filter((state) => state.type === "message") as IPendingMessage[];
137
- this.firstStashedCSN = messages[0].clientSequenceNumber;
138
- }
139
-
140
143
  this.flushModeForNextMessage = initialFlushMode;
141
144
  this.onFlushModeUpdated(initialFlushMode);
142
145
  }
@@ -172,7 +175,7 @@ export class PendingStateManager implements IDisposable {
172
175
 
173
176
  this.pendingStates.push(pendingMessage);
174
177
 
175
- this.pendingMessagesCount++;
178
+ this._pendingMessagesCount++;
176
179
  }
177
180
 
178
181
  /**
@@ -189,7 +192,7 @@ export class PendingStateManager implements IDisposable {
189
192
  public onFlush() {
190
193
  // If the FlushMode is Immediate, we don't need to track an explicit flush call because every message is
191
194
  // automatically flushed. So, flush is a no-op.
192
- if (this.containerRuntime.flushMode === FlushMode.Immediate) {
195
+ if (this.stateHandler.flushMode() === FlushMode.Immediate) {
193
196
  return;
194
197
  }
195
198
 
@@ -205,21 +208,25 @@ export class PendingStateManager implements IDisposable {
205
208
 
206
209
  /**
207
210
  * Applies stashed ops at their reference sequence number so they are ready to be ACKed or resubmitted
211
+ * @param seqNum - Sequence number at which to apply ops. Will apply all ops if seqNum is undefined.
208
212
  */
209
- public async applyStashedOpsAt(seqNum: number) {
213
+ public async applyStashedOpsAt(seqNum?: number) {
210
214
  // apply stashed ops at sequence number
211
215
  while (!this.initialStates.isEmpty()) {
212
216
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
213
217
  const nextState = this.initialStates.peekFront()!;
214
218
  if (nextState.type === "message") {
215
- if (nextState.referenceSequenceNumber > seqNum) {
216
- break; // nothing left to do at this sequence number
217
- } else if (nextState.referenceSequenceNumber > 0 && nextState.referenceSequenceNumber < seqNum) {
218
- throw new Error("loaded from snapshot too recent to apply stashed ops");
219
+ if (seqNum !== undefined) {
220
+ if (nextState.referenceSequenceNumber > seqNum) {
221
+ break; // nothing left to do at this sequence number
222
+ } else if (nextState.referenceSequenceNumber < seqNum) {
223
+ throw new Error("loaded from snapshot too recent to apply stashed ops");
224
+ }
219
225
  }
220
226
 
221
227
  // applyStashedOp will cause the DDS to behave as if it has sent the op but not actually send it
222
- const localOpMetadata = await this.applyStashedOp(nextState.messageType, nextState.content);
228
+ const localOpMetadata =
229
+ await this.stateHandler.applyStashedOp(nextState.messageType, nextState.content);
223
230
  nextState.localOpMetadata = localOpMetadata;
224
231
  }
225
232
 
@@ -229,88 +236,12 @@ export class PendingStateManager implements IDisposable {
229
236
  }
230
237
  }
231
238
 
232
- /**
233
- * Processes a local message once it's ack'd by the server to verify that there was no data corruption and that
234
- * the batch information was preserved for batch messages. Also process remote messages that might have been
235
- * sent from a previous container.
236
- * @param message - The message that got ack'd and needs to be processed.
237
- */
238
- public processMessage(message: ISequencedDocumentMessage, local: boolean) {
239
- // Do not process chunked ops until all pieces are available.
240
- if (message.type === ContainerMessageType.ChunkedOp) {
241
- return { localAck: false, localOpMetadata: undefined };
242
- }
243
-
244
- if (local) {
245
- return { localAck: false, localOpMetadata: this.processPendingLocalMessage(message) };
246
- } else {
247
- return this.processRemoteMessage(message);
248
- }
249
- }
250
-
251
- /**
252
- * Listens for ACKs of stashed ops
253
- */
254
- private processRemoteMessage(message: ISequencedDocumentMessage) {
255
- if (!isRuntimeMessage(message)) {
256
- return { localAck: false, localOpMetadata: undefined };
257
- }
258
-
259
- // this message was a pending op that was actually sent successfully
260
- const isOriginalClientId = message.clientId === Array.from(this.previousClientIds)[0] &&
261
- message.clientSequenceNumber >= this.firstStashedCSN;
262
- // this message is a pending or stashed op that was resubmitted
263
- const isNewClientId = Array.from(this.previousClientIds).indexOf(message.clientId) > 0;
264
-
265
- // if this is an ack for a stashed op, dequeue one message.
266
- // we should have seen its ref seq num by now and the DDS should be ready for it to be ACKed
267
- if (isOriginalClientId || isNewClientId) {
268
- assert(this.clientId === undefined, 0x28b /* "multiple clients connected with stashed ops" */);
269
- while (!this.pendingStates.isEmpty()) {
270
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271
- const nextState = this.pendingStates.shift()!;
272
- // if it's not a message just drop it and keep looking
273
- if (nextState.type === "message") {
274
- this.assertOpMatch(nextState, message, isOriginalClientId);
275
- return { localAck: true, localOpMetadata: nextState.localOpMetadata };
276
- }
277
- }
278
- }
279
-
280
- if (message.type === ContainerMessageType.Rejoin && this.previousClientIds.has(message.contents?.clientId)) {
281
- this.previousClientIds.add(message.clientId);
282
- }
283
-
284
- return { localAck: false, localOpMetadata: undefined };
285
- }
286
-
287
- private assertOpMatch(state: IPendingMessage, message: ISequencedDocumentMessage, isOriginalClientId: boolean) {
288
- assert(message.type === state.messageType, 0x28c /* "different message type" */);
289
- assert(message.clientSequenceNumber === state.clientSequenceNumber || !isOriginalClientId,
290
- 0x28d /* "client sequence number doesn't match" */);
291
- switch (message.type) {
292
- case ContainerMessageType.Attach:
293
- assert(message.contents.id === state.content.id, 0x28e /* "datastore ID doesn't match" */);
294
- break;
295
- case ContainerMessageType.FluidDataStoreOp:
296
- assert(message.contents.address === state.content.address, 0x28f /* "address doesn't match" */);
297
- break;
298
- case ContainerMessageType.BlobAttach:
299
- // todo: assert we have blob storage, assert blob IDs match, remove blob from blob storage since it made
300
- // it through successfully
301
- break;
302
- case ContainerMessageType.Rejoin:
303
- default:
304
- throw new Error(`${message.type} not expected`);
305
- }
306
- }
307
-
308
239
  /**
309
240
  * Processes a local message once its ack'd by the server. It verifies that there was no data corruption and that
310
241
  * the batch information was preserved for batch messages.
311
242
  * @param message - The message that got ack'd and needs to be processed.
312
243
  */
313
- private processPendingLocalMessage(message: ISequencedDocumentMessage): unknown {
244
+ public processPendingLocalMessage(message: ISequencedDocumentMessage): unknown {
314
245
  // Pre-processing part - This may be the start of a batch.
315
246
  this.maybeProcessBatchBegin(message);
316
247
 
@@ -330,11 +261,11 @@ export class PendingStateManager implements IDisposable {
330
261
  { expectedClientSequenceNumber: pendingState.clientSequenceNumber },
331
262
  );
332
263
 
333
- this.containerRuntime.closeFn(error);
264
+ this.stateHandler.close(error);
334
265
  return;
335
266
  }
336
267
 
337
- this.pendingMessagesCount--;
268
+ this._pendingMessagesCount--;
338
269
 
339
270
  // Post-processing part - If we are processing a batch then this could be the last message in the batch.
340
271
  this.maybeProcessBatchEnd(message);
@@ -446,6 +377,31 @@ export class PendingStateManager implements IDisposable {
446
377
  this.isProcessingBatch = false;
447
378
  }
448
379
 
380
+ /**
381
+ * Capture the pending state at this point
382
+ */
383
+ public checkpoint() {
384
+ const checkpointHead = this.pendingStates.peekBack();
385
+ return {
386
+ rollback: () => {
387
+ try {
388
+ while (this.pendingStates.peekBack() !== checkpointHead) {
389
+ this.rollbackNextPendingState();
390
+ }
391
+ } catch (err) {
392
+ const error = wrapError(err, (message) => {
393
+ return DataProcessingError.create(
394
+ `RollbackError: ${message}`,
395
+ "checkpointRollback",
396
+ undefined) as DataProcessingError;
397
+ });
398
+ this.stateHandler.close(error);
399
+ throw error;
400
+ }
401
+ },
402
+ };
403
+ }
404
+
449
405
  /**
450
406
  * Returns the next pending state from the pending state queue.
451
407
  */
@@ -455,17 +411,42 @@ export class PendingStateManager implements IDisposable {
455
411
  return nextPendingState;
456
412
  }
457
413
 
414
+ /**
415
+ * Undo the last pending state
416
+ */
417
+ private rollbackNextPendingState() {
418
+ const pendingStatesCount = this.pendingStates.length;
419
+ if (pendingStatesCount === 0) {
420
+ return;
421
+ }
422
+
423
+ this._pendingMessagesCount--;
424
+
425
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
426
+ const pendingState = this.pendingStates.pop()!;
427
+ switch (pendingState.type) {
428
+ case "message":
429
+ this.stateHandler.rollback(
430
+ pendingState.messageType,
431
+ pendingState.content,
432
+ pendingState.localOpMetadata);
433
+ break;
434
+ default:
435
+ throw new Error(`Can't rollback state ${pendingState.type}`);
436
+ }
437
+ }
438
+
458
439
  /**
459
440
  * Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
460
441
  * states in its queue. This includes setting the FlushMode and triggering resubmission of unacked ops.
461
442
  */
462
443
  public replayPendingStates() {
463
- assert(this.connected, 0x172 /* "The connection state is not consistent with the runtime" */);
444
+ assert(this.stateHandler.connected(), 0x172 /* "The connection state is not consistent with the runtime" */);
464
445
 
465
446
  // This assert suggests we are about to send same ops twice, which will result in data loss.
466
- assert(this.clientId !== this.containerRuntime.clientId,
447
+ assert(this.clientId !== this.stateHandler.clientId(),
467
448
  0x173 /* "replayPendingStates called twice for same clientId!" */);
468
- this.clientId = this.containerRuntime.clientId;
449
+ this.clientId = this.stateHandler.clientId();
469
450
 
470
451
  assert(this.initialStates.isEmpty(), 0x174 /* "initial states should be empty before replaying pending" */);
471
452
 
@@ -475,14 +456,14 @@ export class PendingStateManager implements IDisposable {
475
456
  }
476
457
 
477
458
  // Reset the pending message count because all these messages will be removed from the queue.
478
- this.pendingMessagesCount = 0;
459
+ this._pendingMessagesCount = 0;
479
460
 
480
461
  // Save the current FlushMode so that we can revert it back after replaying the states.
481
- const savedFlushMode = this.containerRuntime.flushMode;
462
+ const savedFlushMode = this.stateHandler.flushMode();
482
463
 
483
464
  // Set the flush mode for the next message. This step is important because the flush mode may have been changed
484
465
  // after the next pending message was sent.
485
- this.containerRuntime.setFlushMode(this.flushModeForNextMessage);
466
+ this.stateHandler.setFlushMode(this.flushModeForNextMessage);
486
467
 
487
468
  // Process exactly `pendingStatesCount` items in the queue as it represents the number of states that were
488
469
  // pending when we connected. This is important because the `reSubmitFn` might add more items in the queue
@@ -492,17 +473,17 @@ export class PendingStateManager implements IDisposable {
492
473
  const pendingState = this.pendingStates.shift()!;
493
474
  switch (pendingState.type) {
494
475
  case "message":
495
- this.containerRuntime.reSubmitFn(
476
+ this.stateHandler.reSubmit(
496
477
  pendingState.messageType,
497
478
  pendingState.content,
498
479
  pendingState.localOpMetadata,
499
480
  pendingState.opMetadata);
500
481
  break;
501
482
  case "flushMode":
502
- this.containerRuntime.setFlushMode(pendingState.flushMode);
483
+ this.stateHandler.setFlushMode(pendingState.flushMode);
503
484
  break;
504
485
  case "flush":
505
- this.containerRuntime.flush();
486
+ this.stateHandler.flush();
506
487
  break;
507
488
  default:
508
489
  break;
@@ -511,6 +492,6 @@ export class PendingStateManager implements IDisposable {
511
492
  }
512
493
 
513
494
  // Revert the FlushMode.
514
- this.containerRuntime.setFlushMode(savedFlushMode);
495
+ this.stateHandler.setFlushMode(savedFlushMode);
515
496
  }
516
497
  }