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

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 (112) hide show
  1. package/README.md +7 -4
  2. package/closeAndGetPendingLocalState.md +51 -0
  3. package/dist/connectionManager.d.ts.map +1 -1
  4. package/dist/connectionManager.js +43 -11
  5. package/dist/connectionManager.js.map +1 -1
  6. package/dist/connectionStateHandler.d.ts +4 -4
  7. package/dist/connectionStateHandler.d.ts.map +1 -1
  8. package/dist/connectionStateHandler.js +7 -0
  9. package/dist/connectionStateHandler.js.map +1 -1
  10. package/dist/container.d.ts +44 -4
  11. package/dist/container.d.ts.map +1 -1
  12. package/dist/container.js +152 -93
  13. package/dist/container.js.map +1 -1
  14. package/dist/containerContext.d.ts +18 -8
  15. package/dist/containerContext.d.ts.map +1 -1
  16. package/dist/containerContext.js +47 -4
  17. package/dist/containerContext.js.map +1 -1
  18. package/dist/containerStorageAdapter.d.ts +41 -2
  19. package/dist/containerStorageAdapter.d.ts.map +1 -1
  20. package/dist/containerStorageAdapter.js +87 -11
  21. package/dist/containerStorageAdapter.js.map +1 -1
  22. package/dist/contracts.d.ts +2 -2
  23. package/dist/contracts.d.ts.map +1 -1
  24. package/dist/contracts.js.map +1 -1
  25. package/dist/deltaManager.d.ts +3 -2
  26. package/dist/deltaManager.d.ts.map +1 -1
  27. package/dist/deltaManager.js +4 -2
  28. package/dist/deltaManager.js.map +1 -1
  29. package/dist/deltaManagerProxy.d.ts +10 -22
  30. package/dist/deltaManagerProxy.d.ts.map +1 -1
  31. package/dist/deltaManagerProxy.js +14 -50
  32. package/dist/deltaManagerProxy.js.map +1 -1
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/loader.d.ts +10 -1
  38. package/dist/loader.d.ts.map +1 -1
  39. package/dist/loader.js +22 -16
  40. package/dist/loader.js.map +1 -1
  41. package/dist/packageVersion.d.ts +1 -1
  42. package/dist/packageVersion.js +1 -1
  43. package/dist/packageVersion.js.map +1 -1
  44. package/dist/protocolTreeDocumentStorageService.d.ts +6 -2
  45. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  46. package/dist/protocolTreeDocumentStorageService.js +7 -4
  47. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  48. package/dist/utils.d.ts.map +1 -1
  49. package/dist/utils.js +2 -1
  50. package/dist/utils.js.map +1 -1
  51. package/lib/connectionManager.d.ts.map +1 -1
  52. package/lib/connectionManager.js +44 -12
  53. package/lib/connectionManager.js.map +1 -1
  54. package/lib/connectionStateHandler.d.ts +4 -4
  55. package/lib/connectionStateHandler.d.ts.map +1 -1
  56. package/lib/connectionStateHandler.js +7 -0
  57. package/lib/connectionStateHandler.js.map +1 -1
  58. package/lib/container.d.ts +44 -4
  59. package/lib/container.d.ts.map +1 -1
  60. package/lib/container.js +155 -96
  61. package/lib/container.js.map +1 -1
  62. package/lib/containerContext.d.ts +18 -8
  63. package/lib/containerContext.d.ts.map +1 -1
  64. package/lib/containerContext.js +48 -5
  65. package/lib/containerContext.js.map +1 -1
  66. package/lib/containerStorageAdapter.d.ts +41 -2
  67. package/lib/containerStorageAdapter.d.ts.map +1 -1
  68. package/lib/containerStorageAdapter.js +85 -11
  69. package/lib/containerStorageAdapter.js.map +1 -1
  70. package/lib/contracts.d.ts +2 -2
  71. package/lib/contracts.d.ts.map +1 -1
  72. package/lib/contracts.js.map +1 -1
  73. package/lib/deltaManager.d.ts +3 -2
  74. package/lib/deltaManager.d.ts.map +1 -1
  75. package/lib/deltaManager.js +4 -2
  76. package/lib/deltaManager.js.map +1 -1
  77. package/lib/deltaManagerProxy.d.ts +10 -22
  78. package/lib/deltaManagerProxy.d.ts.map +1 -1
  79. package/lib/deltaManagerProxy.js +14 -50
  80. package/lib/deltaManagerProxy.js.map +1 -1
  81. package/lib/index.d.ts +3 -2
  82. package/lib/index.d.ts.map +1 -1
  83. package/lib/index.js +2 -2
  84. package/lib/index.js.map +1 -1
  85. package/lib/loader.d.ts +10 -1
  86. package/lib/loader.d.ts.map +1 -1
  87. package/lib/loader.js +21 -16
  88. package/lib/loader.js.map +1 -1
  89. package/lib/packageVersion.d.ts +1 -1
  90. package/lib/packageVersion.js +1 -1
  91. package/lib/packageVersion.js.map +1 -1
  92. package/lib/protocolTreeDocumentStorageService.d.ts +6 -2
  93. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  94. package/lib/protocolTreeDocumentStorageService.js +7 -4
  95. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  96. package/lib/utils.d.ts.map +1 -1
  97. package/lib/utils.js +2 -1
  98. package/lib/utils.js.map +1 -1
  99. package/package.json +64 -56
  100. package/src/connectionManager.ts +48 -17
  101. package/src/connectionStateHandler.ts +17 -5
  102. package/src/container.ts +224 -116
  103. package/src/containerContext.ts +74 -11
  104. package/src/containerStorageAdapter.ts +113 -9
  105. package/src/contracts.ts +2 -2
  106. package/src/deltaManager.ts +9 -4
  107. package/src/deltaManagerProxy.ts +18 -73
  108. package/src/index.ts +2 -3
  109. package/src/loader.ts +28 -26
  110. package/src/packageVersion.ts +1 -1
  111. package/src/protocolTreeDocumentStorageService.ts +6 -3
  112. package/src/utils.ts +7 -4
package/src/container.ts CHANGED
@@ -12,10 +12,10 @@ import {
12
12
  TelemetryEventCategory,
13
13
  } from "@fluidframework/common-definitions";
14
14
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
15
- import { IRequest, IResponse, IFluidRouter } from "@fluidframework/core-interfaces";
15
+ import { IRequest, IResponse, IFluidRouter, FluidObject } from "@fluidframework/core-interfaces";
16
16
  import {
17
17
  IAudience,
18
- IConnectionDetails,
18
+ IConnectionDetailsInternal,
19
19
  IContainer,
20
20
  IContainerEvents,
21
21
  IDeltaManager,
@@ -44,6 +44,7 @@ import {
44
44
  combineAppAndProtocolSummary,
45
45
  runWithRetry,
46
46
  isFluidResolvedUrl,
47
+ isCombinedAppAndProtocolSummary,
47
48
  } from "@fluidframework/driver-utils";
48
49
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
49
50
  import {
@@ -53,7 +54,6 @@ import {
53
54
  ICommittedProposal,
54
55
  IDocumentAttributes,
55
56
  IDocumentMessage,
56
- IProtocolState,
57
57
  IQuorumClients,
58
58
  IQuorumProposals,
59
59
  ISequencedClient,
@@ -74,7 +74,6 @@ import {
74
74
  raiseConnectedEvent,
75
75
  TelemetryLogger,
76
76
  connectedEventName,
77
- disconnectedEventName,
78
77
  normalizeError,
79
78
  MonitoringContext,
80
79
  loggerToMonitoringContext,
@@ -87,7 +86,12 @@ import { DeltaManager, IConnectionArgs } from "./deltaManager";
87
86
  import { DeltaManagerProxy } from "./deltaManagerProxy";
88
87
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
89
88
  import { pkgVersion } from "./packageVersion";
90
- import { ContainerStorageAdapter } from "./containerStorageAdapter";
89
+ import {
90
+ ContainerStorageAdapter,
91
+ getBlobContentsFromTree,
92
+ getBlobContentsFromTreeWithBlobContents,
93
+ ISerializableBlobContents,
94
+ } from "./containerStorageAdapter";
91
95
  import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
92
96
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
93
97
  import {
@@ -105,6 +109,10 @@ const detachedContainerRefSeqNumber = 0;
105
109
  const dirtyContainerEvent = "dirty";
106
110
  const savedContainerEvent = "saved";
107
111
 
112
+ /**
113
+ * @deprecated this is an internal interface and will not longer be exported in future versions
114
+ * @internal
115
+ */
108
116
  export interface IContainerLoadOptions {
109
117
  /**
110
118
  * Disables the Container from reconnecting if false, allows reconnect otherwise.
@@ -125,6 +133,10 @@ export interface IContainerLoadOptions {
125
133
  loadMode?: IContainerLoadMode;
126
134
  }
127
135
 
136
+ /**
137
+ * @deprecated this is an internal interface and will not longer be exported in future versions
138
+ * @internal
139
+ */
128
140
  export interface IContainerConfig {
129
141
  resolvedUrl?: IFluidResolvedUrl;
130
142
  canReconnect?: boolean;
@@ -255,17 +267,36 @@ export async function ReportIfTooLong(
255
267
  /**
256
268
  * State saved by a container at close time, to be used to load a new instance
257
269
  * of the container to the same state
270
+ * @deprecated this is an internal interface and will not longer be exported in future versions
271
+ * @internal
258
272
  */
259
273
  export interface IPendingContainerState {
260
274
  pendingRuntimeState: unknown;
275
+ /**
276
+ * Snapshot from which container initially loaded.
277
+ */
278
+ baseSnapshot: ISnapshotTree;
279
+ /**
280
+ * Serializable blobs from the base snapshot. Used to load offline since
281
+ * storage is not available.
282
+ */
283
+ snapshotBlobs: ISerializableBlobContents;
284
+ /**
285
+ * All ops since base snapshot sequence number up to the latest op
286
+ * seen when the container was closed. Used to apply stashed (saved pending)
287
+ * ops at the same sequence number at which they were made.
288
+ */
289
+ savedOps: ISequencedDocumentMessage[];
261
290
  url: string;
262
- protocol: IProtocolState;
263
291
  term: number;
264
292
  clientId?: string;
265
293
  }
266
294
 
267
295
  const summarizerClientType = "summarizer";
268
296
 
297
+ /**
298
+ * @deprecated - In the next release Container will no longer be exported, IContainer should be used in its place.
299
+ */
269
300
  export class Container
270
301
  extends EventEmitterWithErrorHandling<IContainerEvents>
271
302
  implements IContainer
@@ -274,6 +305,7 @@ export class Container
274
305
 
275
306
  /**
276
307
  * Load an existing container.
308
+ * @internal
277
309
  */
278
310
  public static async load(
279
311
  loader: Loader,
@@ -434,9 +466,9 @@ export class Container
434
466
 
435
467
  private _attachState = AttachState.Detached;
436
468
 
437
- private readonly storageService: ContainerStorageAdapter;
469
+ private readonly storageAdapter: ContainerStorageAdapter;
438
470
  public get storage(): IDocumentStorageService {
439
- return this.storageService;
471
+ return this.storageAdapter;
440
472
  }
441
473
 
442
474
  private readonly clientDetailsOverride: IClientDetails | undefined;
@@ -467,6 +499,9 @@ export class Container
467
499
  private _resolvedUrl: IFluidResolvedUrl | undefined;
468
500
  private attachStarted = false;
469
501
  private _dirtyContainer = false;
502
+ private readonly savedOps: ISequencedDocumentMessage[] = [];
503
+ private baseSnapshot?: ISnapshotTree;
504
+ private baseSnapshotBlobs?: ISerializableBlobContents;
470
505
 
471
506
  private lastVisible: number | undefined;
472
507
  private readonly visibilityEventHandler: (() => void) | undefined;
@@ -549,6 +584,14 @@ export class Container
549
584
  return this._deltaManager.clientDetails;
550
585
  }
551
586
 
587
+ private get offlineLoadEnabled(): boolean {
588
+ // summarizer will not have any pending state we want to save
589
+ return (
590
+ (this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ?? false) &&
591
+ this.clientDetails.capabilities.interactive
592
+ );
593
+ }
594
+
552
595
  /**
553
596
  * Get the code details that are currently specified for the container.
554
597
  * @returns The current code details if any are specified, undefined if none are specified.
@@ -596,6 +639,44 @@ export class Container
596
639
  return this.loader.services.codeLoader;
597
640
  }
598
641
 
642
+ /**
643
+ * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
644
+ */
645
+ public async getEntryPoint?(): Promise<FluidObject | undefined> {
646
+ // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
647
+ // allow it since they mean a kind of read-only state for the Container.
648
+ // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
649
+ if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
650
+ throw new UsageError("The container is disposing or disposed");
651
+ }
652
+ while (this._context === undefined) {
653
+ await new Promise<void>((resolve, reject) => {
654
+ const contextChangedHandler = () => {
655
+ resolve();
656
+ this.off("disposed", disposedHandler);
657
+ };
658
+ const disposedHandler = (error) => {
659
+ reject(error ?? "The Container is disposed");
660
+ this.off("contextChanged", contextChangedHandler);
661
+ };
662
+ this.once("contextChanged", contextChangedHandler);
663
+ this.once("disposed", disposedHandler);
664
+ });
665
+ // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
666
+ // should have set this._context; making sure.
667
+ assert(
668
+ this._context !== undefined,
669
+ 0x5a2 /* Context still not defined after contextChanged event */,
670
+ );
671
+ }
672
+ // Disable lint rule for the sake of more complete stack traces
673
+ // eslint-disable-next-line no-return-await
674
+ return await this._context.getEntryPoint?.();
675
+ }
676
+
677
+ /**
678
+ * @internal
679
+ */
599
680
  constructor(
600
681
  private readonly loader: Loader,
601
682
  config: IContainerConfig,
@@ -650,6 +731,7 @@ export class Container
650
731
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
651
732
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
652
733
  dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
734
+ dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
653
735
  connectionStateDuration: () =>
654
736
  performance.now() - this.connectionTransitionTimes[this.connectionState],
655
737
  },
@@ -658,18 +740,12 @@ export class Container
658
740
  // Prefix all events in this file with container-loader
659
741
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
660
742
 
661
- const summarizeProtocolTree =
662
- this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
663
- this.loader.services.options.summarizeProtocolTree;
664
-
665
743
  this.options = {
666
744
  ...this.loader.services.options,
667
- summarizeProtocolTree,
668
745
  };
669
746
 
670
747
  this._deltaManager = this.createDeltaManager();
671
748
 
672
- this._clientId = config.serializedContainerState?.clientId;
673
749
  this.connectionStateHandler = createConnectionStateHandler(
674
750
  {
675
751
  logger: this.mc.logger,
@@ -725,19 +801,33 @@ export class Container
725
801
  },
726
802
  },
727
803
  this.deltaManager,
728
- this._clientId,
804
+ config.serializedContainerState?.clientId,
729
805
  );
730
806
 
731
807
  this.on(savedContainerEvent, () => {
732
808
  this.connectionStateHandler.containerSaved();
733
809
  });
734
810
 
735
- this.storageService = new ContainerStorageAdapter(
811
+ // We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
812
+ // non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
813
+ // using this callback and fix them up.
814
+ const addProtocolSummaryIfMissing = (summaryTree: ISummaryTree) =>
815
+ isCombinedAppAndProtocolSummary(summaryTree) === true
816
+ ? summaryTree
817
+ : combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
818
+
819
+ // Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
820
+ // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
821
+ const forceEnableSummarizeProtocolTree =
822
+ this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
823
+ this.loader.services.options.summarizeProtocolTree;
824
+
825
+ this.storageAdapter = new ContainerStorageAdapter(
736
826
  this.loader.services.detachedBlobStorage,
737
827
  this.mc.logger,
738
- this.options.summarizeProtocolTree === true
739
- ? () => this.captureProtocolSummary()
740
- : undefined,
828
+ config.serializedContainerState?.snapshotBlobs,
829
+ addProtocolSummaryIfMissing,
830
+ forceEnableSummarizeProtocolTree,
741
831
  );
742
832
 
743
833
  const isDomAvailable =
@@ -760,43 +850,6 @@ export class Container
760
850
  };
761
851
  document.addEventListener("visibilitychange", this.visibilityEventHandler);
762
852
  }
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
853
  }
801
854
 
802
855
  /**
@@ -840,10 +893,15 @@ export class Container
840
893
  try {
841
894
  // Raise event first, to ensure we capture _lifecycleState before transition.
842
895
  // This gives us a chance to know what errors happened on open vs. on fully loaded container.
896
+ // Log generic events instead of error events if container is in loading state, as most errors are not really FF errors
897
+ // which can pollute telemetry for real bugs
843
898
  this.mc.logger.sendTelemetryEvent(
844
899
  {
845
900
  eventName: "ContainerClose",
846
- category: error === undefined ? "generic" : "error",
901
+ category:
902
+ this._lifecycleState !== "loading" && error !== undefined
903
+ ? "error"
904
+ : "generic",
847
905
  },
848
906
  error,
849
907
  );
@@ -856,7 +914,7 @@ export class Container
856
914
 
857
915
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
858
916
 
859
- this.storageService.dispose();
917
+ this.storageAdapter.dispose();
860
918
 
861
919
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
862
920
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -889,7 +947,7 @@ export class Container
889
947
  this.mc.logger.sendTelemetryEvent(
890
948
  {
891
949
  eventName: "ContainerDispose",
892
- category: error === undefined ? "generic" : "error",
950
+ category: "generic",
893
951
  },
894
952
  error,
895
953
  );
@@ -905,7 +963,7 @@ export class Container
905
963
 
906
964
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
907
965
 
908
- this.storageService.dispose();
966
+ this.storageAdapter.dispose();
909
967
 
910
968
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
911
969
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -933,6 +991,9 @@ export class Container
933
991
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
934
992
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
935
993
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
994
+ if (!this.offlineLoadEnabled) {
995
+ throw new UsageError("Can't get pending local state unless offline load is enabled");
996
+ }
936
997
  assert(
937
998
  this.attachState === AttachState.Attached,
938
999
  0x0d1 /* "Container should be attached before close" */,
@@ -946,10 +1007,14 @@ export class Container
946
1007
  this._protocolHandler.attributes.term !== undefined,
947
1008
  0x37e /* Must have a valid protocol handler instance */,
948
1009
  );
1010
+ assert(!!this.baseSnapshot, "no base snapshot");
1011
+ assert(!!this.baseSnapshotBlobs, "no snapshot blobs");
949
1012
  const pendingState: IPendingContainerState = {
950
1013
  pendingRuntimeState: this.context.getPendingLocalState(),
1014
+ baseSnapshot: this.baseSnapshot,
1015
+ snapshotBlobs: this.baseSnapshotBlobs,
1016
+ savedOps: this.savedOps,
951
1017
  url: this.resolvedUrl.url,
952
- protocol: this.protocolHandler.getProtocolState(),
953
1018
  term: this._protocolHandler.attributes.term,
954
1019
  clientId: this.clientId,
955
1020
  };
@@ -1031,9 +1096,13 @@ export class Container
1031
1096
  // starting to attach the container to storage.
1032
1097
  // Also, this should only be fired in detached container.
1033
1098
  this._attachState = AttachState.Attaching;
1034
- this.context.notifyAttaching(
1035
- getSnapshotTreeFromSerializedContainer(summary),
1036
- );
1099
+ this.emit("attaching");
1100
+ if (this.offlineLoadEnabled) {
1101
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1102
+ this.baseSnapshot = snapshot;
1103
+ this.baseSnapshotBlobs =
1104
+ getBlobContentsFromTreeWithBlobContents(snapshot);
1105
+ }
1037
1106
  }
1038
1107
 
1039
1108
  // Actually go and create the resolved document
@@ -1062,7 +1131,7 @@ export class Container
1062
1131
  const resolvedUrl = this.service.resolvedUrl;
1063
1132
  ensureFluidResolvedUrl(resolvedUrl);
1064
1133
  this._resolvedUrl = resolvedUrl;
1065
- await this.storageService.connectToService(this.service);
1134
+ await this.storageAdapter.connectToService(this.service);
1066
1135
 
1067
1136
  if (hasAttachmentBlobs) {
1068
1137
  // upload blobs to storage
@@ -1082,7 +1151,7 @@ export class Container
1082
1151
  for (const id of newIds) {
1083
1152
  const blob =
1084
1153
  await this.loader.services.detachedBlobStorage.readBlob(id);
1085
- const response = await this.storageService.createBlob(blob);
1154
+ const response = await this.storageAdapter.createBlob(blob);
1086
1155
  redirectTable.set(id, response.id);
1087
1156
  }
1088
1157
  }
@@ -1093,11 +1162,15 @@ export class Container
1093
1162
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1094
1163
 
1095
1164
  this._attachState = AttachState.Attaching;
1096
- this.context.notifyAttaching(
1097
- getSnapshotTreeFromSerializedContainer(summary),
1098
- );
1165
+ this.emit("attaching");
1166
+ if (this.offlineLoadEnabled) {
1167
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1168
+ this.baseSnapshot = snapshot;
1169
+ this.baseSnapshotBlobs =
1170
+ getBlobContentsFromTreeWithBlobContents(snapshot);
1171
+ }
1099
1172
 
1100
- await this.storageService.uploadSummaryWithContext(summary, {
1173
+ await this.storageAdapter.uploadSummaryWithContext(summary, {
1101
1174
  referenceSequenceNumber: 0,
1102
1175
  ackHandle: undefined,
1103
1176
  proposalHandle: undefined,
@@ -1272,7 +1345,7 @@ export class Container
1272
1345
  }
1273
1346
 
1274
1347
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
1275
- const versions = await this.storageService.getVersions(version, 1);
1348
+ const versions = await this.storageAdapter.getVersions(version, 1);
1276
1349
  return versions[0];
1277
1350
  }
1278
1351
 
@@ -1329,15 +1402,15 @@ export class Container
1329
1402
 
1330
1403
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1331
1404
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1332
- if (loadMode.deltaConnection === undefined) {
1405
+ if (loadMode.deltaConnection === undefined && !pendingLocalState) {
1333
1406
  this.connectToDeltaStream(connectionArgs);
1334
1407
  }
1335
1408
 
1336
1409
  if (!pendingLocalState) {
1337
- await this.storageService.connectToService(this.service);
1410
+ await this.storageAdapter.connectToService(this.service);
1338
1411
  } else {
1339
1412
  // if we have pendingLocalState we can load without storage; don't wait for connection
1340
- this.storageService.connectToService(this.service).catch((error) => {
1413
+ this.storageAdapter.connectToService(this.service).catch((error) => {
1341
1414
  this.close(error);
1342
1415
  this.dispose?.(error);
1343
1416
  });
@@ -1349,20 +1422,30 @@ export class Container
1349
1422
  const { snapshot, versionId } =
1350
1423
  pendingLocalState === undefined
1351
1424
  ? await this.fetchSnapshotTree(specifiedVersion)
1352
- : { snapshot: undefined, versionId: undefined };
1353
- assert(
1354
- snapshot !== undefined || pendingLocalState !== undefined,
1355
- 0x237 /* "Snapshot should exist" */,
1425
+ : { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
1426
+
1427
+ if (pendingLocalState) {
1428
+ this.baseSnapshot = pendingLocalState.baseSnapshot;
1429
+ this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
1430
+ } else {
1431
+ assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
1432
+ if (this.offlineLoadEnabled) {
1433
+ this.baseSnapshot = snapshot;
1434
+ // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1435
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1436
+ }
1437
+ }
1438
+
1439
+ const attributes: IDocumentAttributes = await this.getDocumentAttributes(
1440
+ this.storageAdapter,
1441
+ snapshot,
1356
1442
  );
1357
1443
 
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
- };
1444
+ // If we saved ops, we will replay them and don't need DeltaManager to fetch them
1445
+ const sequenceNumber =
1446
+ pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber;
1447
+ const dmAttributes =
1448
+ sequenceNumber !== undefined ? { ...attributes, sequenceNumber } : attributes;
1366
1449
 
1367
1450
  let opsBeforeReturnP: Promise<void> | undefined;
1368
1451
 
@@ -1373,15 +1456,15 @@ export class Container
1373
1456
  // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
1374
1457
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1375
1458
  this.attachDeltaManagerOpHandler(
1376
- attributes,
1459
+ dmAttributes,
1377
1460
  loadMode.deltaConnection !== "none" ? "all" : "none",
1378
1461
  );
1379
1462
  break;
1380
1463
  case "cached":
1381
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "cached");
1464
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
1382
1465
  break;
1383
1466
  case "all":
1384
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "all");
1467
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
1385
1468
  break;
1386
1469
  default:
1387
1470
  unreachableCase(loadMode.opsBeforeReturn);
@@ -1389,22 +1472,7 @@ export class Container
1389
1472
 
1390
1473
  // ...load in the existing quorum
1391
1474
  // 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
- }
1475
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
1408
1476
 
1409
1477
  const codeDetails = this.getCodeDetailsFromQuorum();
1410
1478
  await this.instantiateContext(
@@ -1414,6 +1482,24 @@ export class Container
1414
1482
  pendingLocalState?.pendingRuntimeState,
1415
1483
  );
1416
1484
 
1485
+ // replay saved ops
1486
+ if (pendingLocalState) {
1487
+ for (const message of pendingLocalState.savedOps) {
1488
+ this.processRemoteMessage(message);
1489
+
1490
+ // allow runtime to apply stashed ops at this op's sequence number
1491
+ await this.context.notifyOpReplay(message);
1492
+ }
1493
+ pendingLocalState.savedOps = [];
1494
+
1495
+ // now set clientId to stashed clientId so live ops are correctly processed as local
1496
+ assert(
1497
+ this.clientId === undefined,
1498
+ "Unexpected clientId when setting stashed clientId",
1499
+ );
1500
+ this._clientId = pendingLocalState?.clientId;
1501
+ }
1502
+
1417
1503
  // We might have hit some failure that did not manifest itself in exception in this flow,
1418
1504
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
1419
1505
  if (!this.closed) {
@@ -1437,6 +1523,11 @@ export class Container
1437
1523
 
1438
1524
  switch (loadMode.deltaConnection) {
1439
1525
  case undefined:
1526
+ if (pendingLocalState) {
1527
+ // connect to delta stream now since we did not before
1528
+ this.connectToDeltaStream(connectionArgs);
1529
+ }
1530
+ // intentional fallthrough
1440
1531
  case "delayed":
1441
1532
  assert(
1442
1533
  this.inboundQueuePausedFromInit,
@@ -1512,15 +1603,15 @@ export class Container
1512
1603
  }
1513
1604
 
1514
1605
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1515
- this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1516
- const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1606
+ this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1607
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
1517
1608
 
1518
1609
  await this.attachDeltaManagerOpHandler(attributes);
1519
1610
 
1520
1611
  // Initialize the protocol handler
1521
1612
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1522
1613
  const qValues = await readAndParse<[string, ICommittedProposal][]>(
1523
- this.storageService,
1614
+ this.storageAdapter,
1524
1615
  baseTree.blobs.quorumValues,
1525
1616
  );
1526
1617
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
@@ -1744,7 +1835,7 @@ export class Container
1744
1835
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1745
1836
  deltaManager.inboundSignal.pause();
1746
1837
 
1747
- deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
1838
+ deltaManager.on("connect", (details: IConnectionDetailsInternal, _opsBehind?: number) => {
1748
1839
  assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1749
1840
  this.connectionStateHandler.receivedConnectEvent(details);
1750
1841
  });
@@ -1923,7 +2014,7 @@ export class Container
1923
2014
  }
1924
2015
 
1925
2016
  /** @returns clientSequenceNumber of last message in a batch */
1926
- private submitBatch(batch: IBatchMessage[]): number {
2017
+ private submitBatch(batch: IBatchMessage[], referenceSequenceNumber?: number): number {
1927
2018
  let clientSequenceNumber = -1;
1928
2019
  for (const message of batch) {
1929
2020
  clientSequenceNumber = this.submitMessage(
@@ -1932,13 +2023,14 @@ export class Container
1932
2023
  true, // batch
1933
2024
  message.metadata,
1934
2025
  message.compression,
2026
+ referenceSequenceNumber,
1935
2027
  );
1936
2028
  }
1937
2029
  this._deltaManager.flush();
1938
2030
  return clientSequenceNumber;
1939
2031
  }
1940
2032
 
1941
- private submitSummaryMessage(summary: ISummaryContent) {
2033
+ private submitSummaryMessage(summary: ISummaryContent, referenceSequenceNumber?: number) {
1942
2034
  // github #6451: this is only needed for staging so the server
1943
2035
  // know when the protocol tree is included
1944
2036
  // this can be removed once all clients send
@@ -1946,11 +2038,14 @@ export class Container
1946
2038
  if (summary.details === undefined) {
1947
2039
  summary.details = {};
1948
2040
  }
1949
- summary.details.includesProtocolTree = this.options.summarizeProtocolTree === true;
2041
+ summary.details.includesProtocolTree = this.storageAdapter.summarizeProtocolTree;
1950
2042
  return this.submitMessage(
1951
2043
  MessageType.Summarize,
1952
2044
  JSON.stringify(summary),
1953
2045
  false /* batch */,
2046
+ undefined /* metadata */,
2047
+ undefined /* compression */,
2048
+ referenceSequenceNumber,
1954
2049
  );
1955
2050
  }
1956
2051
 
@@ -1960,6 +2055,7 @@ export class Container
1960
2055
  batch?: boolean,
1961
2056
  metadata?: any,
1962
2057
  compression?: string,
2058
+ referenceSequenceNumber?: number,
1963
2059
  ): number {
1964
2060
  if (this.connectionState !== ConnectionState.Connected) {
1965
2061
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
@@ -1968,10 +2064,20 @@ export class Container
1968
2064
 
1969
2065
  this.messageCountAfterDisconnection += 1;
1970
2066
  this.collabWindowTracker?.stopSequenceNumberUpdate();
1971
- return this._deltaManager.submit(type, contents, batch, metadata, compression);
2067
+ return this._deltaManager.submit(
2068
+ type,
2069
+ contents,
2070
+ batch,
2071
+ metadata,
2072
+ compression,
2073
+ referenceSequenceNumber,
2074
+ );
1972
2075
  }
1973
2076
 
1974
2077
  private processRemoteMessage(message: ISequencedDocumentMessage) {
2078
+ if (this.offlineLoadEnabled) {
2079
+ this.savedOps.push(message);
2080
+ }
1975
2081
  const local = this.clientId === message.clientId;
1976
2082
 
1977
2083
  // Allow the protocol handler to process the message
@@ -2044,7 +2150,7 @@ export class Container
2044
2150
  });
2045
2151
  }
2046
2152
  this._loadedFromVersion = version;
2047
- const snapshot = (await this.storageService.getSnapshotTree(version)) ?? undefined;
2153
+ const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
2048
2154
 
2049
2155
  if (snapshot === undefined && version !== undefined) {
2050
2156
  this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
@@ -2083,8 +2189,10 @@ export class Container
2083
2189
  loader,
2084
2190
  (type, contents, batch, metadata) =>
2085
2191
  this.submitContainerMessage(type, contents, batch, metadata),
2086
- (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
2087
- (batch: IBatchMessage[]) => this.submitBatch(batch),
2192
+ (summaryOp: ISummaryContent, referenceSequenceNumber?: number) =>
2193
+ this.submitSummaryMessage(summaryOp, referenceSequenceNumber),
2194
+ (batch: IBatchMessage[], referenceSequenceNumber?: number) =>
2195
+ this.submitBatch(batch, referenceSequenceNumber),
2088
2196
  (message) => this.submitSignal(message),
2089
2197
  (error?: ICriticalContainerError) => this.dispose?.(error),
2090
2198
  (error?: ICriticalContainerError) => this.close(error),