@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/lib/container.js CHANGED
@@ -8,9 +8,9 @@ import { v4 as uuid } from "uuid";
8
8
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
9
9
  import { AttachState, isFluidCodeDetails, } from "@fluidframework/container-definitions";
10
10
  import { GenericError, UsageError } from "@fluidframework/container-utils";
11
- import { readAndParse, OnlineStatus, isOnline, ensureFluidResolvedUrl, combineAppAndProtocolSummary, runWithRetry, isFluidResolvedUrl, } from "@fluidframework/driver-utils";
11
+ import { readAndParse, OnlineStatus, isOnline, ensureFluidResolvedUrl, combineAppAndProtocolSummary, runWithRetry, isFluidResolvedUrl, isCombinedAppAndProtocolSummary, } from "@fluidframework/driver-utils";
12
12
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
13
- import { ChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, TelemetryLogger, connectedEventName, disconnectedEventName, normalizeError, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
13
+ import { ChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, TelemetryLogger, connectedEventName, normalizeError, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
14
14
  import { Audience } from "./audience";
15
15
  import { ContainerContext } from "./containerContext";
16
16
  import { ReconnectMode, getPackageName } from "./contracts";
@@ -18,7 +18,7 @@ import { DeltaManager } from "./deltaManager";
18
18
  import { DeltaManagerProxy } from "./deltaManagerProxy";
19
19
  import { RelativeLoader } from "./loader";
20
20
  import { pkgVersion } from "./packageVersion";
21
- import { ContainerStorageAdapter } from "./containerStorageAdapter";
21
+ import { ContainerStorageAdapter, getBlobContentsFromTree, getBlobContentsFromTreeWithBlobContents, } from "./containerStorageAdapter";
22
22
  import { createConnectionStateHandler } from "./connectionStateHandler";
23
23
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
24
24
  import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy, } from "./quorum";
@@ -119,9 +119,15 @@ export async function ReportIfTooLong(logger, eventName, action) {
119
119
  }
120
120
  }
121
121
  const summarizerClientType = "summarizer";
122
+ /**
123
+ * @deprecated - In the next release Container will no longer be exported, IContainer should be used in its place.
124
+ */
122
125
  export class Container extends EventEmitterWithErrorHandling {
126
+ /**
127
+ * @internal
128
+ */
123
129
  constructor(loader, config, protocolHandlerBuilder) {
124
- var _a, _b;
130
+ var _a, _b, _c;
125
131
  super((name, error) => {
126
132
  this.mc.logger.sendErrorEvent({
127
133
  eventName: "ContainerEventHandlerException",
@@ -157,6 +163,7 @@ export class Container extends EventEmitterWithErrorHandling {
157
163
  this.messageCountAfterDisconnection = 0;
158
164
  this.attachStarted = false;
159
165
  this._dirtyContainer = false;
166
+ this.savedOps = [];
160
167
  this.setAutoReconnectTime = performance.now();
161
168
  this._disposed = false;
162
169
  this.clientDetailsOverride = config.clientDetailsOverride;
@@ -195,15 +202,14 @@ export class Container extends EventEmitterWithErrorHandling {
195
202
  dmLastMsqSeqNumber: () => { var _a, _b; return (_b = (_a = this.deltaManager) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.sequenceNumber; },
196
203
  dmLastMsqSeqTimestamp: () => { var _a, _b; return (_b = (_a = this.deltaManager) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.timestamp; },
197
204
  dmLastMsqSeqClientId: () => { var _a, _b; return (_b = (_a = this.deltaManager) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.clientId; },
205
+ dmLastMsgClientSeq: () => { var _a, _b; return (_b = (_a = this.deltaManager) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.clientSequenceNumber; },
198
206
  connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
199
207
  },
200
208
  });
201
209
  // Prefix all events in this file with container-loader
202
210
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
203
- const summarizeProtocolTree = (_a = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")) !== null && _a !== void 0 ? _a : this.loader.services.options.summarizeProtocolTree;
204
- this.options = Object.assign(Object.assign({}, this.loader.services.options), { summarizeProtocolTree });
211
+ this.options = Object.assign({}, this.loader.services.options);
205
212
  this._deltaManager = this.createDeltaManager();
206
- this._clientId = (_b = config.serializedContainerState) === null || _b === void 0 ? void 0 : _b.clientId;
207
213
  this.connectionStateHandler = createConnectionStateHandler({
208
214
  logger: this.mc.logger,
209
215
  connectionStateChanged: (value, oldState, reason) => {
@@ -242,13 +248,20 @@ export class Container extends EventEmitterWithErrorHandling {
242
248
  this.connect();
243
249
  }
244
250
  },
245
- }, this.deltaManager, this._clientId);
251
+ }, this.deltaManager, (_a = config.serializedContainerState) === null || _a === void 0 ? void 0 : _a.clientId);
246
252
  this.on(savedContainerEvent, () => {
247
253
  this.connectionStateHandler.containerSaved();
248
254
  });
249
- this.storageService = new ContainerStorageAdapter(this.loader.services.detachedBlobStorage, this.mc.logger, this.options.summarizeProtocolTree === true
250
- ? () => this.captureProtocolSummary()
251
- : undefined);
255
+ // We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
256
+ // non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
257
+ // using this callback and fix them up.
258
+ const addProtocolSummaryIfMissing = (summaryTree) => isCombinedAppAndProtocolSummary(summaryTree) === true
259
+ ? summaryTree
260
+ : combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
261
+ // Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
262
+ // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
263
+ const forceEnableSummarizeProtocolTree = (_b = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")) !== null && _b !== void 0 ? _b : this.loader.services.options.summarizeProtocolTree;
264
+ this.storageAdapter = new ContainerStorageAdapter(this.loader.services.detachedBlobStorage, this.mc.logger, (_c = config.serializedContainerState) === null || _c === void 0 ? void 0 : _c.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
252
265
  const isDomAvailable = typeof document === "object" &&
253
266
  document !== null &&
254
267
  typeof document.addEventListener === "function" &&
@@ -269,45 +282,10 @@ export class Container extends EventEmitterWithErrorHandling {
269
282
  };
270
283
  document.addEventListener("visibilitychange", this.visibilityEventHandler);
271
284
  }
272
- // We observed that most users of platform do not check Container.connected event on load, causing bugs.
273
- // As such, we are raising events when new listener pops up.
274
- // Note that we can raise both "disconnected" & "connect" events at the same time,
275
- // if we are in connecting stage.
276
- this.on("newListener", (event, listener) => {
277
- // Fire events on the end of JS turn, giving a chance for caller to be in consistent state.
278
- Promise.resolve()
279
- .then(() => {
280
- switch (event) {
281
- case dirtyContainerEvent:
282
- if (this._dirtyContainer) {
283
- listener();
284
- }
285
- break;
286
- case savedContainerEvent:
287
- if (!this._dirtyContainer) {
288
- listener();
289
- }
290
- break;
291
- case connectedEventName:
292
- if (this.connected) {
293
- listener(this.clientId);
294
- }
295
- break;
296
- case disconnectedEventName:
297
- if (!this.connected) {
298
- listener();
299
- }
300
- break;
301
- default:
302
- }
303
- })
304
- .catch((error) => {
305
- this.mc.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
306
- });
307
- });
308
285
  }
309
286
  /**
310
287
  * Load an existing container.
288
+ * @internal
311
289
  */
312
290
  static async load(loader, loadOptions, pendingLocalState, protocolHandlerBuilder) {
313
291
  const container = new Container(loader, {
@@ -385,7 +363,7 @@ export class Container extends EventEmitterWithErrorHandling {
385
363
  this._lifecycleState === "disposed");
386
364
  }
387
365
  get storage() {
388
- return this.storageService;
366
+ return this.storageAdapter;
389
367
  }
390
368
  get context() {
391
369
  if (this._context === undefined) {
@@ -456,6 +434,12 @@ export class Container extends EventEmitterWithErrorHandling {
456
434
  get clientDetails() {
457
435
  return this._deltaManager.clientDetails;
458
436
  }
437
+ get offlineLoadEnabled() {
438
+ var _a;
439
+ // summarizer will not have any pending state we want to save
440
+ return (((_a = this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad")) !== null && _a !== void 0 ? _a : false) &&
441
+ this.clientDetails.capabilities.interactive);
442
+ }
459
443
  /**
460
444
  * Get the code details that are currently specified for the container.
461
445
  * @returns The current code details if any are specified, undefined if none are specified.
@@ -498,6 +482,38 @@ export class Container extends EventEmitterWithErrorHandling {
498
482
  get codeLoader() {
499
483
  return this.loader.services.codeLoader;
500
484
  }
485
+ /**
486
+ * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
487
+ */
488
+ async getEntryPoint() {
489
+ var _a, _b;
490
+ // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
491
+ // allow it since they mean a kind of read-only state for the Container.
492
+ // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
493
+ if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
494
+ throw new UsageError("The container is disposing or disposed");
495
+ }
496
+ while (this._context === undefined) {
497
+ await new Promise((resolve, reject) => {
498
+ const contextChangedHandler = () => {
499
+ resolve();
500
+ this.off("disposed", disposedHandler);
501
+ };
502
+ const disposedHandler = (error) => {
503
+ reject(error !== null && error !== void 0 ? error : "The Container is disposed");
504
+ this.off("contextChanged", contextChangedHandler);
505
+ };
506
+ this.once("contextChanged", contextChangedHandler);
507
+ this.once("disposed", disposedHandler);
508
+ });
509
+ // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
510
+ // should have set this._context; making sure.
511
+ assert(this._context !== undefined, 0x5a2 /* Context still not defined after contextChanged event */);
512
+ }
513
+ // Disable lint rule for the sake of more complete stack traces
514
+ // eslint-disable-next-line no-return-await
515
+ return await ((_b = (_a = this._context).getEntryPoint) === null || _b === void 0 ? void 0 : _b.call(_a));
516
+ }
501
517
  /**
502
518
  * Retrieves the quorum associated with the document
503
519
  */
@@ -528,15 +544,19 @@ export class Container extends EventEmitterWithErrorHandling {
528
544
  try {
529
545
  // Raise event first, to ensure we capture _lifecycleState before transition.
530
546
  // This gives us a chance to know what errors happened on open vs. on fully loaded container.
547
+ // Log generic events instead of error events if container is in loading state, as most errors are not really FF errors
548
+ // which can pollute telemetry for real bugs
531
549
  this.mc.logger.sendTelemetryEvent({
532
550
  eventName: "ContainerClose",
533
- category: error === undefined ? "generic" : "error",
551
+ category: this._lifecycleState !== "loading" && error !== undefined
552
+ ? "error"
553
+ : "generic",
534
554
  }, error);
535
555
  this._lifecycleState = "closing";
536
556
  (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
537
557
  this.connectionStateHandler.dispose();
538
558
  (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
539
- this.storageService.dispose();
559
+ this.storageAdapter.dispose();
540
560
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
541
561
  // about file, like file being overwritten in storage, but client having stale local cache.
542
562
  // Driver need to ensure all caches are cleared on critical errors
@@ -565,7 +585,7 @@ export class Container extends EventEmitterWithErrorHandling {
565
585
  // This gives us a chance to know what errors happened on open vs. on fully loaded container.
566
586
  this.mc.logger.sendTelemetryEvent({
567
587
  eventName: "ContainerDispose",
568
- category: error === undefined ? "generic" : "error",
588
+ category: "generic",
569
589
  }, error);
570
590
  // ! Progressing from "closed" to "disposing" is not allowed
571
591
  if (this._lifecycleState !== "closed") {
@@ -574,7 +594,7 @@ export class Container extends EventEmitterWithErrorHandling {
574
594
  (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
575
595
  this.connectionStateHandler.dispose();
576
596
  (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
577
- this.storageService.dispose();
597
+ this.storageAdapter.dispose();
578
598
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
579
599
  // about file, like file being overwritten in storage, but client having stale local cache.
580
600
  // Driver need to ensure all caches are cleared on critical errors
@@ -597,14 +617,21 @@ export class Container extends EventEmitterWithErrorHandling {
597
617
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
598
618
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
599
619
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
620
+ if (!this.offlineLoadEnabled) {
621
+ throw new UsageError("Can't get pending local state unless offline load is enabled");
622
+ }
600
623
  assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
601
624
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
602
625
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
603
626
  assert(this._protocolHandler.attributes.term !== undefined, 0x37e /* Must have a valid protocol handler instance */);
627
+ assert(!!this.baseSnapshot, "no base snapshot");
628
+ assert(!!this.baseSnapshotBlobs, "no snapshot blobs");
604
629
  const pendingState = {
605
630
  pendingRuntimeState: this.context.getPendingLocalState(),
631
+ baseSnapshot: this.baseSnapshot,
632
+ snapshotBlobs: this.baseSnapshotBlobs,
633
+ savedOps: this.savedOps,
606
634
  url: this.resolvedUrl.url,
607
- protocol: this.protocolHandler.getProtocolState(),
608
635
  term: this._protocolHandler.attributes.term,
609
636
  clientId: this.clientId,
610
637
  };
@@ -657,7 +684,13 @@ export class Container extends EventEmitterWithErrorHandling {
657
684
  // starting to attach the container to storage.
658
685
  // Also, this should only be fired in detached container.
659
686
  this._attachState = AttachState.Attaching;
660
- this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
687
+ this.emit("attaching");
688
+ if (this.offlineLoadEnabled) {
689
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
690
+ this.baseSnapshot = snapshot;
691
+ this.baseSnapshotBlobs =
692
+ getBlobContentsFromTreeWithBlobContents(snapshot);
693
+ }
661
694
  }
662
695
  // Actually go and create the resolved document
663
696
  const createNewResolvedUrl = await this.urlResolver.resolve(request);
@@ -671,7 +704,7 @@ export class Container extends EventEmitterWithErrorHandling {
671
704
  const resolvedUrl = this.service.resolvedUrl;
672
705
  ensureFluidResolvedUrl(resolvedUrl);
673
706
  this._resolvedUrl = resolvedUrl;
674
- await this.storageService.connectToService(this.service);
707
+ await this.storageAdapter.connectToService(this.service);
675
708
  if (hasAttachmentBlobs) {
676
709
  // upload blobs to storage
677
710
  assert(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
@@ -685,7 +718,7 @@ export class Container extends EventEmitterWithErrorHandling {
685
718
  .filter((id) => !redirectTable.has(id));
686
719
  for (const id of newIds) {
687
720
  const blob = await this.loader.services.detachedBlobStorage.readBlob(id);
688
- const response = await this.storageService.createBlob(blob);
721
+ const response = await this.storageAdapter.createBlob(blob);
689
722
  redirectTable.set(id, response.id);
690
723
  }
691
724
  }
@@ -694,8 +727,14 @@ export class Container extends EventEmitterWithErrorHandling {
694
727
  const protocolSummary = this.captureProtocolSummary();
695
728
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
696
729
  this._attachState = AttachState.Attaching;
697
- this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
698
- await this.storageService.uploadSummaryWithContext(summary, {
730
+ this.emit("attaching");
731
+ if (this.offlineLoadEnabled) {
732
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
733
+ this.baseSnapshot = snapshot;
734
+ this.baseSnapshotBlobs =
735
+ getBlobContentsFromTreeWithBlobContents(snapshot);
736
+ }
737
+ await this.storageAdapter.uploadSummaryWithContext(summary, {
699
738
  referenceSequenceNumber: 0,
700
739
  ackHandle: undefined,
701
740
  proposalHandle: undefined,
@@ -830,7 +869,7 @@ export class Container extends EventEmitterWithErrorHandling {
830
869
  (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error);
831
870
  }
832
871
  async getVersion(version) {
833
- const versions = await this.storageService.getVersions(version, 1);
872
+ const versions = await this.storageAdapter.getVersions(version, 1);
834
873
  return versions[0];
835
874
  }
836
875
  recordConnectStartTime() {
@@ -852,6 +891,7 @@ export class Container extends EventEmitterWithErrorHandling {
852
891
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
853
892
  */
854
893
  async load(specifiedVersion, loadMode, pendingLocalState) {
894
+ var _a;
855
895
  if (this._resolvedUrl === undefined) {
856
896
  throw new Error("Attempting to load without a resolved url");
857
897
  }
@@ -872,15 +912,15 @@ export class Container extends EventEmitterWithErrorHandling {
872
912
  };
873
913
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
874
914
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
875
- if (loadMode.deltaConnection === undefined) {
915
+ if (loadMode.deltaConnection === undefined && !pendingLocalState) {
876
916
  this.connectToDeltaStream(connectionArgs);
877
917
  }
878
918
  if (!pendingLocalState) {
879
- await this.storageService.connectToService(this.service);
919
+ await this.storageAdapter.connectToService(this.service);
880
920
  }
881
921
  else {
882
922
  // if we have pendingLocalState we can load without storage; don't wait for connection
883
- this.storageService.connectToService(this.service).catch((error) => {
923
+ this.storageAdapter.connectToService(this.service).catch((error) => {
884
924
  var _a;
885
925
  this.close(error);
886
926
  (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error);
@@ -890,15 +930,23 @@ export class Container extends EventEmitterWithErrorHandling {
890
930
  // Fetch specified snapshot.
891
931
  const { snapshot, versionId } = pendingLocalState === undefined
892
932
  ? await this.fetchSnapshotTree(specifiedVersion)
893
- : { snapshot: undefined, versionId: undefined };
894
- assert(snapshot !== undefined || pendingLocalState !== undefined, 0x237 /* "Snapshot should exist" */);
895
- const attributes = pendingLocalState === undefined
896
- ? await this.getDocumentAttributes(this.storageService, snapshot)
897
- : {
898
- sequenceNumber: pendingLocalState.protocol.sequenceNumber,
899
- minimumSequenceNumber: pendingLocalState.protocol.minimumSequenceNumber,
900
- term: pendingLocalState.term,
901
- };
933
+ : { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
934
+ if (pendingLocalState) {
935
+ this.baseSnapshot = pendingLocalState.baseSnapshot;
936
+ this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
937
+ }
938
+ else {
939
+ assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
940
+ if (this.offlineLoadEnabled) {
941
+ this.baseSnapshot = snapshot;
942
+ // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
943
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
944
+ }
945
+ }
946
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshot);
947
+ // If we saved ops, we will replay them and don't need DeltaManager to fetch them
948
+ const sequenceNumber = (_a = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.savedOps[pendingLocalState.savedOps.length - 1]) === null || _a === void 0 ? void 0 : _a.sequenceNumber;
949
+ const dmAttributes = sequenceNumber !== undefined ? Object.assign(Object.assign({}, attributes), { sequenceNumber }) : attributes;
902
950
  let opsBeforeReturnP;
903
951
  // Attach op handlers to finish initialization and be able to start processing ops
904
952
  // Kick off any ops fetching if required.
@@ -906,32 +954,35 @@ export class Container extends EventEmitterWithErrorHandling {
906
954
  case undefined:
907
955
  // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
908
956
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
909
- this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none");
957
+ this.attachDeltaManagerOpHandler(dmAttributes, loadMode.deltaConnection !== "none" ? "all" : "none");
910
958
  break;
911
959
  case "cached":
912
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "cached");
960
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
913
961
  break;
914
962
  case "all":
915
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "all");
963
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
916
964
  break;
917
965
  default:
918
966
  unreachableCase(loadMode.opsBeforeReturn);
919
967
  }
920
968
  // ...load in the existing quorum
921
969
  // Initialize the protocol handler
922
- if (pendingLocalState === undefined) {
923
- await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot);
924
- }
925
- else {
926
- this.initializeProtocolState(attributes, {
927
- members: pendingLocalState.protocol.members,
928
- proposals: pendingLocalState.protocol.proposals,
929
- values: pendingLocalState.protocol.values,
930
- });
931
- }
970
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
932
971
  const codeDetails = this.getCodeDetailsFromQuorum();
933
972
  await this.instantiateContext(true, // existing
934
973
  codeDetails, snapshot, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.pendingRuntimeState);
974
+ // replay saved ops
975
+ if (pendingLocalState) {
976
+ for (const message of pendingLocalState.savedOps) {
977
+ this.processRemoteMessage(message);
978
+ // allow runtime to apply stashed ops at this op's sequence number
979
+ await this.context.notifyOpReplay(message);
980
+ }
981
+ pendingLocalState.savedOps = [];
982
+ // now set clientId to stashed clientId so live ops are correctly processed as local
983
+ assert(this.clientId === undefined, "Unexpected clientId when setting stashed clientId");
984
+ this._clientId = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.clientId;
985
+ }
935
986
  // We might have hit some failure that did not manifest itself in exception in this flow,
936
987
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
937
988
  if (!this.closed) {
@@ -944,6 +995,11 @@ export class Container extends EventEmitterWithErrorHandling {
944
995
  }
945
996
  switch (loadMode.deltaConnection) {
946
997
  case undefined:
998
+ if (pendingLocalState) {
999
+ // connect to delta stream now since we did not before
1000
+ this.connectToDeltaStream(connectionArgs);
1001
+ }
1002
+ // intentional fallthrough
947
1003
  case "delayed":
948
1004
  assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
949
1005
  this.inboundQueuePausedFromInit = false;
@@ -998,12 +1054,12 @@ export class Container extends EventEmitterWithErrorHandling {
998
1054
  delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
999
1055
  }
1000
1056
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1001
- this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1002
- const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1057
+ this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1058
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
1003
1059
  await this.attachDeltaManagerOpHandler(attributes);
1004
1060
  // Initialize the protocol handler
1005
1061
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1006
- const qValues = await readAndParse(this.storageService, baseTree.blobs.quorumValues);
1062
+ const qValues = await readAndParse(this.storageAdapter, baseTree.blobs.quorumValues);
1007
1063
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1008
1064
  this.initializeProtocolState(attributes, {
1009
1065
  members: [],
@@ -1280,16 +1336,16 @@ export class Container extends EventEmitterWithErrorHandling {
1280
1336
  }
1281
1337
  }
1282
1338
  /** @returns clientSequenceNumber of last message in a batch */
1283
- submitBatch(batch) {
1339
+ submitBatch(batch, referenceSequenceNumber) {
1284
1340
  let clientSequenceNumber = -1;
1285
1341
  for (const message of batch) {
1286
1342
  clientSequenceNumber = this.submitMessage(MessageType.Operation, message.contents, true, // batch
1287
- message.metadata, message.compression);
1343
+ message.metadata, message.compression, referenceSequenceNumber);
1288
1344
  }
1289
1345
  this._deltaManager.flush();
1290
1346
  return clientSequenceNumber;
1291
1347
  }
1292
- submitSummaryMessage(summary) {
1348
+ submitSummaryMessage(summary, referenceSequenceNumber) {
1293
1349
  // github #6451: this is only needed for staging so the server
1294
1350
  // know when the protocol tree is included
1295
1351
  // this can be removed once all clients send
@@ -1297,10 +1353,10 @@ export class Container extends EventEmitterWithErrorHandling {
1297
1353
  if (summary.details === undefined) {
1298
1354
  summary.details = {};
1299
1355
  }
1300
- summary.details.includesProtocolTree = this.options.summarizeProtocolTree === true;
1301
- return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1356
+ summary.details.includesProtocolTree = this.storageAdapter.summarizeProtocolTree;
1357
+ return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */, undefined /* metadata */, undefined /* compression */, referenceSequenceNumber);
1302
1358
  }
1303
- submitMessage(type, contents, batch, metadata, compression) {
1359
+ submitMessage(type, contents, batch, metadata, compression, referenceSequenceNumber) {
1304
1360
  var _a;
1305
1361
  if (this.connectionState !== ConnectionState.Connected) {
1306
1362
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
@@ -1308,9 +1364,12 @@ export class Container extends EventEmitterWithErrorHandling {
1308
1364
  }
1309
1365
  this.messageCountAfterDisconnection += 1;
1310
1366
  (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1311
- return this._deltaManager.submit(type, contents, batch, metadata, compression);
1367
+ return this._deltaManager.submit(type, contents, batch, metadata, compression, referenceSequenceNumber);
1312
1368
  }
1313
1369
  processRemoteMessage(message) {
1370
+ if (this.offlineLoadEnabled) {
1371
+ this.savedOps.push(message);
1372
+ }
1314
1373
  const local = this.clientId === message.clientId;
1315
1374
  // Allow the protocol handler to process the message
1316
1375
  const result = this.protocolHandler.processMessage(message, local);
@@ -1362,7 +1421,7 @@ export class Container extends EventEmitterWithErrorHandling {
1362
1421
  });
1363
1422
  }
1364
1423
  this._loadedFromVersion = version;
1365
- const snapshot = (_a = (await this.storageService.getSnapshotTree(version))) !== null && _a !== void 0 ? _a : undefined;
1424
+ const snapshot = (_a = (await this.storageAdapter.getSnapshotTree(version))) !== null && _a !== void 0 ? _a : undefined;
1366
1425
  if (snapshot === undefined && version !== undefined) {
1367
1426
  this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1368
1427
  }
@@ -1381,7 +1440,7 @@ export class Container extends EventEmitterWithErrorHandling {
1381
1440
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
1382
1441
  // are set. Global requests will still go directly to the loader
1383
1442
  const loader = new RelativeLoader(this, this.loader);
1384
- this._context = await ContainerContext.createOrLoad(this, this.scope, this.codeLoader, codeDetails, snapshot, new DeltaManagerProxy(this._deltaManager), new QuorumProxy(this.protocolHandler.quorum), loader, (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata), (summaryOp) => this.submitSummaryMessage(summaryOp), (batch) => this.submitBatch(batch), (message) => this.submitSignal(message), (error) => { var _a; return (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error); }, (error) => this.close(error), Container.version, (dirty) => this.updateDirtyContainerState(dirty), existing, pendingLocalState);
1443
+ this._context = await ContainerContext.createOrLoad(this, this.scope, this.codeLoader, codeDetails, snapshot, new DeltaManagerProxy(this._deltaManager), new QuorumProxy(this.protocolHandler.quorum), loader, (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata), (summaryOp, referenceSequenceNumber) => this.submitSummaryMessage(summaryOp, referenceSequenceNumber), (batch, referenceSequenceNumber) => this.submitBatch(batch, referenceSequenceNumber), (message) => this.submitSignal(message), (error) => { var _a; return (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error); }, (error) => this.close(error), Container.version, (dirty) => this.updateDirtyContainerState(dirty), existing, pendingLocalState);
1385
1444
  this.emit("contextChanged", codeDetails);
1386
1445
  }
1387
1446
  updateDirtyContainerState(dirty) {