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