@fluidframework/container-loader 2.0.0-dev.3.1.0.125672 → 2.0.0-dev.4.2.0.153917

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 (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +45 -4
  3. package/closeAndGetPendingLocalState.md +51 -0
  4. package/dist/connectionManager.d.ts +2 -1
  5. package/dist/connectionManager.d.ts.map +1 -1
  6. package/dist/connectionManager.js +59 -17
  7. package/dist/connectionManager.js.map +1 -1
  8. package/dist/connectionStateHandler.d.ts +4 -4
  9. package/dist/connectionStateHandler.d.ts.map +1 -1
  10. package/dist/connectionStateHandler.js +7 -0
  11. package/dist/connectionStateHandler.js.map +1 -1
  12. package/dist/container.d.ts +44 -4
  13. package/dist/container.d.ts.map +1 -1
  14. package/dist/container.js +160 -105
  15. package/dist/container.js.map +1 -1
  16. package/dist/containerContext.d.ts +18 -8
  17. package/dist/containerContext.d.ts.map +1 -1
  18. package/dist/containerContext.js +47 -4
  19. package/dist/containerContext.js.map +1 -1
  20. package/dist/containerStorageAdapter.d.ts +41 -2
  21. package/dist/containerStorageAdapter.d.ts.map +1 -1
  22. package/dist/containerStorageAdapter.js +87 -11
  23. package/dist/containerStorageAdapter.js.map +1 -1
  24. package/dist/contracts.d.ts +2 -2
  25. package/dist/contracts.d.ts.map +1 -1
  26. package/dist/contracts.js.map +1 -1
  27. package/dist/deltaManager.d.ts +4 -5
  28. package/dist/deltaManager.d.ts.map +1 -1
  29. package/dist/deltaManager.js +7 -10
  30. package/dist/deltaManager.js.map +1 -1
  31. package/dist/deltaManagerProxy.d.ts +10 -22
  32. package/dist/deltaManagerProxy.d.ts.map +1 -1
  33. package/dist/deltaManagerProxy.js +14 -50
  34. package/dist/deltaManagerProxy.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/loader.d.ts +10 -1
  40. package/dist/loader.d.ts.map +1 -1
  41. package/dist/loader.js +25 -16
  42. package/dist/loader.js.map +1 -1
  43. package/dist/packageVersion.d.ts +1 -1
  44. package/dist/packageVersion.js +1 -1
  45. package/dist/packageVersion.js.map +1 -1
  46. package/dist/protocol.d.ts +1 -0
  47. package/dist/protocol.d.ts.map +1 -1
  48. package/dist/protocol.js +4 -2
  49. package/dist/protocol.js.map +1 -1
  50. package/dist/protocolTreeDocumentStorageService.d.ts +6 -2
  51. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  52. package/dist/protocolTreeDocumentStorageService.js +7 -4
  53. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  54. package/dist/utils.d.ts.map +1 -1
  55. package/dist/utils.js +2 -1
  56. package/dist/utils.js.map +1 -1
  57. package/lib/connectionManager.d.ts +2 -1
  58. package/lib/connectionManager.d.ts.map +1 -1
  59. package/lib/connectionManager.js +60 -18
  60. package/lib/connectionManager.js.map +1 -1
  61. package/lib/connectionStateHandler.d.ts +4 -4
  62. package/lib/connectionStateHandler.d.ts.map +1 -1
  63. package/lib/connectionStateHandler.js +7 -0
  64. package/lib/connectionStateHandler.js.map +1 -1
  65. package/lib/container.d.ts +44 -4
  66. package/lib/container.d.ts.map +1 -1
  67. package/lib/container.js +164 -109
  68. package/lib/container.js.map +1 -1
  69. package/lib/containerContext.d.ts +18 -8
  70. package/lib/containerContext.d.ts.map +1 -1
  71. package/lib/containerContext.js +48 -5
  72. package/lib/containerContext.js.map +1 -1
  73. package/lib/containerStorageAdapter.d.ts +41 -2
  74. package/lib/containerStorageAdapter.d.ts.map +1 -1
  75. package/lib/containerStorageAdapter.js +85 -11
  76. package/lib/containerStorageAdapter.js.map +1 -1
  77. package/lib/contracts.d.ts +2 -2
  78. package/lib/contracts.d.ts.map +1 -1
  79. package/lib/contracts.js.map +1 -1
  80. package/lib/deltaManager.d.ts +4 -5
  81. package/lib/deltaManager.d.ts.map +1 -1
  82. package/lib/deltaManager.js +7 -10
  83. package/lib/deltaManager.js.map +1 -1
  84. package/lib/deltaManagerProxy.d.ts +10 -22
  85. package/lib/deltaManagerProxy.d.ts.map +1 -1
  86. package/lib/deltaManagerProxy.js +14 -50
  87. package/lib/deltaManagerProxy.js.map +1 -1
  88. package/lib/index.d.ts +3 -2
  89. package/lib/index.d.ts.map +1 -1
  90. package/lib/index.js +2 -2
  91. package/lib/index.js.map +1 -1
  92. package/lib/loader.d.ts +10 -1
  93. package/lib/loader.d.ts.map +1 -1
  94. package/lib/loader.js +24 -16
  95. package/lib/loader.js.map +1 -1
  96. package/lib/packageVersion.d.ts +1 -1
  97. package/lib/packageVersion.js +1 -1
  98. package/lib/packageVersion.js.map +1 -1
  99. package/lib/protocol.d.ts +1 -0
  100. package/lib/protocol.d.ts.map +1 -1
  101. package/lib/protocol.js +3 -1
  102. package/lib/protocol.js.map +1 -1
  103. package/lib/protocolTreeDocumentStorageService.d.ts +6 -2
  104. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  105. package/lib/protocolTreeDocumentStorageService.js +7 -4
  106. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  107. package/lib/utils.d.ts.map +1 -1
  108. package/lib/utils.js +2 -1
  109. package/lib/utils.js.map +1 -1
  110. package/package.json +64 -56
  111. package/src/connectionManager.ts +65 -24
  112. package/src/connectionStateHandler.ts +17 -5
  113. package/src/container.ts +239 -137
  114. package/src/containerContext.ts +74 -11
  115. package/src/containerStorageAdapter.ts +113 -9
  116. package/src/contracts.ts +2 -2
  117. package/src/deltaManager.ts +12 -14
  118. package/src/deltaManagerProxy.ts +18 -73
  119. package/src/index.ts +3 -3
  120. package/src/loader.ts +31 -26
  121. package/src/packageVersion.ts +1 -1
  122. package/src/protocol.ts +4 -1
  123. package/src/protocolTreeDocumentStorageService.ts +6 -3
  124. package/src/utils.ts +7 -4
package/src/container.ts CHANGED
@@ -5,6 +5,9 @@
5
5
 
6
6
  // eslint-disable-next-line import/no-internal-modules
7
7
  import merge from "lodash/merge";
8
+ // eslint-disable-next-line import/no-internal-modules
9
+ import cloneDeep from "lodash/cloneDeep";
10
+
8
11
  import { v4 as uuid } from "uuid";
9
12
  import {
10
13
  ITelemetryLogger,
@@ -12,10 +15,10 @@ import {
12
15
  TelemetryEventCategory,
13
16
  } from "@fluidframework/common-definitions";
14
17
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
15
- import { IRequest, IResponse, IFluidRouter } from "@fluidframework/core-interfaces";
18
+ import { IRequest, IResponse, IFluidRouter, FluidObject } from "@fluidframework/core-interfaces";
16
19
  import {
17
20
  IAudience,
18
- IConnectionDetails,
21
+ IConnectionDetailsInternal,
19
22
  IContainer,
20
23
  IContainerEvents,
21
24
  IDeltaManager,
@@ -44,6 +47,7 @@ import {
44
47
  combineAppAndProtocolSummary,
45
48
  runWithRetry,
46
49
  isFluidResolvedUrl,
50
+ isCombinedAppAndProtocolSummary,
47
51
  } from "@fluidframework/driver-utils";
48
52
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
49
53
  import {
@@ -53,7 +57,6 @@ import {
53
57
  ICommittedProposal,
54
58
  IDocumentAttributes,
55
59
  IDocumentMessage,
56
- IProtocolState,
57
60
  IQuorumClients,
58
61
  IQuorumProposals,
59
62
  ISequencedClient,
@@ -74,7 +77,6 @@ import {
74
77
  raiseConnectedEvent,
75
78
  TelemetryLogger,
76
79
  connectedEventName,
77
- disconnectedEventName,
78
80
  normalizeError,
79
81
  MonitoringContext,
80
82
  loggerToMonitoringContext,
@@ -87,7 +89,12 @@ import { DeltaManager, IConnectionArgs } from "./deltaManager";
87
89
  import { DeltaManagerProxy } from "./deltaManagerProxy";
88
90
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
89
91
  import { pkgVersion } from "./packageVersion";
90
- import { ContainerStorageAdapter } from "./containerStorageAdapter";
92
+ import {
93
+ ContainerStorageAdapter,
94
+ getBlobContentsFromTree,
95
+ getBlobContentsFromTreeWithBlobContents,
96
+ ISerializableBlobContents,
97
+ } from "./containerStorageAdapter";
91
98
  import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
92
99
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
93
100
  import {
@@ -98,13 +105,22 @@ import {
98
105
  import { CollabWindowTracker } from "./collabWindowTracker";
99
106
  import { ConnectionManager } from "./connectionManager";
100
107
  import { ConnectionState } from "./connectionState";
101
- import { IProtocolHandler, ProtocolHandler, ProtocolHandlerBuilder } from "./protocol";
108
+ import {
109
+ OnlyValidTermValue,
110
+ IProtocolHandler,
111
+ ProtocolHandler,
112
+ ProtocolHandlerBuilder,
113
+ } from "./protocol";
102
114
 
103
115
  const detachedContainerRefSeqNumber = 0;
104
116
 
105
117
  const dirtyContainerEvent = "dirty";
106
118
  const savedContainerEvent = "saved";
107
119
 
120
+ /**
121
+ * @deprecated this is an internal interface and will not longer be exported in future versions
122
+ * @internal
123
+ */
108
124
  export interface IContainerLoadOptions {
109
125
  /**
110
126
  * Disables the Container from reconnecting if false, allows reconnect otherwise.
@@ -125,6 +141,10 @@ export interface IContainerLoadOptions {
125
141
  loadMode?: IContainerLoadMode;
126
142
  }
127
143
 
144
+ /**
145
+ * @deprecated this is an internal interface and will not longer be exported in future versions
146
+ * @internal
147
+ */
128
148
  export interface IContainerConfig {
129
149
  resolvedUrl?: IFluidResolvedUrl;
130
150
  canReconnect?: boolean;
@@ -255,17 +275,36 @@ export async function ReportIfTooLong(
255
275
  /**
256
276
  * State saved by a container at close time, to be used to load a new instance
257
277
  * of the container to the same state
278
+ * @deprecated this is an internal interface and will not longer be exported in future versions
279
+ * @internal
258
280
  */
259
281
  export interface IPendingContainerState {
260
282
  pendingRuntimeState: unknown;
283
+ /**
284
+ * Snapshot from which container initially loaded.
285
+ */
286
+ baseSnapshot: ISnapshotTree;
287
+ /**
288
+ * Serializable blobs from the base snapshot. Used to load offline since
289
+ * storage is not available.
290
+ */
291
+ snapshotBlobs: ISerializableBlobContents;
292
+ /**
293
+ * All ops since base snapshot sequence number up to the latest op
294
+ * seen when the container was closed. Used to apply stashed (saved pending)
295
+ * ops at the same sequence number at which they were made.
296
+ */
297
+ savedOps: ISequencedDocumentMessage[];
261
298
  url: string;
262
- protocol: IProtocolState;
263
299
  term: number;
264
300
  clientId?: string;
265
301
  }
266
302
 
267
303
  const summarizerClientType = "summarizer";
268
304
 
305
+ /**
306
+ * @deprecated - In the next release Container will no longer be exported, IContainer should be used in its place.
307
+ */
269
308
  export class Container
270
309
  extends EventEmitterWithErrorHandling<IContainerEvents>
271
310
  implements IContainer
@@ -274,6 +313,7 @@ export class Container
274
313
 
275
314
  /**
276
315
  * Load an existing container.
316
+ * @internal
277
317
  */
278
318
  public static async load(
279
319
  loader: Loader,
@@ -434,9 +474,9 @@ export class Container
434
474
 
435
475
  private _attachState = AttachState.Detached;
436
476
 
437
- private readonly storageService: ContainerStorageAdapter;
477
+ private readonly storageAdapter: ContainerStorageAdapter;
438
478
  public get storage(): IDocumentStorageService {
439
- return this.storageService;
479
+ return this.storageAdapter;
440
480
  }
441
481
 
442
482
  private readonly clientDetailsOverride: IClientDetails | undefined;
@@ -467,6 +507,9 @@ export class Container
467
507
  private _resolvedUrl: IFluidResolvedUrl | undefined;
468
508
  private attachStarted = false;
469
509
  private _dirtyContainer = false;
510
+ private readonly savedOps: ISequencedDocumentMessage[] = [];
511
+ private baseSnapshot?: ISnapshotTree;
512
+ private baseSnapshotBlobs?: ISerializableBlobContents;
470
513
 
471
514
  private lastVisible: number | undefined;
472
515
  private readonly visibilityEventHandler: (() => void) | undefined;
@@ -549,6 +592,14 @@ export class Container
549
592
  return this._deltaManager.clientDetails;
550
593
  }
551
594
 
595
+ private get offlineLoadEnabled(): boolean {
596
+ // summarizer will not have any pending state we want to save
597
+ return (
598
+ (this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ?? false) &&
599
+ this.clientDetails.capabilities.interactive
600
+ );
601
+ }
602
+
552
603
  /**
553
604
  * Get the code details that are currently specified for the container.
554
605
  * @returns The current code details if any are specified, undefined if none are specified.
@@ -596,6 +647,44 @@ export class Container
596
647
  return this.loader.services.codeLoader;
597
648
  }
598
649
 
650
+ /**
651
+ * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
652
+ */
653
+ public async getEntryPoint?(): Promise<FluidObject | undefined> {
654
+ // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
655
+ // allow it since they mean a kind of read-only state for the Container.
656
+ // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
657
+ if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
658
+ throw new UsageError("The container is disposing or disposed");
659
+ }
660
+ while (this._context === undefined) {
661
+ await new Promise<void>((resolve, reject) => {
662
+ const contextChangedHandler = () => {
663
+ resolve();
664
+ this.off("disposed", disposedHandler);
665
+ };
666
+ const disposedHandler = (error) => {
667
+ reject(error ?? "The Container is disposed");
668
+ this.off("contextChanged", contextChangedHandler);
669
+ };
670
+ this.once("contextChanged", contextChangedHandler);
671
+ this.once("disposed", disposedHandler);
672
+ });
673
+ // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
674
+ // should have set this._context; making sure.
675
+ assert(
676
+ this._context !== undefined,
677
+ 0x5a2 /* Context still not defined after contextChanged event */,
678
+ );
679
+ }
680
+ // Disable lint rule for the sake of more complete stack traces
681
+ // eslint-disable-next-line no-return-await
682
+ return await this._context.getEntryPoint?.();
683
+ }
684
+
685
+ /**
686
+ * @internal
687
+ */
599
688
  constructor(
600
689
  private readonly loader: Loader,
601
690
  config: IContainerConfig,
@@ -650,6 +739,7 @@ export class Container
650
739
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
651
740
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
652
741
  dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
742
+ dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
653
743
  connectionStateDuration: () =>
654
744
  performance.now() - this.connectionTransitionTimes[this.connectionState],
655
745
  },
@@ -658,18 +748,10 @@ export class Container
658
748
  // Prefix all events in this file with container-loader
659
749
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
660
750
 
661
- const summarizeProtocolTree =
662
- this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
663
- this.loader.services.options.summarizeProtocolTree;
664
-
665
- this.options = {
666
- ...this.loader.services.options,
667
- summarizeProtocolTree,
668
- };
751
+ this.options = cloneDeep(this.loader.services.options);
669
752
 
670
753
  this._deltaManager = this.createDeltaManager();
671
754
 
672
- this._clientId = config.serializedContainerState?.clientId;
673
755
  this.connectionStateHandler = createConnectionStateHandler(
674
756
  {
675
757
  logger: this.mc.logger,
@@ -725,19 +807,33 @@ export class Container
725
807
  },
726
808
  },
727
809
  this.deltaManager,
728
- this._clientId,
810
+ config.serializedContainerState?.clientId,
729
811
  );
730
812
 
731
813
  this.on(savedContainerEvent, () => {
732
814
  this.connectionStateHandler.containerSaved();
733
815
  });
734
816
 
735
- this.storageService = new ContainerStorageAdapter(
817
+ // We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
818
+ // non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
819
+ // using this callback and fix them up.
820
+ const addProtocolSummaryIfMissing = (summaryTree: ISummaryTree) =>
821
+ isCombinedAppAndProtocolSummary(summaryTree) === true
822
+ ? summaryTree
823
+ : combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
824
+
825
+ // Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
826
+ // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
827
+ const forceEnableSummarizeProtocolTree =
828
+ this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
829
+ this.loader.services.options.summarizeProtocolTree;
830
+
831
+ this.storageAdapter = new ContainerStorageAdapter(
736
832
  this.loader.services.detachedBlobStorage,
737
833
  this.mc.logger,
738
- this.options.summarizeProtocolTree === true
739
- ? () => this.captureProtocolSummary()
740
- : undefined,
834
+ config.serializedContainerState?.snapshotBlobs,
835
+ addProtocolSummaryIfMissing,
836
+ forceEnableSummarizeProtocolTree,
741
837
  );
742
838
 
743
839
  const isDomAvailable =
@@ -760,43 +856,6 @@ export class Container
760
856
  };
761
857
  document.addEventListener("visibilitychange", this.visibilityEventHandler);
762
858
  }
763
-
764
- // We observed that most users of platform do not check Container.connected event on load, causing bugs.
765
- // As such, we are raising events when new listener pops up.
766
- // Note that we can raise both "disconnected" & "connect" events at the same time,
767
- // if we are in connecting stage.
768
- this.on("newListener", (event: string, listener: (...args: any[]) => void) => {
769
- // Fire events on the end of JS turn, giving a chance for caller to be in consistent state.
770
- Promise.resolve()
771
- .then(() => {
772
- switch (event) {
773
- case dirtyContainerEvent:
774
- if (this._dirtyContainer) {
775
- listener();
776
- }
777
- break;
778
- case savedContainerEvent:
779
- if (!this._dirtyContainer) {
780
- listener();
781
- }
782
- break;
783
- case connectedEventName:
784
- if (this.connected) {
785
- listener(this.clientId);
786
- }
787
- break;
788
- case disconnectedEventName:
789
- if (!this.connected) {
790
- listener();
791
- }
792
- break;
793
- default:
794
- }
795
- })
796
- .catch((error) => {
797
- this.mc.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
798
- });
799
- });
800
859
  }
801
860
 
802
861
  /**
@@ -840,10 +899,15 @@ export class Container
840
899
  try {
841
900
  // Raise event first, to ensure we capture _lifecycleState before transition.
842
901
  // This gives us a chance to know what errors happened on open vs. on fully loaded container.
902
+ // Log generic events instead of error events if container is in loading state, as most errors are not really FF errors
903
+ // which can pollute telemetry for real bugs
843
904
  this.mc.logger.sendTelemetryEvent(
844
905
  {
845
906
  eventName: "ContainerClose",
846
- category: error === undefined ? "generic" : "error",
907
+ category:
908
+ this._lifecycleState !== "loading" && error !== undefined
909
+ ? "error"
910
+ : "generic",
847
911
  },
848
912
  error,
849
913
  );
@@ -856,7 +920,7 @@ export class Container
856
920
 
857
921
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
858
922
 
859
- this.storageService.dispose();
923
+ this.storageAdapter.dispose();
860
924
 
861
925
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
862
926
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -889,7 +953,7 @@ export class Container
889
953
  this.mc.logger.sendTelemetryEvent(
890
954
  {
891
955
  eventName: "ContainerDispose",
892
- category: error === undefined ? "generic" : "error",
956
+ category: "generic",
893
957
  },
894
958
  error,
895
959
  );
@@ -905,7 +969,7 @@ export class Container
905
969
 
906
970
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
907
971
 
908
- this.storageService.dispose();
972
+ this.storageAdapter.dispose();
909
973
 
910
974
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
911
975
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -933,6 +997,9 @@ export class Container
933
997
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
934
998
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
935
999
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
1000
+ if (!this.offlineLoadEnabled) {
1001
+ throw new UsageError("Can't get pending local state unless offline load is enabled");
1002
+ }
936
1003
  assert(
937
1004
  this.attachState === AttachState.Attached,
938
1005
  0x0d1 /* "Container should be attached before close" */,
@@ -942,15 +1009,15 @@ export class Container
942
1009
  0x0d2 /* "resolved url should be valid Fluid url" */,
943
1010
  );
944
1011
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
945
- assert(
946
- this._protocolHandler.attributes.term !== undefined,
947
- 0x37e /* Must have a valid protocol handler instance */,
948
- );
1012
+ assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1013
+ assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
949
1014
  const pendingState: IPendingContainerState = {
950
1015
  pendingRuntimeState: this.context.getPendingLocalState(),
1016
+ baseSnapshot: this.baseSnapshot,
1017
+ snapshotBlobs: this.baseSnapshotBlobs,
1018
+ savedOps: this.savedOps,
951
1019
  url: this.resolvedUrl.url,
952
- protocol: this.protocolHandler.getProtocolState(),
953
- term: this._protocolHandler.attributes.term,
1020
+ term: OnlyValidTermValue,
954
1021
  clientId: this.clientId,
955
1022
  };
956
1023
 
@@ -1031,9 +1098,13 @@ export class Container
1031
1098
  // starting to attach the container to storage.
1032
1099
  // Also, this should only be fired in detached container.
1033
1100
  this._attachState = AttachState.Attaching;
1034
- this.context.notifyAttaching(
1035
- getSnapshotTreeFromSerializedContainer(summary),
1036
- );
1101
+ this.emit("attaching");
1102
+ if (this.offlineLoadEnabled) {
1103
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1104
+ this.baseSnapshot = snapshot;
1105
+ this.baseSnapshotBlobs =
1106
+ getBlobContentsFromTreeWithBlobContents(snapshot);
1107
+ }
1037
1108
  }
1038
1109
 
1039
1110
  // Actually go and create the resolved document
@@ -1062,7 +1133,7 @@ export class Container
1062
1133
  const resolvedUrl = this.service.resolvedUrl;
1063
1134
  ensureFluidResolvedUrl(resolvedUrl);
1064
1135
  this._resolvedUrl = resolvedUrl;
1065
- await this.storageService.connectToService(this.service);
1136
+ await this.storageAdapter.connectToService(this.service);
1066
1137
 
1067
1138
  if (hasAttachmentBlobs) {
1068
1139
  // upload blobs to storage
@@ -1082,7 +1153,7 @@ export class Container
1082
1153
  for (const id of newIds) {
1083
1154
  const blob =
1084
1155
  await this.loader.services.detachedBlobStorage.readBlob(id);
1085
- const response = await this.storageService.createBlob(blob);
1156
+ const response = await this.storageAdapter.createBlob(blob);
1086
1157
  redirectTable.set(id, response.id);
1087
1158
  }
1088
1159
  }
@@ -1093,11 +1164,15 @@ export class Container
1093
1164
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1094
1165
 
1095
1166
  this._attachState = AttachState.Attaching;
1096
- this.context.notifyAttaching(
1097
- getSnapshotTreeFromSerializedContainer(summary),
1098
- );
1167
+ this.emit("attaching");
1168
+ if (this.offlineLoadEnabled) {
1169
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1170
+ this.baseSnapshot = snapshot;
1171
+ this.baseSnapshotBlobs =
1172
+ getBlobContentsFromTreeWithBlobContents(snapshot);
1173
+ }
1099
1174
 
1100
- await this.storageService.uploadSummaryWithContext(summary, {
1175
+ await this.storageAdapter.uploadSummaryWithContext(summary, {
1101
1176
  referenceSequenceNumber: 0,
1102
1177
  ackHandle: undefined,
1103
1178
  proposalHandle: undefined,
@@ -1272,7 +1347,7 @@ export class Container
1272
1347
  }
1273
1348
 
1274
1349
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
1275
- const versions = await this.storageService.getVersions(version, 1);
1350
+ const versions = await this.storageAdapter.getVersions(version, 1);
1276
1351
  return versions[0];
1277
1352
  }
1278
1353
 
@@ -1329,15 +1404,15 @@ export class Container
1329
1404
 
1330
1405
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1331
1406
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1332
- if (loadMode.deltaConnection === undefined) {
1407
+ if (loadMode.deltaConnection === undefined && !pendingLocalState) {
1333
1408
  this.connectToDeltaStream(connectionArgs);
1334
1409
  }
1335
1410
 
1336
1411
  if (!pendingLocalState) {
1337
- await this.storageService.connectToService(this.service);
1412
+ await this.storageAdapter.connectToService(this.service);
1338
1413
  } else {
1339
1414
  // if we have pendingLocalState we can load without storage; don't wait for connection
1340
- this.storageService.connectToService(this.service).catch((error) => {
1415
+ this.storageAdapter.connectToService(this.service).catch((error) => {
1341
1416
  this.close(error);
1342
1417
  this.dispose?.(error);
1343
1418
  });
@@ -1349,20 +1424,30 @@ export class Container
1349
1424
  const { snapshot, versionId } =
1350
1425
  pendingLocalState === undefined
1351
1426
  ? await this.fetchSnapshotTree(specifiedVersion)
1352
- : { snapshot: undefined, versionId: undefined };
1353
- assert(
1354
- snapshot !== undefined || pendingLocalState !== undefined,
1355
- 0x237 /* "Snapshot should exist" */,
1427
+ : { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
1428
+
1429
+ if (pendingLocalState) {
1430
+ this.baseSnapshot = pendingLocalState.baseSnapshot;
1431
+ this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
1432
+ } else {
1433
+ assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
1434
+ if (this.offlineLoadEnabled) {
1435
+ this.baseSnapshot = snapshot;
1436
+ // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1437
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1438
+ }
1439
+ }
1440
+
1441
+ const attributes: IDocumentAttributes = await this.getDocumentAttributes(
1442
+ this.storageAdapter,
1443
+ snapshot,
1356
1444
  );
1357
1445
 
1358
- const attributes: IDocumentAttributes =
1359
- pendingLocalState === undefined
1360
- ? await this.getDocumentAttributes(this.storageService, snapshot)
1361
- : {
1362
- sequenceNumber: pendingLocalState.protocol.sequenceNumber,
1363
- minimumSequenceNumber: pendingLocalState.protocol.minimumSequenceNumber,
1364
- term: pendingLocalState.term,
1365
- };
1446
+ // If we saved ops, we will replay them and don't need DeltaManager to fetch them
1447
+ const sequenceNumber =
1448
+ pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber;
1449
+ const dmAttributes =
1450
+ sequenceNumber !== undefined ? { ...attributes, sequenceNumber } : attributes;
1366
1451
 
1367
1452
  let opsBeforeReturnP: Promise<void> | undefined;
1368
1453
 
@@ -1373,15 +1458,15 @@ export class Container
1373
1458
  // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
1374
1459
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1375
1460
  this.attachDeltaManagerOpHandler(
1376
- attributes,
1461
+ dmAttributes,
1377
1462
  loadMode.deltaConnection !== "none" ? "all" : "none",
1378
1463
  );
1379
1464
  break;
1380
1465
  case "cached":
1381
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "cached");
1466
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
1382
1467
  break;
1383
1468
  case "all":
1384
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "all");
1469
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
1385
1470
  break;
1386
1471
  default:
1387
1472
  unreachableCase(loadMode.opsBeforeReturn);
@@ -1389,22 +1474,7 @@ export class Container
1389
1474
 
1390
1475
  // ...load in the existing quorum
1391
1476
  // Initialize the protocol handler
1392
- if (pendingLocalState === undefined) {
1393
- await this.initializeProtocolStateFromSnapshot(
1394
- attributes,
1395
- this.storageService,
1396
- snapshot,
1397
- );
1398
- } else {
1399
- this.initializeProtocolState(
1400
- attributes,
1401
- {
1402
- members: pendingLocalState.protocol.members,
1403
- proposals: pendingLocalState.protocol.proposals,
1404
- values: pendingLocalState.protocol.values,
1405
- }, // pending IQuorumSnapshot
1406
- );
1407
- }
1477
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
1408
1478
 
1409
1479
  const codeDetails = this.getCodeDetailsFromQuorum();
1410
1480
  await this.instantiateContext(
@@ -1414,6 +1484,24 @@ export class Container
1414
1484
  pendingLocalState?.pendingRuntimeState,
1415
1485
  );
1416
1486
 
1487
+ // replay saved ops
1488
+ if (pendingLocalState) {
1489
+ for (const message of pendingLocalState.savedOps) {
1490
+ this.processRemoteMessage(message);
1491
+
1492
+ // allow runtime to apply stashed ops at this op's sequence number
1493
+ await this.context.notifyOpReplay(message);
1494
+ }
1495
+ pendingLocalState.savedOps = [];
1496
+
1497
+ // now set clientId to stashed clientId so live ops are correctly processed as local
1498
+ assert(
1499
+ this.clientId === undefined,
1500
+ 0x5d6 /* Unexpected clientId when setting stashed clientId */,
1501
+ );
1502
+ this._clientId = pendingLocalState?.clientId;
1503
+ }
1504
+
1417
1505
  // We might have hit some failure that did not manifest itself in exception in this flow,
1418
1506
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
1419
1507
  if (!this.closed) {
@@ -1437,6 +1525,11 @@ export class Container
1437
1525
 
1438
1526
  switch (loadMode.deltaConnection) {
1439
1527
  case undefined:
1528
+ if (pendingLocalState) {
1529
+ // connect to delta stream now since we did not before
1530
+ this.connectToDeltaStream(connectionArgs);
1531
+ }
1532
+ // intentional fallthrough
1440
1533
  case "delayed":
1441
1534
  assert(
1442
1535
  this.inboundQueuePausedFromInit,
@@ -1476,7 +1569,7 @@ export class Container
1476
1569
  private async createDetached(source: IFluidCodeDetails) {
1477
1570
  const attributes: IDocumentAttributes = {
1478
1571
  sequenceNumber: detachedContainerRefSeqNumber,
1479
- term: 1,
1572
+ term: OnlyValidTermValue,
1480
1573
  minimumSequenceNumber: 0,
1481
1574
  };
1482
1575
 
@@ -1512,15 +1605,15 @@ export class Container
1512
1605
  }
1513
1606
 
1514
1607
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1515
- this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1516
- const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1608
+ this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1609
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
1517
1610
 
1518
1611
  await this.attachDeltaManagerOpHandler(attributes);
1519
1612
 
1520
1613
  // Initialize the protocol handler
1521
1614
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1522
1615
  const qValues = await readAndParse<[string, ICommittedProposal][]>(
1523
- this.storageService,
1616
+ this.storageAdapter,
1524
1617
  baseTree.blobs.quorumValues,
1525
1618
  );
1526
1619
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
@@ -1550,7 +1643,7 @@ export class Container
1550
1643
  return {
1551
1644
  minimumSequenceNumber: 0,
1552
1645
  sequenceNumber: 0,
1553
- term: 1,
1646
+ term: OnlyValidTermValue,
1554
1647
  };
1555
1648
  }
1556
1649
 
@@ -1562,11 +1655,6 @@ export class Container
1562
1655
 
1563
1656
  const attributes = await readAndParse<IDocumentAttributes>(storage, attributesHash);
1564
1657
 
1565
- // Backward compatibility for older summaries with no term
1566
- if (attributes.term === undefined) {
1567
- attributes.term = 1;
1568
- }
1569
-
1570
1658
  return attributes;
1571
1659
  }
1572
1660
 
@@ -1731,6 +1819,7 @@ export class Container
1731
1819
  (props: IConnectionManagerFactoryArgs) =>
1732
1820
  new ConnectionManager(
1733
1821
  serviceProvider,
1822
+ () => this.isDirty,
1734
1823
  this.client,
1735
1824
  this._canReconnect,
1736
1825
  ChildLogger.create(this.subLogger, "ConnectionManager"),
@@ -1744,7 +1833,7 @@ export class Container
1744
1833
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1745
1834
  deltaManager.inboundSignal.pause();
1746
1835
 
1747
- deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
1836
+ deltaManager.on("connect", (details: IConnectionDetailsInternal, _opsBehind?: number) => {
1748
1837
  assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1749
1838
  this.connectionStateHandler.receivedConnectEvent(details);
1750
1839
  });
@@ -1792,7 +1881,6 @@ export class Container
1792
1881
  return this._deltaManager.attachOpHandler(
1793
1882
  attributes.minimumSequenceNumber,
1794
1883
  attributes.sequenceNumber,
1795
- attributes.term ?? 1,
1796
1884
  {
1797
1885
  process: (message) => this.processRemoteMessage(message),
1798
1886
  processSignal: (message) => {
@@ -1882,10 +1970,7 @@ export class Container
1882
1970
 
1883
1971
  // Both protocol and context should not be undefined if we got so far.
1884
1972
 
1885
- this.setContextConnectedState(
1886
- state,
1887
- this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false,
1888
- );
1973
+ this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
1889
1974
  this.protocolHandler.setConnectionState(state, this.clientId);
1890
1975
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1891
1976
 
@@ -1923,7 +2008,7 @@ export class Container
1923
2008
  }
1924
2009
 
1925
2010
  /** @returns clientSequenceNumber of last message in a batch */
1926
- private submitBatch(batch: IBatchMessage[]): number {
2011
+ private submitBatch(batch: IBatchMessage[], referenceSequenceNumber?: number): number {
1927
2012
  let clientSequenceNumber = -1;
1928
2013
  for (const message of batch) {
1929
2014
  clientSequenceNumber = this.submitMessage(
@@ -1932,13 +2017,14 @@ export class Container
1932
2017
  true, // batch
1933
2018
  message.metadata,
1934
2019
  message.compression,
2020
+ referenceSequenceNumber,
1935
2021
  );
1936
2022
  }
1937
2023
  this._deltaManager.flush();
1938
2024
  return clientSequenceNumber;
1939
2025
  }
1940
2026
 
1941
- private submitSummaryMessage(summary: ISummaryContent) {
2027
+ private submitSummaryMessage(summary: ISummaryContent, referenceSequenceNumber?: number) {
1942
2028
  // github #6451: this is only needed for staging so the server
1943
2029
  // know when the protocol tree is included
1944
2030
  // this can be removed once all clients send
@@ -1946,11 +2032,14 @@ export class Container
1946
2032
  if (summary.details === undefined) {
1947
2033
  summary.details = {};
1948
2034
  }
1949
- summary.details.includesProtocolTree = this.options.summarizeProtocolTree === true;
2035
+ summary.details.includesProtocolTree = this.storageAdapter.summarizeProtocolTree;
1950
2036
  return this.submitMessage(
1951
2037
  MessageType.Summarize,
1952
2038
  JSON.stringify(summary),
1953
2039
  false /* batch */,
2040
+ undefined /* metadata */,
2041
+ undefined /* compression */,
2042
+ referenceSequenceNumber,
1954
2043
  );
1955
2044
  }
1956
2045
 
@@ -1960,6 +2049,7 @@ export class Container
1960
2049
  batch?: boolean,
1961
2050
  metadata?: any,
1962
2051
  compression?: string,
2052
+ referenceSequenceNumber?: number,
1963
2053
  ): number {
1964
2054
  if (this.connectionState !== ConnectionState.Connected) {
1965
2055
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
@@ -1968,10 +2058,20 @@ export class Container
1968
2058
 
1969
2059
  this.messageCountAfterDisconnection += 1;
1970
2060
  this.collabWindowTracker?.stopSequenceNumberUpdate();
1971
- return this._deltaManager.submit(type, contents, batch, metadata, compression);
2061
+ return this._deltaManager.submit(
2062
+ type,
2063
+ contents,
2064
+ batch,
2065
+ metadata,
2066
+ compression,
2067
+ referenceSequenceNumber,
2068
+ );
1972
2069
  }
1973
2070
 
1974
2071
  private processRemoteMessage(message: ISequencedDocumentMessage) {
2072
+ if (this.offlineLoadEnabled) {
2073
+ this.savedOps.push(message);
2074
+ }
1975
2075
  const local = this.clientId === message.clientId;
1976
2076
 
1977
2077
  // Allow the protocol handler to process the message
@@ -2044,7 +2144,7 @@ export class Container
2044
2144
  });
2045
2145
  }
2046
2146
  this._loadedFromVersion = version;
2047
- const snapshot = (await this.storageService.getSnapshotTree(version)) ?? undefined;
2147
+ const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
2048
2148
 
2049
2149
  if (snapshot === undefined && version !== undefined) {
2050
2150
  this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
@@ -2083,8 +2183,10 @@ export class Container
2083
2183
  loader,
2084
2184
  (type, contents, batch, metadata) =>
2085
2185
  this.submitContainerMessage(type, contents, batch, metadata),
2086
- (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
2087
- (batch: IBatchMessage[]) => this.submitBatch(batch),
2186
+ (summaryOp: ISummaryContent, referenceSequenceNumber?: number) =>
2187
+ this.submitSummaryMessage(summaryOp, referenceSequenceNumber),
2188
+ (batch: IBatchMessage[], referenceSequenceNumber?: number) =>
2189
+ this.submitBatch(batch, referenceSequenceNumber),
2088
2190
  (message) => this.submitSignal(message),
2089
2191
  (error?: ICriticalContainerError) => this.dispose?.(error),
2090
2192
  (error?: ICriticalContainerError) => this.close(error),