@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/dist/container.js CHANGED
@@ -71,8 +71,8 @@ async function waitContainerToCatchUp(container) {
71
71
  // Waiting for "connected" state in either case gets us at least to our own Join op
72
72
  // which is a reasonable approximation of "caught up"
73
73
  const waitForOps = () => {
74
- (0, common_utils_1.assert)(container.connectionState === connectionState_1.ConnectionState.CatchingUp
75
- || container.connectionState === connectionState_1.ConnectionState.Connected, 0x0cd /* "Container disconnected while waiting for ops!" */);
74
+ (0, common_utils_1.assert)(container.connectionState === connectionState_1.ConnectionState.CatchingUp ||
75
+ container.connectionState === connectionState_1.ConnectionState.Connected, 0x0cd /* "Container disconnected while waiting for ops!" */);
76
76
  const hasCheckpointSequenceNumber = deltaManager.hasCheckpointSequenceNumber;
77
77
  const connectionOpSeqNumber = deltaManager.lastKnownSeqNumber;
78
78
  (0, common_utils_1.assert)(deltaManager.lastSequenceNumber <= connectionOpSeqNumber, 0x266 /* "lastKnownSeqNumber should never be below last processed sequence number" */);
@@ -113,7 +113,7 @@ const getCodeProposal =
113
113
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
114
114
  (quorum) => { var _a; return (_a = quorum.get("code")) !== null && _a !== void 0 ? _a : quorum.get("code2"); };
115
115
  /**
116
- * Helper function to report to telemetry cases where operation takes longer than expected (1s)
116
+ * Helper function to report to telemetry cases where operation takes longer than expected (200ms)
117
117
  * @param logger - logger to use
118
118
  * @param eventName - event name
119
119
  * @param action - functor to call and measure
@@ -127,9 +127,15 @@ async function ReportIfTooLong(logger, eventName, action) {
127
127
  }
128
128
  exports.ReportIfTooLong = ReportIfTooLong;
129
129
  const summarizerClientType = "summarizer";
130
+ /**
131
+ * @deprecated - In the next release Container will no longer be exported, IContainer should be used in its place.
132
+ */
130
133
  class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
134
+ /**
135
+ * @internal
136
+ */
131
137
  constructor(loader, config, protocolHandlerBuilder) {
132
- var _a, _b;
138
+ var _a, _b, _c;
133
139
  super((name, error) => {
134
140
  this.mc.logger.sendErrorEvent({
135
141
  eventName: "ContainerEventHandlerException",
@@ -141,6 +147,21 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
141
147
  // Tells if container can reconnect on losing fist connection
142
148
  // If false, container gets closed on loss of connection.
143
149
  this._canReconnect = true;
150
+ /**
151
+ * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
152
+ *
153
+ * States are allowed to progress to further states:
154
+ * "loading" - "loaded" - "closing" - "disposing" - "closed" - "disposed"
155
+ *
156
+ * For example, moving from "closed" to "disposing" is not allowed since it is an earlier state.
157
+ *
158
+ * loading: Container has been created, but is not yet in normal/loaded state
159
+ * loaded: Container is in normal/loaded state
160
+ * closing: Container has started closing process (for re-entrancy prevention)
161
+ * disposing: Container has started disposing process (for re-entrancy prevention)
162
+ * closed: Container has closed
163
+ * disposed: Container has been disposed
164
+ */
144
165
  this._lifecycleState = "loading";
145
166
  this._attachState = container_definitions_1.AttachState.Detached;
146
167
  /** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
@@ -150,7 +171,9 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
150
171
  this.messageCountAfterDisconnection = 0;
151
172
  this.attachStarted = false;
152
173
  this._dirtyContainer = false;
174
+ this.savedOps = [];
153
175
  this.setAutoReconnectTime = common_utils_1.performance.now();
176
+ this._disposed = false;
154
177
  this.clientDetailsOverride = config.clientDetailsOverride;
155
178
  this._resolvedUrl = config.resolvedUrl;
156
179
  if (config.canReconnect !== undefined) {
@@ -187,15 +210,14 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
187
210
  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; },
188
211
  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; },
189
212
  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; },
213
+ 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; },
190
214
  connectionStateDuration: () => common_utils_1.performance.now() - this.connectionTransitionTimes[this.connectionState],
191
215
  },
192
216
  });
193
217
  // Prefix all events in this file with container-loader
194
218
  this.mc = (0, telemetry_utils_1.loggerToMonitoringContext)(telemetry_utils_1.ChildLogger.create(this.subLogger, "Container"));
195
- const summarizeProtocolTree = (_a = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")) !== null && _a !== void 0 ? _a : this.loader.services.options.summarizeProtocolTree;
196
- this.options = Object.assign(Object.assign({}, this.loader.services.options), { summarizeProtocolTree });
219
+ this.options = Object.assign({}, this.loader.services.options);
197
220
  this._deltaManager = this.createDeltaManager();
198
- this._clientId = (_b = config.serializedContainerState) === null || _b === void 0 ? void 0 : _b.clientId;
199
221
  this.connectionStateHandler = (0, connectionStateHandler_1.createConnectionStateHandler)({
200
222
  logger: this.mc.logger,
201
223
  connectionStateChanged: (value, oldState, reason) => {
@@ -204,17 +226,23 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
204
226
  }
205
227
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
206
228
  if (this._lifecycleState === "loaded") {
207
- this.propagateConnectionState(false /* initial transition */, value === connectionState_1.ConnectionState.Disconnected ? reason : undefined /* disconnectedReason */);
229
+ this.propagateConnectionState(false /* initial transition */, value === connectionState_1.ConnectionState.Disconnected
230
+ ? reason
231
+ : undefined /* disconnectedReason */);
208
232
  }
209
233
  },
210
234
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
211
235
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
212
- logConnectionIssue: (eventName, details) => {
236
+ logConnectionIssue: (eventName, category, details) => {
213
237
  const mode = this.connectionMode;
214
238
  // We get here when socket does not receive any ops on "write" connection, including
215
- // its own join op. Attempt recovery option.
239
+ // its own join op.
240
+ // Report issues only if we already loaded container - op processing is paused while container is loading,
241
+ // so we always time-out processing of join op in cases where fetching snapshot takes a minute.
242
+ // It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
216
243
  this._deltaManager.logConnectionIssue(Object.assign({ eventName,
217
- mode, duration: common_utils_1.performance.now() - this.connectionTransitionTimes[connectionState_1.ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
244
+ mode, category: this._lifecycleState === "loading" ? "generic" : category, duration: common_utils_1.performance.now() -
245
+ this.connectionTransitionTimes[connectionState_1.ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
218
246
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
219
247
  // to very slow op fetches and we will eventually get there.
220
248
  // For "read" connections, we get here due to self join signal not arriving on time. We will need to
@@ -228,13 +256,20 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
228
256
  this.connect();
229
257
  }
230
258
  },
231
- }, this.deltaManager, this._clientId);
259
+ }, this.deltaManager, (_a = config.serializedContainerState) === null || _a === void 0 ? void 0 : _a.clientId);
232
260
  this.on(savedContainerEvent, () => {
233
261
  this.connectionStateHandler.containerSaved();
234
262
  });
235
- this.storageService = new containerStorageAdapter_1.ContainerStorageAdapter(this.loader.services.detachedBlobStorage, this.mc.logger, this.options.summarizeProtocolTree === true
236
- ? () => this.captureProtocolSummary()
237
- : undefined);
263
+ // We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
264
+ // non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
265
+ // using this callback and fix them up.
266
+ const addProtocolSummaryIfMissing = (summaryTree) => (0, driver_utils_1.isCombinedAppAndProtocolSummary)(summaryTree) === true
267
+ ? summaryTree
268
+ : (0, driver_utils_1.combineAppAndProtocolSummary)(summaryTree, this.captureProtocolSummary());
269
+ // Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
270
+ // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
271
+ const forceEnableSummarizeProtocolTree = (_b = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")) !== null && _b !== void 0 ? _b : this.loader.services.options.summarizeProtocolTree;
272
+ this.storageAdapter = new containerStorageAdapter_1.ContainerStorageAdapter(this.loader.services.detachedBlobStorage, this.mc.logger, (_c = config.serializedContainerState) === null || _c === void 0 ? void 0 : _c.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
238
273
  const isDomAvailable = typeof document === "object" &&
239
274
  document !== null &&
240
275
  typeof document.addEventListener === "function" &&
@@ -248,48 +283,17 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
248
283
  }
249
284
  else {
250
285
  // settimeout so this will hopefully fire after disconnect event if being hidden caused it
251
- setTimeout(() => { this.lastVisible = undefined; }, 0);
286
+ setTimeout(() => {
287
+ this.lastVisible = undefined;
288
+ }, 0);
252
289
  }
253
290
  };
254
291
  document.addEventListener("visibilitychange", this.visibilityEventHandler);
255
292
  }
256
- // We observed that most users of platform do not check Container.connected event on load, causing bugs.
257
- // As such, we are raising events when new listener pops up.
258
- // Note that we can raise both "disconnected" & "connect" events at the same time,
259
- // if we are in connecting stage.
260
- this.on("newListener", (event, listener) => {
261
- // Fire events on the end of JS turn, giving a chance for caller to be in consistent state.
262
- Promise.resolve().then(() => {
263
- switch (event) {
264
- case dirtyContainerEvent:
265
- if (this._dirtyContainer) {
266
- listener();
267
- }
268
- break;
269
- case savedContainerEvent:
270
- if (!this._dirtyContainer) {
271
- listener();
272
- }
273
- break;
274
- case telemetry_utils_1.connectedEventName:
275
- if (this.connected) {
276
- listener(this.clientId);
277
- }
278
- break;
279
- case telemetry_utils_1.disconnectedEventName:
280
- if (!this.connected) {
281
- listener();
282
- }
283
- break;
284
- default:
285
- }
286
- }).catch((error) => {
287
- this.mc.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
288
- });
289
- });
290
293
  }
291
294
  /**
292
295
  * Load an existing container.
296
+ * @internal
293
297
  */
294
298
  static async load(loader, loadOptions, pendingLocalState, protocolHandlerBuilder) {
295
299
  const container = new Container(loader, {
@@ -311,7 +315,8 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
311
315
  reject(err !== null && err !== void 0 ? err : new container_utils_1.GenericError("Container closed without error during load"));
312
316
  };
313
317
  container.on("closed", onClosed);
314
- container.load(version, mode, pendingLocalState)
318
+ container
319
+ .load(version, mode, pendingLocalState)
315
320
  .finally(() => {
316
321
  container.removeListener("closed", onClosed);
317
322
  })
@@ -360,10 +365,13 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
360
365
  }
361
366
  }
362
367
  get closed() {
363
- return (this._lifecycleState === "closing" || this._lifecycleState === "closed");
368
+ return (this._lifecycleState === "closing" ||
369
+ this._lifecycleState === "closed" ||
370
+ this._lifecycleState === "disposing" ||
371
+ this._lifecycleState === "disposed");
364
372
  }
365
373
  get storage() {
366
- return this.storageService;
374
+ return this.storageAdapter;
367
375
  }
368
376
  get context() {
369
377
  if (this._context === undefined) {
@@ -377,8 +385,12 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
377
385
  }
378
386
  return this._protocolHandler;
379
387
  }
380
- get connectionMode() { return this._deltaManager.connectionManager.connectionMode; }
381
- get IFluidRouter() { return this; }
388
+ get connectionMode() {
389
+ return this._deltaManager.connectionManager.connectionMode;
390
+ }
391
+ get IFluidRouter() {
392
+ return this;
393
+ }
382
394
  get resolvedUrl() {
383
395
  return this._resolvedUrl;
384
396
  }
@@ -430,6 +442,12 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
430
442
  get clientDetails() {
431
443
  return this._deltaManager.clientDetails;
432
444
  }
445
+ get offlineLoadEnabled() {
446
+ var _a;
447
+ // summarizer will not have any pending state we want to save
448
+ return (((_a = this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad")) !== null && _a !== void 0 ? _a : false) &&
449
+ this.clientDetails.capabilities.interactive);
450
+ }
433
451
  /**
434
452
  * Get the code details that are currently specified for the container.
435
453
  * @returns The current code details if any are specified, undefined if none are specified.
@@ -460,24 +478,71 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
460
478
  get isDirty() {
461
479
  return this._dirtyContainer;
462
480
  }
463
- get serviceFactory() { return this.loader.services.documentServiceFactory; }
464
- get urlResolver() { return this.loader.services.urlResolver; }
465
- get scope() { return this.loader.services.scope; }
466
- get codeLoader() { return this.loader.services.codeLoader; }
481
+ get serviceFactory() {
482
+ return this.loader.services.documentServiceFactory;
483
+ }
484
+ get urlResolver() {
485
+ return this.loader.services.urlResolver;
486
+ }
487
+ get scope() {
488
+ return this.loader.services.scope;
489
+ }
490
+ get codeLoader() {
491
+ return this.loader.services.codeLoader;
492
+ }
493
+ /**
494
+ * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
495
+ */
496
+ async getEntryPoint() {
497
+ var _a, _b;
498
+ // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
499
+ // allow it since they mean a kind of read-only state for the Container.
500
+ // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
501
+ if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
502
+ throw new container_utils_1.UsageError("The container is disposing or disposed");
503
+ }
504
+ while (this._context === undefined) {
505
+ await new Promise((resolve, reject) => {
506
+ const contextChangedHandler = () => {
507
+ resolve();
508
+ this.off("disposed", disposedHandler);
509
+ };
510
+ const disposedHandler = (error) => {
511
+ reject(error !== null && error !== void 0 ? error : "The Container is disposed");
512
+ this.off("contextChanged", contextChangedHandler);
513
+ };
514
+ this.once("contextChanged", contextChangedHandler);
515
+ this.once("disposed", disposedHandler);
516
+ });
517
+ // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
518
+ // should have set this._context; making sure.
519
+ (0, common_utils_1.assert)(this._context !== undefined, 0x5a2 /* Context still not defined after contextChanged event */);
520
+ }
521
+ // Disable lint rule for the sake of more complete stack traces
522
+ // eslint-disable-next-line no-return-await
523
+ return await ((_b = (_a = this._context).getEntryPoint) === null || _b === void 0 ? void 0 : _b.call(_a));
524
+ }
467
525
  /**
468
526
  * Retrieves the quorum associated with the document
469
527
  */
470
528
  getQuorum() {
471
529
  return this.protocolHandler.quorum;
472
530
  }
531
+ dispose(error) {
532
+ this._deltaManager.close(error, true /* doDispose */);
533
+ this.verifyClosed();
534
+ }
473
535
  close(error) {
474
536
  // 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
475
537
  // 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
476
538
  // handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
477
539
  // "closing" will lose that info (can also solve by tracking extra state).
478
540
  this._deltaManager.close(error);
541
+ this.verifyClosed();
542
+ }
543
+ verifyClosed() {
479
544
  (0, common_utils_1.assert)(this.connectionState === connectionState_1.ConnectionState.Disconnected, 0x0cf /* "disconnect event was not raised!" */);
480
- (0, common_utils_1.assert)(this._lifecycleState === "closed", 0x314 /* Container properly closed */);
545
+ (0, common_utils_1.assert)(this._lifecycleState === "closed" || this._lifecycleState === "disposed", 0x314 /* Container properly closed */);
481
546
  }
482
547
  closeCore(error) {
483
548
  var _a, _b, _c;
@@ -487,15 +552,19 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
487
552
  try {
488
553
  // Raise event first, to ensure we capture _lifecycleState before transition.
489
554
  // This gives us a chance to know what errors happened on open vs. on fully loaded container.
555
+ // Log generic events instead of error events if container is in loading state, as most errors are not really FF errors
556
+ // which can pollute telemetry for real bugs
490
557
  this.mc.logger.sendTelemetryEvent({
491
558
  eventName: "ContainerClose",
492
- category: error === undefined ? "generic" : "error",
559
+ category: this._lifecycleState !== "loading" && error !== undefined
560
+ ? "error"
561
+ : "generic",
493
562
  }, error);
494
563
  this._lifecycleState = "closing";
495
564
  (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
496
565
  this.connectionStateHandler.dispose();
497
566
  (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
498
- this.storageService.dispose();
567
+ this.storageAdapter.dispose();
499
568
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
500
569
  // about file, like file being overwritten in storage, but client having stale local cache.
501
570
  // Driver need to ensure all caches are cleared on critical errors
@@ -505,7 +574,6 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
505
574
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
506
575
  }
507
576
  this.emit("closed", error);
508
- this.removeAllListeners();
509
577
  if (this.visibilityEventHandler !== undefined) {
510
578
  document.removeEventListener("visibilitychange", this.visibilityEventHandler);
511
579
  }
@@ -514,22 +582,69 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
514
582
  this._lifecycleState = "closed";
515
583
  }
516
584
  }
585
+ disposeCore(error) {
586
+ var _a, _b, _c;
587
+ (0, common_utils_1.assert)(!this._disposed, 0x54c /* Container already disposed */);
588
+ this._disposed = true;
589
+ try {
590
+ // Ensure that we raise all key events even if one of these throws
591
+ try {
592
+ // Raise event first, to ensure we capture _lifecycleState before transition.
593
+ // This gives us a chance to know what errors happened on open vs. on fully loaded container.
594
+ this.mc.logger.sendTelemetryEvent({
595
+ eventName: "ContainerDispose",
596
+ category: "generic",
597
+ }, error);
598
+ // ! Progressing from "closed" to "disposing" is not allowed
599
+ if (this._lifecycleState !== "closed") {
600
+ this._lifecycleState = "disposing";
601
+ }
602
+ (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
603
+ this.connectionStateHandler.dispose();
604
+ (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
605
+ this.storageAdapter.dispose();
606
+ // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
607
+ // about file, like file being overwritten in storage, but client having stale local cache.
608
+ // Driver need to ensure all caches are cleared on critical errors
609
+ (_c = this.service) === null || _c === void 0 ? void 0 : _c.dispose(error);
610
+ }
611
+ catch (exception) {
612
+ this.mc.logger.sendErrorEvent({ eventName: "ContainerDisposeException" }, exception);
613
+ }
614
+ this.emit("disposed", error);
615
+ this.removeAllListeners();
616
+ if (this.visibilityEventHandler !== undefined) {
617
+ document.removeEventListener("visibilitychange", this.visibilityEventHandler);
618
+ }
619
+ }
620
+ finally {
621
+ this._lifecycleState = "disposed";
622
+ }
623
+ }
517
624
  closeAndGetPendingLocalState() {
518
625
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
519
626
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
520
627
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
628
+ if (!this.offlineLoadEnabled) {
629
+ throw new container_utils_1.UsageError("Can't get pending local state unless offline load is enabled");
630
+ }
521
631
  (0, common_utils_1.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
522
632
  (0, common_utils_1.assert)(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
523
633
  (0, common_utils_1.assert)(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
524
634
  (0, common_utils_1.assert)(this._protocolHandler.attributes.term !== undefined, 0x37e /* Must have a valid protocol handler instance */);
635
+ (0, common_utils_1.assert)(!!this.baseSnapshot, "no base snapshot");
636
+ (0, common_utils_1.assert)(!!this.baseSnapshotBlobs, "no snapshot blobs");
525
637
  const pendingState = {
526
638
  pendingRuntimeState: this.context.getPendingLocalState(),
639
+ baseSnapshot: this.baseSnapshot,
640
+ snapshotBlobs: this.baseSnapshotBlobs,
641
+ savedOps: this.savedOps,
527
642
  url: this.resolvedUrl.url,
528
- protocol: this.protocolHandler.getProtocolState(),
529
643
  term: this._protocolHandler.attributes.term,
530
644
  clientId: this.clientId,
531
645
  };
532
646
  this.mc.logger.sendTelemetryEvent({ eventName: "CloseAndGetPendingLocalState" });
647
+ // Only close here as method name suggests
533
648
  this.close();
534
649
  return JSON.stringify(pendingState);
535
650
  }
@@ -541,13 +656,18 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
541
656
  const appSummary = this.context.createSummary();
542
657
  const protocolSummary = this.captureProtocolSummary();
543
658
  const combinedSummary = (0, driver_utils_1.combineAppAndProtocolSummary)(appSummary, protocolSummary);
544
- if (this.loader.services.detachedBlobStorage && this.loader.services.detachedBlobStorage.size > 0) {
545
- combinedSummary.tree[".hasAttachmentBlobs"] = { type: protocol_definitions_1.SummaryType.Blob, content: "true" };
659
+ if (this.loader.services.detachedBlobStorage &&
660
+ this.loader.services.detachedBlobStorage.size > 0) {
661
+ combinedSummary.tree[".hasAttachmentBlobs"] = {
662
+ type: protocol_definitions_1.SummaryType.Blob,
663
+ content: "true",
664
+ };
546
665
  }
547
666
  return JSON.stringify(combinedSummary);
548
667
  }
549
668
  async attach(request) {
550
669
  await telemetry_utils_1.PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
670
+ var _a;
551
671
  if (this._lifecycleState !== "loaded") {
552
672
  // pre-0.58 error message: containerNotValidForAttach
553
673
  throw new container_utils_1.UsageError(`The Container is not in a valid state for attach [${this._lifecycleState}]`);
@@ -556,8 +676,8 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
556
676
  (0, common_utils_1.assert)(this._attachState === container_definitions_1.AttachState.Detached && !this.attachStarted, 0x205 /* "attach() called more than once" */);
557
677
  this.attachStarted = true;
558
678
  // If attachment blobs were uploaded in detached state we will go through a different attach flow
559
- const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined
560
- && this.loader.services.detachedBlobStorage.size > 0;
679
+ const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined &&
680
+ this.loader.services.detachedBlobStorage.size > 0;
561
681
  try {
562
682
  (0, common_utils_1.assert)(this.deltaManager.inbound.length === 0, 0x0d6 /* "Inbound queue should be empty when attaching" */);
563
683
  let summary;
@@ -572,7 +692,13 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
572
692
  // starting to attach the container to storage.
573
693
  // Also, this should only be fired in detached container.
574
694
  this._attachState = container_definitions_1.AttachState.Attaching;
575
- this.context.notifyAttaching((0, utils_1.getSnapshotTreeFromSerializedContainer)(summary));
695
+ this.emit("attaching");
696
+ if (this.offlineLoadEnabled) {
697
+ const snapshot = (0, utils_1.getSnapshotTreeFromSerializedContainer)(summary);
698
+ this.baseSnapshot = snapshot;
699
+ this.baseSnapshotBlobs =
700
+ (0, containerStorageAdapter_1.getBlobContentsFromTreeWithBlobContents)(snapshot);
701
+ }
576
702
  }
577
703
  // Actually go and create the resolved document
578
704
  const createNewResolvedUrl = await this.urlResolver.resolve(request);
@@ -586,7 +712,7 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
586
712
  const resolvedUrl = this.service.resolvedUrl;
587
713
  (0, driver_utils_1.ensureFluidResolvedUrl)(resolvedUrl);
588
714
  this._resolvedUrl = resolvedUrl;
589
- await this.storageService.connectToService(this.service);
715
+ await this.storageAdapter.connectToService(this.service);
590
716
  if (hasAttachmentBlobs) {
591
717
  // upload blobs to storage
592
718
  (0, common_utils_1.assert)(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
@@ -595,10 +721,12 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
595
721
  const redirectTable = new Map();
596
722
  // if new blobs are added while uploading, upload them too
597
723
  while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
598
- const newIds = this.loader.services.detachedBlobStorage.getBlobIds().filter((id) => !redirectTable.has(id));
724
+ const newIds = this.loader.services.detachedBlobStorage
725
+ .getBlobIds()
726
+ .filter((id) => !redirectTable.has(id));
599
727
  for (const id of newIds) {
600
728
  const blob = await this.loader.services.detachedBlobStorage.readBlob(id);
601
- const response = await this.storageService.createBlob(blob);
729
+ const response = await this.storageAdapter.createBlob(blob);
602
730
  redirectTable.set(id, response.id);
603
731
  }
604
732
  }
@@ -607,8 +735,14 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
607
735
  const protocolSummary = this.captureProtocolSummary();
608
736
  summary = (0, driver_utils_1.combineAppAndProtocolSummary)(appSummary, protocolSummary);
609
737
  this._attachState = container_definitions_1.AttachState.Attaching;
610
- this.context.notifyAttaching((0, utils_1.getSnapshotTreeFromSerializedContainer)(summary));
611
- await this.storageService.uploadSummaryWithContext(summary, {
738
+ this.emit("attaching");
739
+ if (this.offlineLoadEnabled) {
740
+ const snapshot = (0, utils_1.getSnapshotTreeFromSerializedContainer)(summary);
741
+ this.baseSnapshot = snapshot;
742
+ this.baseSnapshotBlobs =
743
+ (0, containerStorageAdapter_1.getBlobContentsFromTreeWithBlobContents)(snapshot);
744
+ }
745
+ await this.storageAdapter.uploadSummaryWithContext(summary, {
612
746
  referenceSequenceNumber: 0,
613
747
  ackHandle: undefined,
614
748
  proposalHandle: undefined,
@@ -617,7 +751,10 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
617
751
  this._attachState = container_definitions_1.AttachState.Attached;
618
752
  this.emit("attached");
619
753
  if (!this.closed) {
620
- this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
754
+ this.resumeInternal({
755
+ fetchOpsFromStorage: false,
756
+ reason: "createDetached",
757
+ });
621
758
  }
622
759
  }
623
760
  catch (error) {
@@ -628,6 +765,7 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
628
765
  newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
629
766
  }
630
767
  this.close(newError);
768
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, newError);
631
769
  throw newError;
632
770
  }
633
771
  }, { start: true, end: true, cancel: "generic" });
@@ -716,26 +854,30 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
716
854
  throw new Error("Proposed code details should be greater than the current");
717
855
  }
718
856
  }
719
- return this.protocolHandler.quorum.propose("code", codeDetails)
857
+ return this.protocolHandler.quorum
858
+ .propose("code", codeDetails)
720
859
  .then(() => true)
721
860
  .catch(() => false);
722
861
  }
723
862
  async processCodeProposal() {
863
+ var _a;
724
864
  const codeDetails = this.getCodeDetailsFromQuorum();
725
865
  await Promise.all([
726
866
  this.deltaManager.inbound.pause(),
727
- this.deltaManager.inboundSignal.pause()
867
+ this.deltaManager.inboundSignal.pause(),
728
868
  ]);
729
- if ((await this.context.satisfies(codeDetails) === true)) {
869
+ if ((await this.context.satisfies(codeDetails)) === true) {
730
870
  this.deltaManager.inbound.resume();
731
871
  this.deltaManager.inboundSignal.resume();
732
872
  return;
733
873
  }
734
874
  // pre-0.58 error message: existingContextDoesNotSatisfyIncomingProposal
735
- this.close(new container_utils_1.GenericError("Existing context does not satisfy incoming proposal"));
875
+ const error = new container_utils_1.GenericError("Existing context does not satisfy incoming proposal");
876
+ this.close(error);
877
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error);
736
878
  }
737
879
  async getVersion(version) {
738
- const versions = await this.storageService.getVersions(version, 1);
880
+ const versions = await this.storageAdapter.getVersions(version, 1);
739
881
  return versions[0];
740
882
  }
741
883
  recordConnectStartTime() {
@@ -757,6 +899,7 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
757
899
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
758
900
  */
759
901
  async load(specifiedVersion, loadMode, pendingLocalState) {
902
+ var _a;
760
903
  if (this._resolvedUrl === undefined) {
761
904
  throw new Error("Attempting to load without a resolved url");
762
905
  }
@@ -770,32 +913,48 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
770
913
  // connections to same file) in two ways:
771
914
  // A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
772
915
  // B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
773
- const connectionArgs = { reason: "DocumentOpen", mode: "write", fetchOpsFromStorage: false };
916
+ const connectionArgs = {
917
+ reason: "DocumentOpen",
918
+ mode: "write",
919
+ fetchOpsFromStorage: false,
920
+ };
774
921
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
775
922
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
776
- if (loadMode.deltaConnection === undefined) {
923
+ if (loadMode.deltaConnection === undefined && !pendingLocalState) {
777
924
  this.connectToDeltaStream(connectionArgs);
778
925
  }
779
926
  if (!pendingLocalState) {
780
- await this.storageService.connectToService(this.service);
927
+ await this.storageAdapter.connectToService(this.service);
781
928
  }
782
929
  else {
783
930
  // if we have pendingLocalState we can load without storage; don't wait for connection
784
- this.storageService.connectToService(this.service).catch((error) => this.close(error));
931
+ this.storageAdapter.connectToService(this.service).catch((error) => {
932
+ var _a;
933
+ this.close(error);
934
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, error);
935
+ });
785
936
  }
786
937
  this._attachState = container_definitions_1.AttachState.Attached;
787
938
  // Fetch specified snapshot.
788
939
  const { snapshot, versionId } = pendingLocalState === undefined
789
940
  ? await this.fetchSnapshotTree(specifiedVersion)
790
- : { snapshot: undefined, versionId: undefined };
791
- (0, common_utils_1.assert)(snapshot !== undefined || pendingLocalState !== undefined, 0x237 /* "Snapshot should exist" */);
792
- const attributes = pendingLocalState === undefined
793
- ? await this.getDocumentAttributes(this.storageService, snapshot)
794
- : {
795
- sequenceNumber: pendingLocalState.protocol.sequenceNumber,
796
- minimumSequenceNumber: pendingLocalState.protocol.minimumSequenceNumber,
797
- term: pendingLocalState.term,
798
- };
941
+ : { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
942
+ if (pendingLocalState) {
943
+ this.baseSnapshot = pendingLocalState.baseSnapshot;
944
+ this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
945
+ }
946
+ else {
947
+ (0, common_utils_1.assert)(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
948
+ if (this.offlineLoadEnabled) {
949
+ this.baseSnapshot = snapshot;
950
+ // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
951
+ this.baseSnapshotBlobs = await (0, containerStorageAdapter_1.getBlobContentsFromTree)(snapshot, this.storage);
952
+ }
953
+ }
954
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshot);
955
+ // If we saved ops, we will replay them and don't need DeltaManager to fetch them
956
+ const sequenceNumber = (_a = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.savedOps[pendingLocalState.savedOps.length - 1]) === null || _a === void 0 ? void 0 : _a.sequenceNumber;
957
+ const dmAttributes = sequenceNumber !== undefined ? Object.assign(Object.assign({}, attributes), { sequenceNumber }) : attributes;
799
958
  let opsBeforeReturnP;
800
959
  // Attach op handlers to finish initialization and be able to start processing ops
801
960
  // Kick off any ops fetching if required.
@@ -803,44 +962,52 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
803
962
  case undefined:
804
963
  // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
805
964
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
806
- this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none");
965
+ this.attachDeltaManagerOpHandler(dmAttributes, loadMode.deltaConnection !== "none" ? "all" : "none");
807
966
  break;
808
967
  case "cached":
809
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "cached");
968
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
810
969
  break;
811
970
  case "all":
812
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "all");
971
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
813
972
  break;
814
973
  default:
815
974
  (0, common_utils_1.unreachableCase)(loadMode.opsBeforeReturn);
816
975
  }
817
976
  // ...load in the existing quorum
818
977
  // Initialize the protocol handler
819
- if (pendingLocalState === undefined) {
820
- await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot);
821
- }
822
- else {
823
- this.initializeProtocolState(attributes, {
824
- members: pendingLocalState.protocol.members,
825
- proposals: pendingLocalState.protocol.proposals,
826
- values: pendingLocalState.protocol.values,
827
- });
828
- }
978
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
829
979
  const codeDetails = this.getCodeDetailsFromQuorum();
830
980
  await this.instantiateContext(true, // existing
831
981
  codeDetails, snapshot, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.pendingRuntimeState);
982
+ // replay saved ops
983
+ if (pendingLocalState) {
984
+ for (const message of pendingLocalState.savedOps) {
985
+ this.processRemoteMessage(message);
986
+ // allow runtime to apply stashed ops at this op's sequence number
987
+ await this.context.notifyOpReplay(message);
988
+ }
989
+ pendingLocalState.savedOps = [];
990
+ // now set clientId to stashed clientId so live ops are correctly processed as local
991
+ (0, common_utils_1.assert)(this.clientId === undefined, "Unexpected clientId when setting stashed clientId");
992
+ this._clientId = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.clientId;
993
+ }
832
994
  // We might have hit some failure that did not manifest itself in exception in this flow,
833
995
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
834
996
  if (!this.closed) {
835
997
  if (opsBeforeReturnP !== undefined) {
836
998
  this._deltaManager.inbound.resume();
837
- await ReportIfTooLong(this.mc.logger, "WaitOps", async () => { await opsBeforeReturnP; return {}; });
838
- await ReportIfTooLong(this.mc.logger, "WaitOpProcessing", async () => this._deltaManager.inbound.waitTillProcessingDone());
999
+ await telemetry_utils_1.PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "WaitOps" }, async () => opsBeforeReturnP);
1000
+ await telemetry_utils_1.PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "WaitOpProcessing" }, async () => this._deltaManager.inbound.waitTillProcessingDone());
839
1001
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
840
1002
  this._deltaManager.inbound.pause();
841
1003
  }
842
1004
  switch (loadMode.deltaConnection) {
843
1005
  case undefined:
1006
+ if (pendingLocalState) {
1007
+ // connect to delta stream now since we did not before
1008
+ this.connectToDeltaStream(connectionArgs);
1009
+ }
1010
+ // intentional fallthrough
844
1011
  case "delayed":
845
1012
  (0, common_utils_1.assert)(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
846
1013
  this.inboundQueuePausedFromInit = false;
@@ -890,16 +1057,17 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
890
1057
  }
891
1058
  async rehydrateDetachedFromSnapshot(detachedContainerSnapshot) {
892
1059
  if (detachedContainerSnapshot.tree[".hasAttachmentBlobs"] !== undefined) {
893
- (0, common_utils_1.assert)(!!this.loader.services.detachedBlobStorage && this.loader.services.detachedBlobStorage.size > 0, 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */);
1060
+ (0, common_utils_1.assert)(!!this.loader.services.detachedBlobStorage &&
1061
+ this.loader.services.detachedBlobStorage.size > 0, 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */);
894
1062
  delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
895
1063
  }
896
1064
  const snapshotTree = (0, utils_1.getSnapshotTreeFromSerializedContainer)(detachedContainerSnapshot);
897
- this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
898
- const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1065
+ this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1066
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
899
1067
  await this.attachDeltaManagerOpHandler(attributes);
900
1068
  // Initialize the protocol handler
901
1069
  const baseTree = (0, utils_1.getProtocolSnapshotTree)(snapshotTree);
902
- const qValues = await (0, driver_utils_1.readAndParse)(this.storageService, baseTree.blobs.quorumValues);
1070
+ const qValues = await (0, driver_utils_1.readAndParse)(this.storageAdapter, baseTree.blobs.quorumValues);
903
1071
  const codeDetails = (0, quorum_1.getCodeDetailsFromQuorumValues)(qValues);
904
1072
  this.initializeProtocolState(attributes, {
905
1073
  members: [],
@@ -937,11 +1105,12 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
937
1105
  };
938
1106
  if (snapshot !== undefined) {
939
1107
  const baseTree = (0, utils_1.getProtocolSnapshotTree)(snapshot);
940
- [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
941
- (0, driver_utils_1.readAndParse)(storage, baseTree.blobs.quorumMembers),
942
- (0, driver_utils_1.readAndParse)(storage, baseTree.blobs.quorumProposals),
943
- (0, driver_utils_1.readAndParse)(storage, baseTree.blobs.quorumValues),
944
- ]);
1108
+ [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] =
1109
+ await Promise.all([
1110
+ (0, driver_utils_1.readAndParse)(storage, baseTree.blobs.quorumMembers),
1111
+ (0, driver_utils_1.readAndParse)(storage, baseTree.blobs.quorumProposals),
1112
+ (0, driver_utils_1.readAndParse)(storage, baseTree.blobs.quorumValues),
1113
+ ]);
945
1114
  }
946
1115
  this.initializeProtocolState(attributes, quorumSnapshot);
947
1116
  }
@@ -968,7 +1137,10 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
968
1137
  });
969
1138
  }
970
1139
  this.processCodeProposal().catch((error) => {
971
- this.close((0, telemetry_utils_1.normalizeError)(error));
1140
+ var _a;
1141
+ const normalizedError = (0, telemetry_utils_1.normalizeError)(error);
1142
+ this.close(normalizedError);
1143
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, normalizedError);
972
1144
  throw error;
973
1145
  });
974
1146
  }
@@ -1025,7 +1197,10 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1025
1197
  if (this.clientDetailsOverride !== undefined) {
1026
1198
  (0, merge_1.default)(client.details, this.clientDetailsOverride);
1027
1199
  }
1028
- client.details.environment = [client.details.environment, ` loaderVersion:${packageVersion_1.pkgVersion}`].join(";");
1200
+ client.details.environment = [
1201
+ client.details.environment,
1202
+ ` loaderVersion:${packageVersion_1.pkgVersion}`,
1203
+ ].join(";");
1029
1204
  return client;
1030
1205
  }
1031
1206
  /**
@@ -1035,8 +1210,7 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1035
1210
  * If it's not true, runtime is not in position to send ops.
1036
1211
  */
1037
1212
  activeConnection() {
1038
- return this.connectionState === connectionState_1.ConnectionState.Connected &&
1039
- this.connectionMode === "write";
1213
+ return (this.connectionState === connectionState_1.ConnectionState.Connected && this.connectionMode === "write");
1040
1214
  }
1041
1215
  createDeltaManager() {
1042
1216
  const serviceProvider = () => this.service;
@@ -1053,7 +1227,9 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1053
1227
  deltaManager.on("disconnect", (reason) => {
1054
1228
  var _a;
1055
1229
  (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1056
- this.connectionStateHandler.receivedDisconnectEvent(reason);
1230
+ if (!this.closed) {
1231
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1232
+ }
1057
1233
  });
1058
1234
  deltaManager.on("throttled", (warning) => {
1059
1235
  const warn = warning;
@@ -1071,6 +1247,9 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1071
1247
  deltaManager.on("closed", (error) => {
1072
1248
  this.closeCore(error);
1073
1249
  });
1250
+ deltaManager.on("disposed", (error) => {
1251
+ this.disposeCore(error);
1252
+ });
1074
1253
  return deltaManager;
1075
1254
  }
1076
1255
  async attachDeltaManagerOpHandler(attributes, prefetchType) {
@@ -1098,7 +1277,8 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1098
1277
  }
1099
1278
  else {
1100
1279
  if (value === connectionState_1.ConnectionState.Connected) {
1101
- durationFromDisconnected = time - this.connectionTransitionTimes[connectionState_1.ConnectionState.Disconnected];
1280
+ durationFromDisconnected =
1281
+ time - this.connectionTransitionTimes[connectionState_1.ConnectionState.Disconnected];
1102
1282
  durationFromDisconnected = telemetry_utils_1.TelemetryLogger.formatTick(durationFromDisconnected);
1103
1283
  }
1104
1284
  else {
@@ -1141,32 +1321,39 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1141
1321
  this.protocolHandler.setConnectionState(state, this.clientId);
1142
1322
  (0, telemetry_utils_1.raiseConnectedEvent)(this.mc.logger, this, state, this.clientId, disconnectedReason);
1143
1323
  if (logOpsOnReconnect) {
1144
- this.mc.logger.sendTelemetryEvent({ eventName: "OpsSentOnReconnect", count: this.messageCountAfterDisconnection });
1324
+ this.mc.logger.sendTelemetryEvent({
1325
+ eventName: "OpsSentOnReconnect",
1326
+ count: this.messageCountAfterDisconnection,
1327
+ });
1145
1328
  }
1146
1329
  }
1147
1330
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1148
1331
  submitContainerMessage(type, contents, batch, metadata) {
1332
+ var _a;
1149
1333
  switch (type) {
1150
1334
  case protocol_definitions_1.MessageType.Operation:
1151
1335
  return this.submitMessage(type, JSON.stringify(contents), batch, metadata);
1152
1336
  case protocol_definitions_1.MessageType.Summarize:
1153
1337
  return this.submitSummaryMessage(contents);
1154
- default:
1155
- this.close(new container_utils_1.GenericError("invalidContainerSubmitOpType", undefined /* error */, { messageType: type }));
1338
+ default: {
1339
+ const newError = new container_utils_1.GenericError("invalidContainerSubmitOpType", undefined /* error */, { messageType: type });
1340
+ this.close(newError);
1341
+ (_a = this.dispose) === null || _a === void 0 ? void 0 : _a.call(this, newError);
1156
1342
  return -1;
1343
+ }
1157
1344
  }
1158
1345
  }
1159
1346
  /** @returns clientSequenceNumber of last message in a batch */
1160
- submitBatch(batch) {
1347
+ submitBatch(batch, referenceSequenceNumber) {
1161
1348
  let clientSequenceNumber = -1;
1162
1349
  for (const message of batch) {
1163
1350
  clientSequenceNumber = this.submitMessage(protocol_definitions_1.MessageType.Operation, message.contents, true, // batch
1164
- message.metadata, message.compression);
1351
+ message.metadata, message.compression, referenceSequenceNumber);
1165
1352
  }
1166
1353
  this._deltaManager.flush();
1167
1354
  return clientSequenceNumber;
1168
1355
  }
1169
- submitSummaryMessage(summary) {
1356
+ submitSummaryMessage(summary, referenceSequenceNumber) {
1170
1357
  // github #6451: this is only needed for staging so the server
1171
1358
  // know when the protocol tree is included
1172
1359
  // this can be removed once all clients send
@@ -1174,11 +1361,10 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1174
1361
  if (summary.details === undefined) {
1175
1362
  summary.details = {};
1176
1363
  }
1177
- summary.details.includesProtocolTree =
1178
- this.options.summarizeProtocolTree === true;
1179
- return this.submitMessage(protocol_definitions_1.MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1364
+ summary.details.includesProtocolTree = this.storageAdapter.summarizeProtocolTree;
1365
+ return this.submitMessage(protocol_definitions_1.MessageType.Summarize, JSON.stringify(summary), false /* batch */, undefined /* metadata */, undefined /* compression */, referenceSequenceNumber);
1180
1366
  }
1181
- submitMessage(type, contents, batch, metadata, compression) {
1367
+ submitMessage(type, contents, batch, metadata, compression, referenceSequenceNumber) {
1182
1368
  var _a;
1183
1369
  if (this.connectionState !== connectionState_1.ConnectionState.Connected) {
1184
1370
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
@@ -1186,9 +1372,12 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1186
1372
  }
1187
1373
  this.messageCountAfterDisconnection += 1;
1188
1374
  (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1189
- return this._deltaManager.submit(type, contents, batch, metadata, compression);
1375
+ return this._deltaManager.submit(type, contents, batch, metadata, compression, referenceSequenceNumber);
1190
1376
  }
1191
1377
  processRemoteMessage(message) {
1378
+ if (this.offlineLoadEnabled) {
1379
+ this.savedOps.push(message);
1380
+ }
1192
1381
  const local = this.clientId === message.clientId;
1193
1382
  // Allow the protocol handler to process the message
1194
1383
  const result = this.protocolHandler.processMessage(message, local);
@@ -1234,10 +1423,13 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1234
1423
  const version = await this.getVersion(specifiedVersion !== null && specifiedVersion !== void 0 ? specifiedVersion : null);
1235
1424
  if (version === undefined && specifiedVersion !== undefined) {
1236
1425
  // We should have a defined version to load from if specified version requested
1237
- this.mc.logger.sendErrorEvent({ eventName: "NoVersionFoundWhenSpecified", id: specifiedVersion });
1426
+ this.mc.logger.sendErrorEvent({
1427
+ eventName: "NoVersionFoundWhenSpecified",
1428
+ id: specifiedVersion,
1429
+ });
1238
1430
  }
1239
1431
  this._loadedFromVersion = version;
1240
- const snapshot = (_a = await this.storageService.getSnapshotTree(version)) !== null && _a !== void 0 ? _a : undefined;
1432
+ const snapshot = (_a = (await this.storageAdapter.getSnapshotTree(version))) !== null && _a !== void 0 ? _a : undefined;
1241
1433
  if (snapshot === undefined && version !== undefined) {
1242
1434
  this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1243
1435
  }
@@ -1256,7 +1448,7 @@ class Container extends telemetry_utils_1.EventEmitterWithErrorHandling {
1256
1448
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
1257
1449
  // are set. Global requests will still go directly to the loader
1258
1450
  const loader = new loader_1.RelativeLoader(this, this.loader);
1259
- this._context = await containerContext_1.ContainerContext.createOrLoad(this, this.scope, this.codeLoader, codeDetails, snapshot, new deltaManagerProxy_1.DeltaManagerProxy(this._deltaManager), new quorum_1.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);
1451
+ this._context = await containerContext_1.ContainerContext.createOrLoad(this, this.scope, this.codeLoader, codeDetails, snapshot, new deltaManagerProxy_1.DeltaManagerProxy(this._deltaManager), new quorum_1.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);
1260
1452
  this.emit("contextChanged", codeDetails);
1261
1453
  }
1262
1454
  updateDirtyContainerState(dirty) {