@fluidframework/container-loader 2.0.0-dev.2.3.0.115467 → 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 (168) hide show
  1. package/.eslintrc.js +18 -21
  2. package/.mocharc.js +2 -2
  3. package/README.md +65 -44
  4. package/api-extractor.json +2 -2
  5. package/closeAndGetPendingLocalState.md +51 -0
  6. package/dist/audience.d.ts +0 -1
  7. package/dist/audience.d.ts.map +1 -1
  8. package/dist/audience.js.map +1 -1
  9. package/dist/catchUpMonitor.d.ts.map +1 -1
  10. package/dist/catchUpMonitor.js.map +1 -1
  11. package/dist/collabWindowTracker.d.ts.map +1 -1
  12. package/dist/collabWindowTracker.js.map +1 -1
  13. package/dist/connectionManager.d.ts +5 -5
  14. package/dist/connectionManager.d.ts.map +1 -1
  15. package/dist/connectionManager.js +107 -44
  16. package/dist/connectionManager.js.map +1 -1
  17. package/dist/connectionState.d.ts.map +1 -1
  18. package/dist/connectionState.js.map +1 -1
  19. package/dist/connectionStateHandler.d.ts +7 -7
  20. package/dist/connectionStateHandler.d.ts.map +1 -1
  21. package/dist/connectionStateHandler.js +50 -21
  22. package/dist/connectionStateHandler.js.map +1 -1
  23. package/dist/container.d.ts +64 -5
  24. package/dist/container.d.ts.map +1 -1
  25. package/dist/container.js +329 -137
  26. package/dist/container.js.map +1 -1
  27. package/dist/containerContext.d.ts +19 -8
  28. package/dist/containerContext.d.ts.map +1 -1
  29. package/dist/containerContext.js +58 -14
  30. package/dist/containerContext.js.map +1 -1
  31. package/dist/containerStorageAdapter.d.ts +41 -2
  32. package/dist/containerStorageAdapter.d.ts.map +1 -1
  33. package/dist/containerStorageAdapter.js +88 -14
  34. package/dist/containerStorageAdapter.js.map +1 -1
  35. package/dist/contracts.d.ts +3 -3
  36. package/dist/contracts.d.ts.map +1 -1
  37. package/dist/contracts.js.map +1 -1
  38. package/dist/deltaManager.d.ts +21 -8
  39. package/dist/deltaManager.d.ts.map +1 -1
  40. package/dist/deltaManager.js +112 -37
  41. package/dist/deltaManager.js.map +1 -1
  42. package/dist/deltaManagerProxy.d.ts +10 -22
  43. package/dist/deltaManagerProxy.d.ts.map +1 -1
  44. package/dist/deltaManagerProxy.js +14 -50
  45. package/dist/deltaManagerProxy.js.map +1 -1
  46. package/dist/deltaQueue.d.ts.map +1 -1
  47. package/dist/deltaQueue.js +4 -2
  48. package/dist/deltaQueue.js.map +1 -1
  49. package/dist/index.d.ts +4 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -3
  52. package/dist/index.js.map +1 -1
  53. package/dist/loader.d.ts +13 -4
  54. package/dist/loader.d.ts.map +1 -1
  55. package/dist/loader.js +38 -24
  56. package/dist/loader.js.map +1 -1
  57. package/dist/packageVersion.d.ts +1 -1
  58. package/dist/packageVersion.js +1 -1
  59. package/dist/packageVersion.js.map +1 -1
  60. package/dist/protocol.d.ts.map +1 -1
  61. package/dist/protocol.js +2 -1
  62. package/dist/protocol.js.map +1 -1
  63. package/dist/protocolTreeDocumentStorageService.d.ts +6 -2
  64. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  65. package/dist/protocolTreeDocumentStorageService.js +7 -4
  66. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  67. package/dist/quorum.d.ts.map +1 -1
  68. package/dist/quorum.js.map +1 -1
  69. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  70. package/dist/retriableDocumentStorageService.js +6 -2
  71. package/dist/retriableDocumentStorageService.js.map +1 -1
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +8 -5
  74. package/dist/utils.js.map +1 -1
  75. package/lib/audience.d.ts +0 -1
  76. package/lib/audience.d.ts.map +1 -1
  77. package/lib/audience.js.map +1 -1
  78. package/lib/catchUpMonitor.d.ts.map +1 -1
  79. package/lib/catchUpMonitor.js.map +1 -1
  80. package/lib/collabWindowTracker.d.ts.map +1 -1
  81. package/lib/collabWindowTracker.js.map +1 -1
  82. package/lib/connectionManager.d.ts +5 -5
  83. package/lib/connectionManager.d.ts.map +1 -1
  84. package/lib/connectionManager.js +110 -47
  85. package/lib/connectionManager.js.map +1 -1
  86. package/lib/connectionState.d.ts.map +1 -1
  87. package/lib/connectionState.js.map +1 -1
  88. package/lib/connectionStateHandler.d.ts +7 -7
  89. package/lib/connectionStateHandler.d.ts.map +1 -1
  90. package/lib/connectionStateHandler.js +50 -21
  91. package/lib/connectionStateHandler.js.map +1 -1
  92. package/lib/container.d.ts +64 -5
  93. package/lib/container.d.ts.map +1 -1
  94. package/lib/container.js +336 -144
  95. package/lib/container.js.map +1 -1
  96. package/lib/containerContext.d.ts +19 -8
  97. package/lib/containerContext.d.ts.map +1 -1
  98. package/lib/containerContext.js +59 -15
  99. package/lib/containerContext.js.map +1 -1
  100. package/lib/containerStorageAdapter.d.ts +41 -2
  101. package/lib/containerStorageAdapter.d.ts.map +1 -1
  102. package/lib/containerStorageAdapter.js +86 -14
  103. package/lib/containerStorageAdapter.js.map +1 -1
  104. package/lib/contracts.d.ts +3 -3
  105. package/lib/contracts.d.ts.map +1 -1
  106. package/lib/contracts.js.map +1 -1
  107. package/lib/deltaManager.d.ts +21 -8
  108. package/lib/deltaManager.d.ts.map +1 -1
  109. package/lib/deltaManager.js +114 -39
  110. package/lib/deltaManager.js.map +1 -1
  111. package/lib/deltaManagerProxy.d.ts +10 -22
  112. package/lib/deltaManagerProxy.d.ts.map +1 -1
  113. package/lib/deltaManagerProxy.js +14 -50
  114. package/lib/deltaManagerProxy.js.map +1 -1
  115. package/lib/deltaQueue.d.ts.map +1 -1
  116. package/lib/deltaQueue.js +4 -2
  117. package/lib/deltaQueue.js.map +1 -1
  118. package/lib/index.d.ts +4 -3
  119. package/lib/index.d.ts.map +1 -1
  120. package/lib/index.js +2 -2
  121. package/lib/index.js.map +1 -1
  122. package/lib/loader.d.ts +13 -4
  123. package/lib/loader.d.ts.map +1 -1
  124. package/lib/loader.js +37 -24
  125. package/lib/loader.js.map +1 -1
  126. package/lib/packageVersion.d.ts +1 -1
  127. package/lib/packageVersion.js +1 -1
  128. package/lib/packageVersion.js.map +1 -1
  129. package/lib/protocol.d.ts.map +1 -1
  130. package/lib/protocol.js +2 -1
  131. package/lib/protocol.js.map +1 -1
  132. package/lib/protocolTreeDocumentStorageService.d.ts +6 -2
  133. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  134. package/lib/protocolTreeDocumentStorageService.js +7 -4
  135. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  136. package/lib/quorum.d.ts.map +1 -1
  137. package/lib/quorum.js.map +1 -1
  138. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  139. package/lib/retriableDocumentStorageService.js +6 -2
  140. package/lib/retriableDocumentStorageService.js.map +1 -1
  141. package/lib/utils.d.ts.map +1 -1
  142. package/lib/utils.js +8 -5
  143. package/lib/utils.js.map +1 -1
  144. package/package.json +67 -56
  145. package/prettier.config.cjs +1 -1
  146. package/src/audience.ts +51 -46
  147. package/src/catchUpMonitor.ts +39 -37
  148. package/src/collabWindowTracker.ts +75 -70
  149. package/src/connectionManager.ts +1040 -941
  150. package/src/connectionState.ts +19 -19
  151. package/src/connectionStateHandler.ts +557 -463
  152. package/src/container.ts +2147 -1784
  153. package/src/containerContext.ts +417 -345
  154. package/src/containerStorageAdapter.ts +268 -154
  155. package/src/contracts.ts +155 -153
  156. package/src/deltaManager.ts +1074 -945
  157. package/src/deltaManagerProxy.ts +88 -137
  158. package/src/deltaQueue.ts +155 -151
  159. package/src/index.ts +13 -17
  160. package/src/loader.ts +434 -427
  161. package/src/packageVersion.ts +1 -1
  162. package/src/protocol.ts +93 -87
  163. package/src/protocolTreeDocumentStorageService.ts +34 -34
  164. package/src/quorum.ts +34 -34
  165. package/src/retriableDocumentStorageService.ts +118 -102
  166. package/src/utils.ts +93 -83
  167. package/tsconfig.esnext.json +6 -6
  168. package/tsconfig.json +8 -12
package/lib/container.js CHANGED
@@ -7,10 +7,10 @@ import merge from "lodash/merge";
7
7
  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
- import { GenericError, UsageError, } from "@fluidframework/container-utils";
11
- import { readAndParse, OnlineStatus, isOnline, ensureFluidResolvedUrl, combineAppAndProtocolSummary, runWithRetry, isFluidResolvedUrl, } from "@fluidframework/driver-utils";
10
+ import { GenericError, UsageError } from "@fluidframework/container-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,14 +18,14 @@ 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";
22
- import { createConnectionStateHandler, } from "./connectionStateHandler";
21
+ import { ContainerStorageAdapter, getBlobContentsFromTree, getBlobContentsFromTreeWithBlobContents, } from "./containerStorageAdapter";
22
+ import { createConnectionStateHandler } from "./connectionStateHandler";
23
23
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
24
- import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy } from "./quorum";
24
+ import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy, } from "./quorum";
25
25
  import { CollabWindowTracker } from "./collabWindowTracker";
26
26
  import { ConnectionManager } from "./connectionManager";
27
27
  import { ConnectionState } from "./connectionState";
28
- import { ProtocolHandler, } from "./protocol";
28
+ import { ProtocolHandler } from "./protocol";
29
29
  const detachedContainerRefSeqNumber = 0;
30
30
  const dirtyContainerEvent = "dirty";
31
31
  const savedContainerEvent = "saved";
@@ -65,8 +65,8 @@ export async function waitContainerToCatchUp(container) {
65
65
  // Waiting for "connected" state in either case gets us at least to our own Join op
66
66
  // which is a reasonable approximation of "caught up"
67
67
  const waitForOps = () => {
68
- assert(container.connectionState === ConnectionState.CatchingUp
69
- || container.connectionState === ConnectionState.Connected, 0x0cd /* "Container disconnected while waiting for ops!" */);
68
+ assert(container.connectionState === ConnectionState.CatchingUp ||
69
+ container.connectionState === ConnectionState.Connected, 0x0cd /* "Container disconnected while waiting for ops!" */);
70
70
  const hasCheckpointSequenceNumber = deltaManager.hasCheckpointSequenceNumber;
71
71
  const connectionOpSeqNumber = deltaManager.lastKnownSeqNumber;
72
72
  assert(deltaManager.lastSequenceNumber <= connectionOpSeqNumber, 0x266 /* "lastKnownSeqNumber should never be below last processed sequence number" */);
@@ -106,7 +106,7 @@ const getCodeProposal =
106
106
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
107
107
  (quorum) => { var _a; return (_a = quorum.get("code")) !== null && _a !== void 0 ? _a : quorum.get("code2"); };
108
108
  /**
109
- * Helper function to report to telemetry cases where operation takes longer than expected (1s)
109
+ * Helper function to report to telemetry cases where operation takes longer than expected (200ms)
110
110
  * @param logger - logger to use
111
111
  * @param eventName - event name
112
112
  * @param action - functor to call and measure
@@ -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",
@@ -133,6 +139,21 @@ export class Container extends EventEmitterWithErrorHandling {
133
139
  // Tells if container can reconnect on losing fist connection
134
140
  // If false, container gets closed on loss of connection.
135
141
  this._canReconnect = true;
142
+ /**
143
+ * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
144
+ *
145
+ * States are allowed to progress to further states:
146
+ * "loading" - "loaded" - "closing" - "disposing" - "closed" - "disposed"
147
+ *
148
+ * For example, moving from "closed" to "disposing" is not allowed since it is an earlier state.
149
+ *
150
+ * loading: Container has been created, but is not yet in normal/loaded state
151
+ * loaded: Container is in normal/loaded state
152
+ * closing: Container has started closing process (for re-entrancy prevention)
153
+ * disposing: Container has started disposing process (for re-entrancy prevention)
154
+ * closed: Container has closed
155
+ * disposed: Container has been disposed
156
+ */
136
157
  this._lifecycleState = "loading";
137
158
  this._attachState = AttachState.Detached;
138
159
  /** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
@@ -142,7 +163,9 @@ export class Container extends EventEmitterWithErrorHandling {
142
163
  this.messageCountAfterDisconnection = 0;
143
164
  this.attachStarted = false;
144
165
  this._dirtyContainer = false;
166
+ this.savedOps = [];
145
167
  this.setAutoReconnectTime = performance.now();
168
+ this._disposed = false;
146
169
  this.clientDetailsOverride = config.clientDetailsOverride;
147
170
  this._resolvedUrl = config.resolvedUrl;
148
171
  if (config.canReconnect !== undefined) {
@@ -179,15 +202,14 @@ export class Container extends EventEmitterWithErrorHandling {
179
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; },
180
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; },
181
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; },
182
206
  connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
183
207
  },
184
208
  });
185
209
  // Prefix all events in this file with container-loader
186
210
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
187
- const summarizeProtocolTree = (_a = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")) !== null && _a !== void 0 ? _a : this.loader.services.options.summarizeProtocolTree;
188
- this.options = Object.assign(Object.assign({}, this.loader.services.options), { summarizeProtocolTree });
211
+ this.options = Object.assign({}, this.loader.services.options);
189
212
  this._deltaManager = this.createDeltaManager();
190
- this._clientId = (_b = config.serializedContainerState) === null || _b === void 0 ? void 0 : _b.clientId;
191
213
  this.connectionStateHandler = createConnectionStateHandler({
192
214
  logger: this.mc.logger,
193
215
  connectionStateChanged: (value, oldState, reason) => {
@@ -196,17 +218,23 @@ export class Container extends EventEmitterWithErrorHandling {
196
218
  }
197
219
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
198
220
  if (this._lifecycleState === "loaded") {
199
- this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected ? reason : undefined /* disconnectedReason */);
221
+ this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
222
+ ? reason
223
+ : undefined /* disconnectedReason */);
200
224
  }
201
225
  },
202
226
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
203
227
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
204
- logConnectionIssue: (eventName, details) => {
228
+ logConnectionIssue: (eventName, category, details) => {
205
229
  const mode = this.connectionMode;
206
230
  // We get here when socket does not receive any ops on "write" connection, including
207
- // its own join op. Attempt recovery option.
231
+ // its own join op.
232
+ // Report issues only if we already loaded container - op processing is paused while container is loading,
233
+ // so we always time-out processing of join op in cases where fetching snapshot takes a minute.
234
+ // It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
208
235
  this._deltaManager.logConnectionIssue(Object.assign({ eventName,
209
- mode, duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
236
+ mode, category: this._lifecycleState === "loading" ? "generic" : category, duration: performance.now() -
237
+ this.connectionTransitionTimes[ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
210
238
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
211
239
  // to very slow op fetches and we will eventually get there.
212
240
  // For "read" connections, we get here due to self join signal not arriving on time. We will need to
@@ -220,13 +248,20 @@ export class Container extends EventEmitterWithErrorHandling {
220
248
  this.connect();
221
249
  }
222
250
  },
223
- }, this.deltaManager, this._clientId);
251
+ }, this.deltaManager, (_a = config.serializedContainerState) === null || _a === void 0 ? void 0 : _a.clientId);
224
252
  this.on(savedContainerEvent, () => {
225
253
  this.connectionStateHandler.containerSaved();
226
254
  });
227
- this.storageService = new ContainerStorageAdapter(this.loader.services.detachedBlobStorage, this.mc.logger, this.options.summarizeProtocolTree === true
228
- ? () => this.captureProtocolSummary()
229
- : 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);
230
265
  const isDomAvailable = typeof document === "object" &&
231
266
  document !== null &&
232
267
  typeof document.addEventListener === "function" &&
@@ -240,48 +275,17 @@ export class Container extends EventEmitterWithErrorHandling {
240
275
  }
241
276
  else {
242
277
  // settimeout so this will hopefully fire after disconnect event if being hidden caused it
243
- setTimeout(() => { this.lastVisible = undefined; }, 0);
278
+ setTimeout(() => {
279
+ this.lastVisible = undefined;
280
+ }, 0);
244
281
  }
245
282
  };
246
283
  document.addEventListener("visibilitychange", this.visibilityEventHandler);
247
284
  }
248
- // We observed that most users of platform do not check Container.connected event on load, causing bugs.
249
- // As such, we are raising events when new listener pops up.
250
- // Note that we can raise both "disconnected" & "connect" events at the same time,
251
- // if we are in connecting stage.
252
- this.on("newListener", (event, listener) => {
253
- // Fire events on the end of JS turn, giving a chance for caller to be in consistent state.
254
- Promise.resolve().then(() => {
255
- switch (event) {
256
- case dirtyContainerEvent:
257
- if (this._dirtyContainer) {
258
- listener();
259
- }
260
- break;
261
- case savedContainerEvent:
262
- if (!this._dirtyContainer) {
263
- listener();
264
- }
265
- break;
266
- case connectedEventName:
267
- if (this.connected) {
268
- listener(this.clientId);
269
- }
270
- break;
271
- case disconnectedEventName:
272
- if (!this.connected) {
273
- listener();
274
- }
275
- break;
276
- default:
277
- }
278
- }).catch((error) => {
279
- this.mc.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
280
- });
281
- });
282
285
  }
283
286
  /**
284
287
  * Load an existing container.
288
+ * @internal
285
289
  */
286
290
  static async load(loader, loadOptions, pendingLocalState, protocolHandlerBuilder) {
287
291
  const container = new Container(loader, {
@@ -303,7 +307,8 @@ export class Container extends EventEmitterWithErrorHandling {
303
307
  reject(err !== null && err !== void 0 ? err : new GenericError("Container closed without error during load"));
304
308
  };
305
309
  container.on("closed", onClosed);
306
- container.load(version, mode, pendingLocalState)
310
+ container
311
+ .load(version, mode, pendingLocalState)
307
312
  .finally(() => {
308
313
  container.removeListener("closed", onClosed);
309
314
  })
@@ -352,10 +357,13 @@ export class Container extends EventEmitterWithErrorHandling {
352
357
  }
353
358
  }
354
359
  get closed() {
355
- return (this._lifecycleState === "closing" || this._lifecycleState === "closed");
360
+ return (this._lifecycleState === "closing" ||
361
+ this._lifecycleState === "closed" ||
362
+ this._lifecycleState === "disposing" ||
363
+ this._lifecycleState === "disposed");
356
364
  }
357
365
  get storage() {
358
- return this.storageService;
366
+ return this.storageAdapter;
359
367
  }
360
368
  get context() {
361
369
  if (this._context === undefined) {
@@ -369,8 +377,12 @@ export class Container extends EventEmitterWithErrorHandling {
369
377
  }
370
378
  return this._protocolHandler;
371
379
  }
372
- get connectionMode() { return this._deltaManager.connectionManager.connectionMode; }
373
- get IFluidRouter() { return this; }
380
+ get connectionMode() {
381
+ return this._deltaManager.connectionManager.connectionMode;
382
+ }
383
+ get IFluidRouter() {
384
+ return this;
385
+ }
374
386
  get resolvedUrl() {
375
387
  return this._resolvedUrl;
376
388
  }
@@ -422,6 +434,12 @@ export class Container extends EventEmitterWithErrorHandling {
422
434
  get clientDetails() {
423
435
  return this._deltaManager.clientDetails;
424
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
+ }
425
443
  /**
426
444
  * Get the code details that are currently specified for the container.
427
445
  * @returns The current code details if any are specified, undefined if none are specified.
@@ -452,24 +470,71 @@ export class Container extends EventEmitterWithErrorHandling {
452
470
  get isDirty() {
453
471
  return this._dirtyContainer;
454
472
  }
455
- get serviceFactory() { return this.loader.services.documentServiceFactory; }
456
- get urlResolver() { return this.loader.services.urlResolver; }
457
- get scope() { return this.loader.services.scope; }
458
- get codeLoader() { return this.loader.services.codeLoader; }
473
+ get serviceFactory() {
474
+ return this.loader.services.documentServiceFactory;
475
+ }
476
+ get urlResolver() {
477
+ return this.loader.services.urlResolver;
478
+ }
479
+ get scope() {
480
+ return this.loader.services.scope;
481
+ }
482
+ get codeLoader() {
483
+ return this.loader.services.codeLoader;
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
+ }
459
517
  /**
460
518
  * Retrieves the quorum associated with the document
461
519
  */
462
520
  getQuorum() {
463
521
  return this.protocolHandler.quorum;
464
522
  }
523
+ dispose(error) {
524
+ this._deltaManager.close(error, true /* doDispose */);
525
+ this.verifyClosed();
526
+ }
465
527
  close(error) {
466
528
  // 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
467
529
  // 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
468
530
  // handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
469
531
  // "closing" will lose that info (can also solve by tracking extra state).
470
532
  this._deltaManager.close(error);
533
+ this.verifyClosed();
534
+ }
535
+ verifyClosed() {
471
536
  assert(this.connectionState === ConnectionState.Disconnected, 0x0cf /* "disconnect event was not raised!" */);
472
- assert(this._lifecycleState === "closed", 0x314 /* Container properly closed */);
537
+ assert(this._lifecycleState === "closed" || this._lifecycleState === "disposed", 0x314 /* Container properly closed */);
473
538
  }
474
539
  closeCore(error) {
475
540
  var _a, _b, _c;
@@ -479,15 +544,19 @@ export class Container extends EventEmitterWithErrorHandling {
479
544
  try {
480
545
  // Raise event first, to ensure we capture _lifecycleState before transition.
481
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
482
549
  this.mc.logger.sendTelemetryEvent({
483
550
  eventName: "ContainerClose",
484
- category: error === undefined ? "generic" : "error",
551
+ category: this._lifecycleState !== "loading" && error !== undefined
552
+ ? "error"
553
+ : "generic",
485
554
  }, error);
486
555
  this._lifecycleState = "closing";
487
556
  (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
488
557
  this.connectionStateHandler.dispose();
489
558
  (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
490
- this.storageService.dispose();
559
+ this.storageAdapter.dispose();
491
560
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
492
561
  // about file, like file being overwritten in storage, but client having stale local cache.
493
562
  // Driver need to ensure all caches are cleared on critical errors
@@ -497,7 +566,6 @@ export class Container extends EventEmitterWithErrorHandling {
497
566
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
498
567
  }
499
568
  this.emit("closed", error);
500
- this.removeAllListeners();
501
569
  if (this.visibilityEventHandler !== undefined) {
502
570
  document.removeEventListener("visibilitychange", this.visibilityEventHandler);
503
571
  }
@@ -506,22 +574,69 @@ export class Container extends EventEmitterWithErrorHandling {
506
574
  this._lifecycleState = "closed";
507
575
  }
508
576
  }
577
+ disposeCore(error) {
578
+ var _a, _b, _c;
579
+ assert(!this._disposed, 0x54c /* Container already disposed */);
580
+ this._disposed = true;
581
+ try {
582
+ // Ensure that we raise all key events even if one of these throws
583
+ try {
584
+ // Raise event first, to ensure we capture _lifecycleState before transition.
585
+ // This gives us a chance to know what errors happened on open vs. on fully loaded container.
586
+ this.mc.logger.sendTelemetryEvent({
587
+ eventName: "ContainerDispose",
588
+ category: "generic",
589
+ }, error);
590
+ // ! Progressing from "closed" to "disposing" is not allowed
591
+ if (this._lifecycleState !== "closed") {
592
+ this._lifecycleState = "disposing";
593
+ }
594
+ (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
595
+ this.connectionStateHandler.dispose();
596
+ (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
597
+ this.storageAdapter.dispose();
598
+ // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
599
+ // about file, like file being overwritten in storage, but client having stale local cache.
600
+ // Driver need to ensure all caches are cleared on critical errors
601
+ (_c = this.service) === null || _c === void 0 ? void 0 : _c.dispose(error);
602
+ }
603
+ catch (exception) {
604
+ this.mc.logger.sendErrorEvent({ eventName: "ContainerDisposeException" }, exception);
605
+ }
606
+ this.emit("disposed", error);
607
+ this.removeAllListeners();
608
+ if (this.visibilityEventHandler !== undefined) {
609
+ document.removeEventListener("visibilitychange", this.visibilityEventHandler);
610
+ }
611
+ }
612
+ finally {
613
+ this._lifecycleState = "disposed";
614
+ }
615
+ }
509
616
  closeAndGetPendingLocalState() {
510
617
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
511
618
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
512
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
+ }
513
623
  assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
514
624
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
515
625
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
516
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");
517
629
  const pendingState = {
518
630
  pendingRuntimeState: this.context.getPendingLocalState(),
631
+ baseSnapshot: this.baseSnapshot,
632
+ snapshotBlobs: this.baseSnapshotBlobs,
633
+ savedOps: this.savedOps,
519
634
  url: this.resolvedUrl.url,
520
- protocol: this.protocolHandler.getProtocolState(),
521
635
  term: this._protocolHandler.attributes.term,
522
636
  clientId: this.clientId,
523
637
  };
524
638
  this.mc.logger.sendTelemetryEvent({ eventName: "CloseAndGetPendingLocalState" });
639
+ // Only close here as method name suggests
525
640
  this.close();
526
641
  return JSON.stringify(pendingState);
527
642
  }
@@ -533,13 +648,18 @@ export class Container extends EventEmitterWithErrorHandling {
533
648
  const appSummary = this.context.createSummary();
534
649
  const protocolSummary = this.captureProtocolSummary();
535
650
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
536
- if (this.loader.services.detachedBlobStorage && this.loader.services.detachedBlobStorage.size > 0) {
537
- combinedSummary.tree[".hasAttachmentBlobs"] = { type: SummaryType.Blob, content: "true" };
651
+ if (this.loader.services.detachedBlobStorage &&
652
+ this.loader.services.detachedBlobStorage.size > 0) {
653
+ combinedSummary.tree[".hasAttachmentBlobs"] = {
654
+ type: SummaryType.Blob,
655
+ content: "true",
656
+ };
538
657
  }
539
658
  return JSON.stringify(combinedSummary);
540
659
  }
541
660
  async attach(request) {
542
661
  await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
662
+ var _a;
543
663
  if (this._lifecycleState !== "loaded") {
544
664
  // pre-0.58 error message: containerNotValidForAttach
545
665
  throw new UsageError(`The Container is not in a valid state for attach [${this._lifecycleState}]`);
@@ -548,8 +668,8 @@ export class Container extends EventEmitterWithErrorHandling {
548
668
  assert(this._attachState === AttachState.Detached && !this.attachStarted, 0x205 /* "attach() called more than once" */);
549
669
  this.attachStarted = true;
550
670
  // If attachment blobs were uploaded in detached state we will go through a different attach flow
551
- const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined
552
- && this.loader.services.detachedBlobStorage.size > 0;
671
+ const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined &&
672
+ this.loader.services.detachedBlobStorage.size > 0;
553
673
  try {
554
674
  assert(this.deltaManager.inbound.length === 0, 0x0d6 /* "Inbound queue should be empty when attaching" */);
555
675
  let summary;
@@ -564,7 +684,13 @@ export class Container extends EventEmitterWithErrorHandling {
564
684
  // starting to attach the container to storage.
565
685
  // Also, this should only be fired in detached container.
566
686
  this._attachState = AttachState.Attaching;
567
- 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
+ }
568
694
  }
569
695
  // Actually go and create the resolved document
570
696
  const createNewResolvedUrl = await this.urlResolver.resolve(request);
@@ -578,7 +704,7 @@ export class Container extends EventEmitterWithErrorHandling {
578
704
  const resolvedUrl = this.service.resolvedUrl;
579
705
  ensureFluidResolvedUrl(resolvedUrl);
580
706
  this._resolvedUrl = resolvedUrl;
581
- await this.storageService.connectToService(this.service);
707
+ await this.storageAdapter.connectToService(this.service);
582
708
  if (hasAttachmentBlobs) {
583
709
  // upload blobs to storage
584
710
  assert(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
@@ -587,10 +713,12 @@ export class Container extends EventEmitterWithErrorHandling {
587
713
  const redirectTable = new Map();
588
714
  // if new blobs are added while uploading, upload them too
589
715
  while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
590
- const newIds = this.loader.services.detachedBlobStorage.getBlobIds().filter((id) => !redirectTable.has(id));
716
+ const newIds = this.loader.services.detachedBlobStorage
717
+ .getBlobIds()
718
+ .filter((id) => !redirectTable.has(id));
591
719
  for (const id of newIds) {
592
720
  const blob = await this.loader.services.detachedBlobStorage.readBlob(id);
593
- const response = await this.storageService.createBlob(blob);
721
+ const response = await this.storageAdapter.createBlob(blob);
594
722
  redirectTable.set(id, response.id);
595
723
  }
596
724
  }
@@ -599,8 +727,14 @@ export class Container extends EventEmitterWithErrorHandling {
599
727
  const protocolSummary = this.captureProtocolSummary();
600
728
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
601
729
  this._attachState = AttachState.Attaching;
602
- this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
603
- 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, {
604
738
  referenceSequenceNumber: 0,
605
739
  ackHandle: undefined,
606
740
  proposalHandle: undefined,
@@ -609,7 +743,10 @@ export class Container extends EventEmitterWithErrorHandling {
609
743
  this._attachState = AttachState.Attached;
610
744
  this.emit("attached");
611
745
  if (!this.closed) {
612
- this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
746
+ this.resumeInternal({
747
+ fetchOpsFromStorage: false,
748
+ reason: "createDetached",
749
+ });
613
750
  }
614
751
  }
615
752
  catch (error) {
@@ -620,6 +757,7 @@ export class Container extends EventEmitterWithErrorHandling {
620
757
  newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
621
758
  }
622
759
  this.close(newError);
760
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, newError);
623
761
  throw newError;
624
762
  }
625
763
  }, { start: true, end: true, cancel: "generic" });
@@ -708,26 +846,30 @@ export class Container extends EventEmitterWithErrorHandling {
708
846
  throw new Error("Proposed code details should be greater than the current");
709
847
  }
710
848
  }
711
- return this.protocolHandler.quorum.propose("code", codeDetails)
849
+ return this.protocolHandler.quorum
850
+ .propose("code", codeDetails)
712
851
  .then(() => true)
713
852
  .catch(() => false);
714
853
  }
715
854
  async processCodeProposal() {
855
+ var _a;
716
856
  const codeDetails = this.getCodeDetailsFromQuorum();
717
857
  await Promise.all([
718
858
  this.deltaManager.inbound.pause(),
719
- this.deltaManager.inboundSignal.pause()
859
+ this.deltaManager.inboundSignal.pause(),
720
860
  ]);
721
- if ((await this.context.satisfies(codeDetails) === true)) {
861
+ if ((await this.context.satisfies(codeDetails)) === true) {
722
862
  this.deltaManager.inbound.resume();
723
863
  this.deltaManager.inboundSignal.resume();
724
864
  return;
725
865
  }
726
866
  // pre-0.58 error message: existingContextDoesNotSatisfyIncomingProposal
727
- this.close(new GenericError("Existing context does not satisfy incoming proposal"));
867
+ const error = new GenericError("Existing context does not satisfy incoming proposal");
868
+ this.close(error);
869
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error);
728
870
  }
729
871
  async getVersion(version) {
730
- const versions = await this.storageService.getVersions(version, 1);
872
+ const versions = await this.storageAdapter.getVersions(version, 1);
731
873
  return versions[0];
732
874
  }
733
875
  recordConnectStartTime() {
@@ -749,6 +891,7 @@ export class Container extends EventEmitterWithErrorHandling {
749
891
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
750
892
  */
751
893
  async load(specifiedVersion, loadMode, pendingLocalState) {
894
+ var _a;
752
895
  if (this._resolvedUrl === undefined) {
753
896
  throw new Error("Attempting to load without a resolved url");
754
897
  }
@@ -762,32 +905,48 @@ export class Container extends EventEmitterWithErrorHandling {
762
905
  // connections to same file) in two ways:
763
906
  // A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
764
907
  // B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
765
- const connectionArgs = { reason: "DocumentOpen", mode: "write", fetchOpsFromStorage: false };
908
+ const connectionArgs = {
909
+ reason: "DocumentOpen",
910
+ mode: "write",
911
+ fetchOpsFromStorage: false,
912
+ };
766
913
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
767
914
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
768
- if (loadMode.deltaConnection === undefined) {
915
+ if (loadMode.deltaConnection === undefined && !pendingLocalState) {
769
916
  this.connectToDeltaStream(connectionArgs);
770
917
  }
771
918
  if (!pendingLocalState) {
772
- await this.storageService.connectToService(this.service);
919
+ await this.storageAdapter.connectToService(this.service);
773
920
  }
774
921
  else {
775
922
  // if we have pendingLocalState we can load without storage; don't wait for connection
776
- this.storageService.connectToService(this.service).catch((error) => this.close(error));
923
+ this.storageAdapter.connectToService(this.service).catch((error) => {
924
+ var _a;
925
+ this.close(error);
926
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error);
927
+ });
777
928
  }
778
929
  this._attachState = AttachState.Attached;
779
930
  // Fetch specified snapshot.
780
931
  const { snapshot, versionId } = pendingLocalState === undefined
781
932
  ? await this.fetchSnapshotTree(specifiedVersion)
782
- : { snapshot: undefined, versionId: undefined };
783
- assert(snapshot !== undefined || pendingLocalState !== undefined, 0x237 /* "Snapshot should exist" */);
784
- const attributes = pendingLocalState === undefined
785
- ? await this.getDocumentAttributes(this.storageService, snapshot)
786
- : {
787
- sequenceNumber: pendingLocalState.protocol.sequenceNumber,
788
- minimumSequenceNumber: pendingLocalState.protocol.minimumSequenceNumber,
789
- term: pendingLocalState.term,
790
- };
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;
791
950
  let opsBeforeReturnP;
792
951
  // Attach op handlers to finish initialization and be able to start processing ops
793
952
  // Kick off any ops fetching if required.
@@ -795,44 +954,52 @@ export class Container extends EventEmitterWithErrorHandling {
795
954
  case undefined:
796
955
  // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
797
956
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
798
- this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none");
957
+ this.attachDeltaManagerOpHandler(dmAttributes, loadMode.deltaConnection !== "none" ? "all" : "none");
799
958
  break;
800
959
  case "cached":
801
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "cached");
960
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
802
961
  break;
803
962
  case "all":
804
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "all");
963
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
805
964
  break;
806
965
  default:
807
966
  unreachableCase(loadMode.opsBeforeReturn);
808
967
  }
809
968
  // ...load in the existing quorum
810
969
  // Initialize the protocol handler
811
- if (pendingLocalState === undefined) {
812
- await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot);
813
- }
814
- else {
815
- this.initializeProtocolState(attributes, {
816
- members: pendingLocalState.protocol.members,
817
- proposals: pendingLocalState.protocol.proposals,
818
- values: pendingLocalState.protocol.values,
819
- });
820
- }
970
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
821
971
  const codeDetails = this.getCodeDetailsFromQuorum();
822
972
  await this.instantiateContext(true, // existing
823
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
+ }
824
986
  // We might have hit some failure that did not manifest itself in exception in this flow,
825
987
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
826
988
  if (!this.closed) {
827
989
  if (opsBeforeReturnP !== undefined) {
828
990
  this._deltaManager.inbound.resume();
829
- await ReportIfTooLong(this.mc.logger, "WaitOps", async () => { await opsBeforeReturnP; return {}; });
830
- await ReportIfTooLong(this.mc.logger, "WaitOpProcessing", async () => this._deltaManager.inbound.waitTillProcessingDone());
991
+ await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "WaitOps" }, async () => opsBeforeReturnP);
992
+ await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "WaitOpProcessing" }, async () => this._deltaManager.inbound.waitTillProcessingDone());
831
993
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
832
994
  this._deltaManager.inbound.pause();
833
995
  }
834
996
  switch (loadMode.deltaConnection) {
835
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
836
1003
  case "delayed":
837
1004
  assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
838
1005
  this.inboundQueuePausedFromInit = false;
@@ -882,16 +1049,17 @@ export class Container extends EventEmitterWithErrorHandling {
882
1049
  }
883
1050
  async rehydrateDetachedFromSnapshot(detachedContainerSnapshot) {
884
1051
  if (detachedContainerSnapshot.tree[".hasAttachmentBlobs"] !== undefined) {
885
- assert(!!this.loader.services.detachedBlobStorage && this.loader.services.detachedBlobStorage.size > 0, 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */);
1052
+ assert(!!this.loader.services.detachedBlobStorage &&
1053
+ this.loader.services.detachedBlobStorage.size > 0, 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */);
886
1054
  delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
887
1055
  }
888
1056
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
889
- this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
890
- const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1057
+ this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1058
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
891
1059
  await this.attachDeltaManagerOpHandler(attributes);
892
1060
  // Initialize the protocol handler
893
1061
  const baseTree = getProtocolSnapshotTree(snapshotTree);
894
- const qValues = await readAndParse(this.storageService, baseTree.blobs.quorumValues);
1062
+ const qValues = await readAndParse(this.storageAdapter, baseTree.blobs.quorumValues);
895
1063
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
896
1064
  this.initializeProtocolState(attributes, {
897
1065
  members: [],
@@ -929,11 +1097,12 @@ export class Container extends EventEmitterWithErrorHandling {
929
1097
  };
930
1098
  if (snapshot !== undefined) {
931
1099
  const baseTree = getProtocolSnapshotTree(snapshot);
932
- [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
933
- readAndParse(storage, baseTree.blobs.quorumMembers),
934
- readAndParse(storage, baseTree.blobs.quorumProposals),
935
- readAndParse(storage, baseTree.blobs.quorumValues),
936
- ]);
1100
+ [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] =
1101
+ await Promise.all([
1102
+ readAndParse(storage, baseTree.blobs.quorumMembers),
1103
+ readAndParse(storage, baseTree.blobs.quorumProposals),
1104
+ readAndParse(storage, baseTree.blobs.quorumValues),
1105
+ ]);
937
1106
  }
938
1107
  this.initializeProtocolState(attributes, quorumSnapshot);
939
1108
  }
@@ -960,7 +1129,10 @@ export class Container extends EventEmitterWithErrorHandling {
960
1129
  });
961
1130
  }
962
1131
  this.processCodeProposal().catch((error) => {
963
- this.close(normalizeError(error));
1132
+ var _a;
1133
+ const normalizedError = normalizeError(error);
1134
+ this.close(normalizedError);
1135
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, normalizedError);
964
1136
  throw error;
965
1137
  });
966
1138
  }
@@ -1017,7 +1189,10 @@ export class Container extends EventEmitterWithErrorHandling {
1017
1189
  if (this.clientDetailsOverride !== undefined) {
1018
1190
  merge(client.details, this.clientDetailsOverride);
1019
1191
  }
1020
- client.details.environment = [client.details.environment, ` loaderVersion:${pkgVersion}`].join(";");
1192
+ client.details.environment = [
1193
+ client.details.environment,
1194
+ ` loaderVersion:${pkgVersion}`,
1195
+ ].join(";");
1021
1196
  return client;
1022
1197
  }
1023
1198
  /**
@@ -1027,8 +1202,7 @@ export class Container extends EventEmitterWithErrorHandling {
1027
1202
  * If it's not true, runtime is not in position to send ops.
1028
1203
  */
1029
1204
  activeConnection() {
1030
- return this.connectionState === ConnectionState.Connected &&
1031
- this.connectionMode === "write";
1205
+ return (this.connectionState === ConnectionState.Connected && this.connectionMode === "write");
1032
1206
  }
1033
1207
  createDeltaManager() {
1034
1208
  const serviceProvider = () => this.service;
@@ -1045,7 +1219,9 @@ export class Container extends EventEmitterWithErrorHandling {
1045
1219
  deltaManager.on("disconnect", (reason) => {
1046
1220
  var _a;
1047
1221
  (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1048
- this.connectionStateHandler.receivedDisconnectEvent(reason);
1222
+ if (!this.closed) {
1223
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1224
+ }
1049
1225
  });
1050
1226
  deltaManager.on("throttled", (warning) => {
1051
1227
  const warn = warning;
@@ -1063,6 +1239,9 @@ export class Container extends EventEmitterWithErrorHandling {
1063
1239
  deltaManager.on("closed", (error) => {
1064
1240
  this.closeCore(error);
1065
1241
  });
1242
+ deltaManager.on("disposed", (error) => {
1243
+ this.disposeCore(error);
1244
+ });
1066
1245
  return deltaManager;
1067
1246
  }
1068
1247
  async attachDeltaManagerOpHandler(attributes, prefetchType) {
@@ -1090,7 +1269,8 @@ export class Container extends EventEmitterWithErrorHandling {
1090
1269
  }
1091
1270
  else {
1092
1271
  if (value === ConnectionState.Connected) {
1093
- durationFromDisconnected = time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1272
+ durationFromDisconnected =
1273
+ time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1094
1274
  durationFromDisconnected = TelemetryLogger.formatTick(durationFromDisconnected);
1095
1275
  }
1096
1276
  else {
@@ -1133,32 +1313,39 @@ export class Container extends EventEmitterWithErrorHandling {
1133
1313
  this.protocolHandler.setConnectionState(state, this.clientId);
1134
1314
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1135
1315
  if (logOpsOnReconnect) {
1136
- this.mc.logger.sendTelemetryEvent({ eventName: "OpsSentOnReconnect", count: this.messageCountAfterDisconnection });
1316
+ this.mc.logger.sendTelemetryEvent({
1317
+ eventName: "OpsSentOnReconnect",
1318
+ count: this.messageCountAfterDisconnection,
1319
+ });
1137
1320
  }
1138
1321
  }
1139
1322
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1140
1323
  submitContainerMessage(type, contents, batch, metadata) {
1324
+ var _a;
1141
1325
  switch (type) {
1142
1326
  case MessageType.Operation:
1143
1327
  return this.submitMessage(type, JSON.stringify(contents), batch, metadata);
1144
1328
  case MessageType.Summarize:
1145
1329
  return this.submitSummaryMessage(contents);
1146
- default:
1147
- this.close(new GenericError("invalidContainerSubmitOpType", undefined /* error */, { messageType: type }));
1330
+ default: {
1331
+ const newError = new GenericError("invalidContainerSubmitOpType", undefined /* error */, { messageType: type });
1332
+ this.close(newError);
1333
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, newError);
1148
1334
  return -1;
1335
+ }
1149
1336
  }
1150
1337
  }
1151
1338
  /** @returns clientSequenceNumber of last message in a batch */
1152
- submitBatch(batch) {
1339
+ submitBatch(batch, referenceSequenceNumber) {
1153
1340
  let clientSequenceNumber = -1;
1154
1341
  for (const message of batch) {
1155
1342
  clientSequenceNumber = this.submitMessage(MessageType.Operation, message.contents, true, // batch
1156
- message.metadata, message.compression);
1343
+ message.metadata, message.compression, referenceSequenceNumber);
1157
1344
  }
1158
1345
  this._deltaManager.flush();
1159
1346
  return clientSequenceNumber;
1160
1347
  }
1161
- submitSummaryMessage(summary) {
1348
+ submitSummaryMessage(summary, referenceSequenceNumber) {
1162
1349
  // github #6451: this is only needed for staging so the server
1163
1350
  // know when the protocol tree is included
1164
1351
  // this can be removed once all clients send
@@ -1166,11 +1353,10 @@ export class Container extends EventEmitterWithErrorHandling {
1166
1353
  if (summary.details === undefined) {
1167
1354
  summary.details = {};
1168
1355
  }
1169
- summary.details.includesProtocolTree =
1170
- this.options.summarizeProtocolTree === true;
1171
- 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);
1172
1358
  }
1173
- submitMessage(type, contents, batch, metadata, compression) {
1359
+ submitMessage(type, contents, batch, metadata, compression, referenceSequenceNumber) {
1174
1360
  var _a;
1175
1361
  if (this.connectionState !== ConnectionState.Connected) {
1176
1362
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
@@ -1178,9 +1364,12 @@ export class Container extends EventEmitterWithErrorHandling {
1178
1364
  }
1179
1365
  this.messageCountAfterDisconnection += 1;
1180
1366
  (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1181
- return this._deltaManager.submit(type, contents, batch, metadata, compression);
1367
+ return this._deltaManager.submit(type, contents, batch, metadata, compression, referenceSequenceNumber);
1182
1368
  }
1183
1369
  processRemoteMessage(message) {
1370
+ if (this.offlineLoadEnabled) {
1371
+ this.savedOps.push(message);
1372
+ }
1184
1373
  const local = this.clientId === message.clientId;
1185
1374
  // Allow the protocol handler to process the message
1186
1375
  const result = this.protocolHandler.processMessage(message, local);
@@ -1226,10 +1415,13 @@ export class Container extends EventEmitterWithErrorHandling {
1226
1415
  const version = await this.getVersion(specifiedVersion !== null && specifiedVersion !== void 0 ? specifiedVersion : null);
1227
1416
  if (version === undefined && specifiedVersion !== undefined) {
1228
1417
  // We should have a defined version to load from if specified version requested
1229
- this.mc.logger.sendErrorEvent({ eventName: "NoVersionFoundWhenSpecified", id: specifiedVersion });
1418
+ this.mc.logger.sendErrorEvent({
1419
+ eventName: "NoVersionFoundWhenSpecified",
1420
+ id: specifiedVersion,
1421
+ });
1230
1422
  }
1231
1423
  this._loadedFromVersion = version;
1232
- 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;
1233
1425
  if (snapshot === undefined && version !== undefined) {
1234
1426
  this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1235
1427
  }
@@ -1248,7 +1440,7 @@ export class Container extends EventEmitterWithErrorHandling {
1248
1440
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
1249
1441
  // are set. Global requests will still go directly to the loader
1250
1442
  const loader = new RelativeLoader(this, this.loader);
1251
- 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) => 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);
1252
1444
  this.emit("contextChanged", codeDetails);
1253
1445
  }
1254
1446
  updateDirtyContainerState(dirty) {