@fluidframework/container-loader 2.0.0-dev.5.2.0.169897 → 2.0.0-dev.6.4.0.191258

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 (204) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +10 -6
  3. package/dist/audience.d.ts +1 -0
  4. package/dist/audience.d.ts.map +1 -1
  5. package/dist/audience.js +5 -3
  6. package/dist/audience.js.map +1 -1
  7. package/dist/catchUpMonitor.d.ts +1 -1
  8. package/dist/catchUpMonitor.d.ts.map +1 -1
  9. package/dist/catchUpMonitor.js +2 -2
  10. package/dist/catchUpMonitor.js.map +1 -1
  11. package/dist/connectionManager.d.ts +6 -6
  12. package/dist/connectionManager.d.ts.map +1 -1
  13. package/dist/connectionManager.js +97 -93
  14. package/dist/connectionManager.js.map +1 -1
  15. package/dist/connectionStateHandler.d.ts +19 -15
  16. package/dist/connectionStateHandler.d.ts.map +1 -1
  17. package/dist/connectionStateHandler.js +59 -59
  18. package/dist/connectionStateHandler.js.map +1 -1
  19. package/dist/container.d.ts +48 -38
  20. package/dist/container.d.ts.map +1 -1
  21. package/dist/container.js +447 -325
  22. package/dist/container.js.map +1 -1
  23. package/dist/containerContext.d.ts +22 -70
  24. package/dist/containerContext.d.ts.map +1 -1
  25. package/dist/containerContext.js +24 -221
  26. package/dist/containerContext.js.map +1 -1
  27. package/dist/containerStorageAdapter.d.ts +1 -1
  28. package/dist/containerStorageAdapter.d.ts.map +1 -1
  29. package/dist/containerStorageAdapter.js +47 -16
  30. package/dist/containerStorageAdapter.js.map +1 -1
  31. package/dist/contracts.d.ts +21 -10
  32. package/dist/contracts.d.ts.map +1 -1
  33. package/dist/contracts.js +3 -3
  34. package/dist/contracts.js.map +1 -1
  35. package/dist/debugLogger.d.ts +30 -0
  36. package/dist/debugLogger.d.ts.map +1 -0
  37. package/dist/debugLogger.js +95 -0
  38. package/dist/debugLogger.js.map +1 -0
  39. package/dist/deltaManager.d.ts +21 -9
  40. package/dist/deltaManager.d.ts.map +1 -1
  41. package/dist/deltaManager.js +114 -66
  42. package/dist/deltaManager.js.map +1 -1
  43. package/dist/deltaQueue.d.ts +1 -1
  44. package/dist/deltaQueue.d.ts.map +1 -1
  45. package/dist/deltaQueue.js +10 -10
  46. package/dist/deltaQueue.js.map +1 -1
  47. package/dist/disposal.d.ts +13 -0
  48. package/dist/disposal.d.ts.map +1 -0
  49. package/dist/disposal.js +25 -0
  50. package/dist/disposal.js.map +1 -0
  51. package/dist/error.d.ts +23 -0
  52. package/dist/error.d.ts.map +1 -0
  53. package/dist/error.js +32 -0
  54. package/dist/error.js.map +1 -0
  55. package/dist/loader.d.ts +23 -5
  56. package/dist/loader.d.ts.map +1 -1
  57. package/dist/loader.js +82 -51
  58. package/dist/loader.js.map +1 -1
  59. package/dist/noopHeuristic.d.ts +23 -0
  60. package/dist/noopHeuristic.d.ts.map +1 -0
  61. package/dist/noopHeuristic.js +90 -0
  62. package/dist/noopHeuristic.js.map +1 -0
  63. package/dist/packageVersion.d.ts +1 -1
  64. package/dist/packageVersion.js +1 -1
  65. package/dist/packageVersion.js.map +1 -1
  66. package/dist/protocol.d.ts +9 -12
  67. package/dist/protocol.d.ts.map +1 -1
  68. package/dist/protocol.js +26 -7
  69. package/dist/protocol.js.map +1 -1
  70. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  71. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  72. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  73. package/dist/quorum.d.ts +1 -14
  74. package/dist/quorum.d.ts.map +1 -1
  75. package/dist/quorum.js +1 -29
  76. package/dist/quorum.js.map +1 -1
  77. package/dist/retriableDocumentStorageService.d.ts +1 -1
  78. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  79. package/dist/retriableDocumentStorageService.js +4 -4
  80. package/dist/retriableDocumentStorageService.js.map +1 -1
  81. package/dist/utils.d.ts +8 -1
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +30 -11
  84. package/dist/utils.js.map +1 -1
  85. package/lib/audience.d.ts +1 -0
  86. package/lib/audience.d.ts.map +1 -1
  87. package/lib/audience.js +4 -2
  88. package/lib/audience.js.map +1 -1
  89. package/lib/catchUpMonitor.d.ts +1 -1
  90. package/lib/catchUpMonitor.d.ts.map +1 -1
  91. package/lib/catchUpMonitor.js +1 -1
  92. package/lib/catchUpMonitor.js.map +1 -1
  93. package/lib/connectionManager.d.ts +6 -6
  94. package/lib/connectionManager.d.ts.map +1 -1
  95. package/lib/connectionManager.js +74 -67
  96. package/lib/connectionManager.js.map +1 -1
  97. package/lib/connectionStateHandler.d.ts +19 -15
  98. package/lib/connectionStateHandler.d.ts.map +1 -1
  99. package/lib/connectionStateHandler.js +36 -36
  100. package/lib/connectionStateHandler.js.map +1 -1
  101. package/lib/container.d.ts +48 -38
  102. package/lib/container.d.ts.map +1 -1
  103. package/lib/container.js +414 -292
  104. package/lib/container.js.map +1 -1
  105. package/lib/containerContext.d.ts +22 -70
  106. package/lib/containerContext.d.ts.map +1 -1
  107. package/lib/containerContext.js +24 -221
  108. package/lib/containerContext.js.map +1 -1
  109. package/lib/containerStorageAdapter.d.ts +1 -1
  110. package/lib/containerStorageAdapter.d.ts.map +1 -1
  111. package/lib/containerStorageAdapter.js +43 -12
  112. package/lib/containerStorageAdapter.js.map +1 -1
  113. package/lib/contracts.d.ts +21 -10
  114. package/lib/contracts.d.ts.map +1 -1
  115. package/lib/contracts.js +3 -3
  116. package/lib/contracts.js.map +1 -1
  117. package/lib/debugLogger.d.ts +30 -0
  118. package/lib/debugLogger.d.ts.map +1 -0
  119. package/lib/debugLogger.js +91 -0
  120. package/lib/debugLogger.js.map +1 -0
  121. package/lib/deltaManager.d.ts +21 -9
  122. package/lib/deltaManager.d.ts.map +1 -1
  123. package/lib/deltaManager.js +88 -37
  124. package/lib/deltaManager.js.map +1 -1
  125. package/lib/deltaQueue.d.ts +1 -1
  126. package/lib/deltaQueue.d.ts.map +1 -1
  127. package/lib/deltaQueue.js +3 -3
  128. package/lib/deltaQueue.js.map +1 -1
  129. package/lib/disposal.d.ts +13 -0
  130. package/lib/disposal.d.ts.map +1 -0
  131. package/lib/disposal.js +21 -0
  132. package/lib/disposal.js.map +1 -0
  133. package/lib/error.d.ts +23 -0
  134. package/lib/error.d.ts.map +1 -0
  135. package/lib/error.js +28 -0
  136. package/lib/error.js.map +1 -0
  137. package/lib/loader.d.ts +23 -5
  138. package/lib/loader.d.ts.map +1 -1
  139. package/lib/loader.js +82 -51
  140. package/lib/loader.js.map +1 -1
  141. package/lib/noopHeuristic.d.ts +23 -0
  142. package/lib/noopHeuristic.d.ts.map +1 -0
  143. package/lib/{collabWindowTracker.js → noopHeuristic.js} +31 -42
  144. package/lib/noopHeuristic.js.map +1 -0
  145. package/lib/packageVersion.d.ts +1 -1
  146. package/lib/packageVersion.js +1 -1
  147. package/lib/packageVersion.js.map +1 -1
  148. package/lib/protocol.d.ts +9 -12
  149. package/lib/protocol.d.ts.map +1 -1
  150. package/lib/protocol.js +24 -6
  151. package/lib/protocol.js.map +1 -1
  152. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  153. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  154. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  155. package/lib/quorum.d.ts +1 -14
  156. package/lib/quorum.d.ts.map +1 -1
  157. package/lib/quorum.js +0 -26
  158. package/lib/quorum.js.map +1 -1
  159. package/lib/retriableDocumentStorageService.d.ts +1 -1
  160. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  161. package/lib/retriableDocumentStorageService.js +2 -2
  162. package/lib/retriableDocumentStorageService.js.map +1 -1
  163. package/lib/utils.d.ts +8 -1
  164. package/lib/utils.d.ts.map +1 -1
  165. package/lib/utils.js +25 -7
  166. package/lib/utils.js.map +1 -1
  167. package/package.json +26 -28
  168. package/src/audience.ts +7 -1
  169. package/src/catchUpMonitor.ts +2 -2
  170. package/src/connectionManager.ts +76 -52
  171. package/src/connectionStateHandler.ts +46 -48
  172. package/src/container.ts +561 -326
  173. package/src/containerContext.ts +31 -349
  174. package/src/containerStorageAdapter.ts +49 -6
  175. package/src/contracts.ts +27 -13
  176. package/src/debugLogger.ts +113 -0
  177. package/src/deltaManager.ts +93 -36
  178. package/src/deltaQueue.ts +2 -1
  179. package/src/disposal.ts +25 -0
  180. package/src/error.ts +44 -0
  181. package/src/loader.ts +84 -36
  182. package/src/{collabWindowTracker.ts → noopHeuristic.ts} +38 -47
  183. package/src/packageVersion.ts +1 -1
  184. package/src/protocol.ts +26 -16
  185. package/src/protocolTreeDocumentStorageService.ts +1 -1
  186. package/src/quorum.ts +1 -40
  187. package/src/retriableDocumentStorageService.ts +3 -4
  188. package/src/utils.ts +33 -8
  189. package/dist/collabWindowTracker.d.ts +0 -19
  190. package/dist/collabWindowTracker.d.ts.map +0 -1
  191. package/dist/collabWindowTracker.js +0 -101
  192. package/dist/collabWindowTracker.js.map +0 -1
  193. package/dist/deltaManagerProxy.d.ts +0 -42
  194. package/dist/deltaManagerProxy.d.ts.map +0 -1
  195. package/dist/deltaManagerProxy.js +0 -79
  196. package/dist/deltaManagerProxy.js.map +0 -1
  197. package/lib/collabWindowTracker.d.ts +0 -19
  198. package/lib/collabWindowTracker.d.ts.map +0 -1
  199. package/lib/collabWindowTracker.js.map +0 -1
  200. package/lib/deltaManagerProxy.d.ts +0 -42
  201. package/lib/deltaManagerProxy.d.ts.map +0 -1
  202. package/lib/deltaManagerProxy.js +0 -74
  203. package/lib/deltaManagerProxy.js.map +0 -1
  204. package/src/deltaManagerProxy.ts +0 -109
package/lib/container.js CHANGED
@@ -5,30 +5,31 @@
5
5
  // eslint-disable-next-line import/no-internal-modules
6
6
  import merge from "lodash/merge";
7
7
  import { v4 as uuid } from "uuid";
8
- import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
8
+ import { assert, unreachableCase } from "@fluidframework/core-utils";
9
+ import { TypedEventEmitter, performance } from "@fluid-internal/client-utils";
10
+ import { LogLevel, } from "@fluidframework/core-interfaces";
9
11
  import { AttachState, isFluidCodeDetails, } from "@fluidframework/container-definitions";
10
- import { GenericError, UsageError } from "@fluidframework/container-utils";
11
- import { readAndParse, OnlineStatus, isOnline, combineAppAndProtocolSummary, runWithRetry, isCombinedAppAndProtocolSummary, } from "@fluidframework/driver-utils";
12
+ import { readAndParse, OnlineStatus, isOnline, runWithRetry, isCombinedAppAndProtocolSummary, MessageType2, } from "@fluidframework/driver-utils";
12
13
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
13
- import { ChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, TelemetryLogger, connectedEventName, normalizeError, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
14
+ import { createChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, connectedEventName, normalizeError, createChildMonitoringContext, wrapError, formatTick, GenericError, UsageError, } from "@fluidframework/telemetry-utils";
14
15
  import { Audience } from "./audience";
15
16
  import { ContainerContext } from "./containerContext";
16
- import { ReconnectMode, getPackageName } from "./contracts";
17
+ import { ReconnectMode, getPackageName, } from "./contracts";
17
18
  import { DeltaManager } from "./deltaManager";
18
- import { DeltaManagerProxy } from "./deltaManagerProxy";
19
19
  import { RelativeLoader } from "./loader";
20
20
  import { pkgVersion } from "./packageVersion";
21
21
  import { ContainerStorageAdapter, getBlobContentsFromTree, getBlobContentsFromTreeWithBlobContents, } from "./containerStorageAdapter";
22
22
  import { createConnectionStateHandler } from "./connectionStateHandler";
23
- import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
24
- import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy, } from "./quorum";
25
- import { CollabWindowTracker } from "./collabWindowTracker";
23
+ import { combineAppAndProtocolSummary, getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer, } from "./utils";
24
+ import { initQuorumValuesFromCodeDetails } from "./quorum";
25
+ import { NoopHeuristic } from "./noopHeuristic";
26
26
  import { ConnectionManager } from "./connectionManager";
27
27
  import { ConnectionState } from "./connectionState";
28
- import { OnlyValidTermValue, ProtocolHandler, } from "./protocol";
28
+ import { OnlyValidTermValue, ProtocolHandler, protocolHandlerShouldProcessSignal, } from "./protocol";
29
29
  const detachedContainerRefSeqNumber = 0;
30
30
  const dirtyContainerEvent = "dirty";
31
31
  const savedContainerEvent = "saved";
32
+ const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
32
33
  /**
33
34
  * Waits until container connects to delta storage and gets up-to-date.
34
35
  *
@@ -104,7 +105,7 @@ export async function waitContainerToCatchUp(container) {
104
105
  }
105
106
  const getCodeProposal =
106
107
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
107
- (quorum) => { var _a; return (_a = quorum.get("code")) !== null && _a !== void 0 ? _a : quorum.get("code2"); };
108
+ (quorum) => quorum.get("code") ?? quorum.get("code2");
108
109
  /**
109
110
  * Helper function to report to telemetry cases where operation takes longer than expected (200ms)
110
111
  * @param logger - logger to use
@@ -124,7 +125,6 @@ export class Container extends EventEmitterWithErrorHandling {
124
125
  * @internal
125
126
  */
126
127
  constructor(createProps, loadProps) {
127
- var _a;
128
128
  super((name, error) => {
129
129
  this.mc.logger.sendErrorEvent({
130
130
  eventName: "ContainerEventHandlerException",
@@ -152,16 +152,31 @@ export class Container extends EventEmitterWithErrorHandling {
152
152
  this.inboundQueuePausedFromInit = true;
153
153
  this.firstConnection = true;
154
154
  this.connectionTransitionTimes = [];
155
- this.messageCountAfterDisconnection = 0;
156
155
  this.attachStarted = false;
157
156
  this._dirtyContainer = false;
158
157
  this.savedOps = [];
158
+ this.clientsWhoShouldHaveLeft = new Set();
159
159
  this.setAutoReconnectTime = performance.now();
160
+ this._lifecycleEvents = new TypedEventEmitter();
160
161
  this._disposed = false;
162
+ this.getAbsoluteUrl = async (relativeUrl) => {
163
+ if (this.resolvedUrl === undefined) {
164
+ return undefined;
165
+ }
166
+ return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName(this._loadedCodeDetails));
167
+ };
168
+ this.updateDirtyContainerState = (dirty) => {
169
+ if (this._dirtyContainer === dirty) {
170
+ return;
171
+ }
172
+ this._dirtyContainer = dirty;
173
+ this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
174
+ };
161
175
  const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
162
176
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
163
- const pendingLocalState = loadProps === null || loadProps === void 0 ? void 0 : loadProps.pendingLocalState;
164
- this._canReconnect = canReconnect !== null && canReconnect !== void 0 ? canReconnect : true;
177
+ const pendingLocalState = loadProps?.pendingLocalState;
178
+ this._clientId = pendingLocalState?.clientId;
179
+ this._canReconnect = canReconnect ?? true;
165
180
  this.clientDetailsOverride = clientDetailsOverride;
166
181
  this.urlResolver = urlResolver;
167
182
  this.serviceFactory = documentServiceFactory;
@@ -169,14 +184,18 @@ export class Container extends EventEmitterWithErrorHandling {
169
184
  // Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
170
185
  // all clients that were loaded from the same loader (including summarizer clients).
171
186
  // Tracking alternative ways to handle this in AB#4129.
172
- this.options = Object.assign({}, options);
187
+ this.options = { ...options };
173
188
  this.scope = scope;
174
189
  this.detachedBlobStorage = detachedBlobStorage;
175
190
  this.protocolHandlerBuilder =
176
- protocolHandlerBuilder !== null && protocolHandlerBuilder !== void 0 ? protocolHandlerBuilder : ((...args) => new ProtocolHandler(...args, new Audience()));
191
+ protocolHandlerBuilder ??
192
+ ((attributes, quorumSnapshot, sendProposal) => new ProtocolHandler(attributes, quorumSnapshot, sendProposal, new Audience(), (clientId) => this.clientsWhoShouldHaveLeft.has(clientId)));
177
193
  // Note that we capture the createProps here so we can replicate the creation call when we want to clone.
178
194
  this.clone = async (_loadProps, createParamOverrides) => {
179
- return Container.load(_loadProps, Object.assign(Object.assign({}, createProps), createParamOverrides));
195
+ return Container.load(_loadProps, {
196
+ ...createProps,
197
+ ...createParamOverrides,
198
+ });
180
199
  };
181
200
  // Create logger for data stores to use
182
201
  const type = this.client.details.type;
@@ -184,45 +203,50 @@ export class Container extends EventEmitterWithErrorHandling {
184
203
  const clientType = `${interactive ? "interactive" : "noninteractive"}${type !== undefined && type !== "" ? `/${type}` : ""}`;
185
204
  // Need to use the property getter for docId because for detached flow we don't have the docId initially.
186
205
  // We assign the id later so property getter is used.
187
- this.subLogger = ChildLogger.create(subLogger, undefined, {
188
- all: {
189
- clientType,
190
- containerId: uuid(),
191
- docId: () => { var _a; return (_a = this.resolvedUrl) === null || _a === void 0 ? void 0 : _a.id; },
192
- containerAttachState: () => this._attachState,
193
- containerLifecycleState: () => this._lifecycleState,
194
- containerConnectionState: () => ConnectionState[this.connectionState],
195
- serializedContainer: pendingLocalState !== undefined,
196
- },
197
- // we need to be judicious with our logging here to avoid generating too much data
198
- // all data logged here should be broadly applicable, and not specific to a
199
- // specific error or class of errors
200
- error: {
201
- // load information to associate errors with the specific load point
202
- dmInitialSeqNumber: () => { var _a; return (_a = this._deltaManager) === null || _a === void 0 ? void 0 : _a.initialSequenceNumber; },
203
- dmLastProcessedSeqNumber: () => { var _a; return (_a = this._deltaManager) === null || _a === void 0 ? void 0 : _a.lastSequenceNumber; },
204
- dmLastKnownSeqNumber: () => { var _a; return (_a = this._deltaManager) === null || _a === void 0 ? void 0 : _a.lastKnownSeqNumber; },
205
- containerLoadedFromVersionId: () => { var _a; return (_a = this.loadedFromVersion) === null || _a === void 0 ? void 0 : _a.id; },
206
- containerLoadedFromVersionDate: () => { var _a; return (_a = this.loadedFromVersion) === null || _a === void 0 ? void 0 : _a.date; },
207
- // message information to associate errors with the specific execution state
208
- // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
209
- 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; },
210
- 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; },
211
- 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; },
212
- 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; },
213
- connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
206
+ this.subLogger = createChildLogger({
207
+ logger: subLogger,
208
+ properties: {
209
+ all: {
210
+ clientType,
211
+ containerId: uuid(),
212
+ docId: () => this.resolvedUrl?.id,
213
+ containerAttachState: () => this._attachState,
214
+ containerLifecycleState: () => this._lifecycleState,
215
+ containerConnectionState: () => ConnectionState[this.connectionState],
216
+ serializedContainer: pendingLocalState !== undefined,
217
+ },
218
+ // we need to be judicious with our logging here to avoid generating too much data
219
+ // all data logged here should be broadly applicable, and not specific to a
220
+ // specific error or class of errors
221
+ error: {
222
+ // load information to associate errors with the specific load point
223
+ dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
224
+ dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
225
+ dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
226
+ containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
227
+ containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
228
+ // message information to associate errors with the specific execution state
229
+ // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
230
+ dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
231
+ dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
232
+ dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId === null
233
+ ? "null"
234
+ : this.deltaManager?.lastMessage?.clientId,
235
+ dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
236
+ connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
237
+ },
214
238
  },
215
239
  });
216
240
  // Prefix all events in this file with container-loader
217
- this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
241
+ this.mc = createChildMonitoringContext({ logger: this.subLogger, namespace: "Container" });
218
242
  this._deltaManager = this.createDeltaManager();
219
243
  this.connectionStateHandler = createConnectionStateHandler({
220
244
  logger: this.mc.logger,
221
- connectionStateChanged: (value, oldState, reason, error) => {
245
+ connectionStateChanged: (value, oldState, reason) => {
222
246
  if (value === ConnectionState.Connected) {
223
247
  this._clientId = this.connectionStateHandler.pendingClientId;
224
248
  }
225
- this.logConnectionStateChangeTelemetry(value, oldState, reason, error);
249
+ this.logConnectionStateChangeTelemetry(value, oldState, reason);
226
250
  if (this._lifecycleState === "loaded") {
227
251
  this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
228
252
  ? reason
@@ -238,9 +262,14 @@ export class Container extends EventEmitterWithErrorHandling {
238
262
  // Report issues only if we already loaded container - op processing is paused while container is loading,
239
263
  // so we always time-out processing of join op in cases where fetching snapshot takes a minute.
240
264
  // It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
241
- this._deltaManager.logConnectionIssue(Object.assign({ eventName,
242
- mode, category: this._lifecycleState === "loading" ? "generic" : category, duration: performance.now() -
243
- this.connectionTransitionTimes[ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
265
+ this._deltaManager.logConnectionIssue({
266
+ eventName,
267
+ mode,
268
+ category: this._lifecycleState === "loading" ? "generic" : category,
269
+ duration: performance.now() -
270
+ this.connectionTransitionTimes[ConnectionState.CatchingUp],
271
+ ...(details === undefined ? {} : { details: JSON.stringify(details) }),
272
+ });
244
273
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
245
274
  // to very slow op fetches and we will eventually get there.
246
275
  // For "read" connections, we get here due to self join signal not arriving on time. We will need to
@@ -250,11 +279,15 @@ export class Container extends EventEmitterWithErrorHandling {
250
279
  // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
251
280
  // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
252
281
  if (mode === "read") {
253
- this.disconnect();
254
- this.connect();
282
+ const reason = { text: "NoJoinSignal" };
283
+ this.disconnectInternal(reason);
284
+ this.connectInternal({ reason, fetchOpsFromStorage: false });
255
285
  }
256
286
  },
257
- }, this.deltaManager, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.clientId);
287
+ clientShouldHaveLeft: (clientId) => {
288
+ this.clientsWhoShouldHaveLeft.add(clientId);
289
+ },
290
+ }, this.deltaManager, pendingLocalState?.clientId);
258
291
  this.on(savedContainerEvent, () => {
259
292
  this.connectionStateHandler.containerSaved();
260
293
  });
@@ -266,14 +299,15 @@ export class Container extends EventEmitterWithErrorHandling {
266
299
  : combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
267
300
  // Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
268
301
  // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
269
- const forceEnableSummarizeProtocolTree = (_a = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")) !== null && _a !== void 0 ? _a : options.summarizeProtocolTree;
270
- this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
302
+ const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
303
+ options.summarizeProtocolTree;
304
+ this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
271
305
  const isDomAvailable = typeof document === "object" &&
272
306
  document !== null &&
273
307
  typeof document.addEventListener === "function" &&
274
308
  document.addEventListener !== null;
275
- // keep track of last time page was visible for telemetry
276
- if (isDomAvailable) {
309
+ // keep track of last time page was visible for telemetry (on interactive clients only)
310
+ if (isDomAvailable && interactive) {
277
311
  this.lastVisible = document.hidden ? performance.now() : undefined;
278
312
  this.visibilityEventHandler = () => {
279
313
  if (document.hidden) {
@@ -294,7 +328,7 @@ export class Container extends EventEmitterWithErrorHandling {
294
328
  * @internal
295
329
  */
296
330
  static async load(loadProps, createProps) {
297
- const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
331
+ const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } = loadProps;
298
332
  const container = new Container(createProps, loadProps);
299
333
  const disableRecordHeapSize = container.mc.config.getBoolean("Fluid.Loader.DisableRecordHeapSize");
300
334
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
@@ -302,26 +336,31 @@ export class Container extends EventEmitterWithErrorHandling {
302
336
  // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
303
337
  // to return container, so ignore this value and use undefined for opsBeforeReturn
304
338
  const mode = pendingLocalState
305
- ? Object.assign(Object.assign({}, (loadMode !== null && loadMode !== void 0 ? loadMode : defaultMode)), { opsBeforeReturn: undefined }) : loadMode !== null && loadMode !== void 0 ? loadMode : defaultMode;
339
+ ? { ...(loadMode ?? defaultMode), opsBeforeReturn: undefined }
340
+ : loadMode ?? defaultMode;
306
341
  const onClosed = (err) => {
307
342
  // pre-0.58 error message: containerClosedWithoutErrorDuringLoad
308
- reject(err !== null && err !== void 0 ? err : new GenericError("Container closed without error during load"));
343
+ reject(err ?? new GenericError("Container closed without error during load"));
309
344
  };
310
345
  container.on("closed", onClosed);
311
346
  container
312
- .load(version, mode, resolvedUrl, pendingLocalState)
347
+ .load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
313
348
  .finally(() => {
314
349
  container.removeListener("closed", onClosed);
315
350
  })
316
351
  .then((props) => {
317
- event.end(Object.assign(Object.assign({}, props), loadMode));
352
+ event.end({ ...props, ...loadMode });
318
353
  resolve(container);
319
354
  }, (error) => {
320
355
  const err = normalizeError(error);
321
356
  // Depending where error happens, we can be attempting to connect to web socket
322
357
  // and continuously retrying (consider offline mode)
323
358
  // Host has no container to close, so it's prudent to do it here
359
+ // Note: We could only dispose the container instead of just close but that would
360
+ // the telemetry where users sometimes search for ContainerClose event to look
361
+ // for load failures. So not removing this at this time.
324
362
  container.close(err);
363
+ container.dispose(err);
325
364
  onClosed(err);
326
365
  });
327
366
  }), { start: true, end: true, cancel: "generic" }, disableRecordHeapSize !== true /* recordHeapSize */);
@@ -358,19 +397,16 @@ export class Container extends EventEmitterWithErrorHandling {
358
397
  }
359
398
  }
360
399
  get closed() {
361
- return (this._lifecycleState === "closing" ||
362
- this._lifecycleState === "closed" ||
363
- this._lifecycleState === "disposing" ||
364
- this._lifecycleState === "disposed");
400
+ return (this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed);
365
401
  }
366
- get storage() {
367
- return this.storageAdapter;
402
+ get disposed() {
403
+ return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
368
404
  }
369
- get context() {
370
- if (this._context === undefined) {
371
- throw new GenericError("Attempted to access context before it was defined");
405
+ get runtime() {
406
+ if (this._runtime === undefined) {
407
+ throw new Error("Attempted to access runtime before it was defined");
372
408
  }
373
- return this._context;
409
+ return this._runtime;
374
410
  }
375
411
  get protocolHandler() {
376
412
  if (this._protocolHandler === undefined) {
@@ -385,7 +421,6 @@ export class Container extends EventEmitterWithErrorHandling {
385
421
  return this;
386
422
  }
387
423
  get resolvedUrl() {
388
- var _a;
389
424
  /**
390
425
  * All attached containers will have a document service,
391
426
  * this is required, as attached containers are attached to
@@ -397,17 +432,11 @@ export class Container extends EventEmitterWithErrorHandling {
397
432
  * is always the same as the containers, as we had to
398
433
  * obtain the resolved url, and then create the service from it.
399
434
  */
400
- return (_a = this.service) === null || _a === void 0 ? void 0 : _a.resolvedUrl;
401
- }
402
- get loadedFromVersion() {
403
- return this._loadedFromVersion;
435
+ return this.service?.resolvedUrl;
404
436
  }
405
437
  get readOnlyInfo() {
406
438
  return this._deltaManager.readOnlyInfo;
407
439
  }
408
- get closeSignal() {
409
- return this._deltaManager.closeAbortController.signal;
410
- }
411
440
  /**
412
441
  * Tracks host requiring read-only mode.
413
442
  */
@@ -423,13 +452,6 @@ export class Container extends EventEmitterWithErrorHandling {
423
452
  get connected() {
424
453
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
425
454
  }
426
- /**
427
- * Service configuration details. If running in offline mode will be undefined otherwise will contain service
428
- * configuration details returned as part of the initial connection.
429
- */
430
- get serviceConfiguration() {
431
- return this._deltaManager.serviceConfiguration;
432
- }
433
455
  /**
434
456
  * The server provided id of the client.
435
457
  * Set once this.connected is true, otherwise undefined
@@ -437,21 +459,11 @@ export class Container extends EventEmitterWithErrorHandling {
437
459
  get clientId() {
438
460
  return this._clientId;
439
461
  }
440
- /**
441
- * The server provided claims of the client.
442
- * Set once this.connected is true, otherwise undefined
443
- */
444
- get scopes() {
445
- return this._deltaManager.connectionManager.scopes;
446
- }
447
- get clientDetails() {
448
- return this._deltaManager.clientDetails;
449
- }
450
462
  get offlineLoadEnabled() {
451
- var _a, _b;
452
- const enabled = (_a = this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad")) !== null && _a !== void 0 ? _a : ((_b = this.options) === null || _b === void 0 ? void 0 : _b.enableOfflineLoad) === true;
463
+ const enabled = this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
464
+ this.options?.enableOfflineLoad === true;
453
465
  // summarizer will not have any pending state we want to save
454
- return enabled && this.clientDetails.capabilities.interactive;
466
+ return enabled && this.deltaManager.clientDetails.capabilities.interactive;
455
467
  }
456
468
  /**
457
469
  * Get the code details that are currently specified for the container.
@@ -466,8 +478,7 @@ export class Container extends EventEmitterWithErrorHandling {
466
478
  * loaded.
467
479
  */
468
480
  getLoadedCodeDetails() {
469
- var _a;
470
- return (_a = this._context) === null || _a === void 0 ? void 0 : _a.codeDetails;
481
+ return this._loadedCodeDetails;
471
482
  }
472
483
  /**
473
484
  * Retrieves the audience associated with the document
@@ -487,33 +498,25 @@ export class Container extends EventEmitterWithErrorHandling {
487
498
  * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
488
499
  */
489
500
  async getEntryPoint() {
490
- var _a, _b;
491
- // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
492
- // allow it since they mean a kind of read-only state for the Container.
493
- // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
494
- if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
495
- throw new UsageError("The container is disposing or disposed");
501
+ if (this._disposed) {
502
+ throw new UsageError("The context is already disposed");
496
503
  }
497
- while (this._context === undefined) {
498
- await new Promise((resolve, reject) => {
499
- const contextChangedHandler = () => {
500
- resolve();
501
- this.off("disposed", disposedHandler);
502
- };
503
- const disposedHandler = (error) => {
504
- reject(error !== null && error !== void 0 ? error : "The Container is disposed");
505
- this.off("contextChanged", contextChangedHandler);
506
- };
507
- this.once("contextChanged", contextChangedHandler);
508
- this.once("disposed", disposedHandler);
509
- });
510
- // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
511
- // should have set this._context; making sure.
512
- assert(this._context !== undefined, 0x5a2 /* Context still not defined after contextChanged event */);
504
+ if (this._runtime !== undefined) {
505
+ return this._runtime.getEntryPoint?.();
513
506
  }
514
- // Disable lint rule for the sake of more complete stack traces
515
- // eslint-disable-next-line no-return-await
516
- return await ((_b = (_a = this._context).getEntryPoint) === null || _b === void 0 ? void 0 : _b.call(_a));
507
+ return new Promise((resolve, reject) => {
508
+ const runtimeInstantiatedHandler = () => {
509
+ assert(this._runtime !== undefined, 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */);
510
+ resolve(this._runtime.getEntryPoint?.());
511
+ this._lifecycleEvents.off("disposed", disposedHandler);
512
+ };
513
+ const disposedHandler = () => {
514
+ reject(new Error("ContainerContext was disposed"));
515
+ this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
516
+ };
517
+ this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
518
+ this._lifecycleEvents.once("disposed", disposedHandler);
519
+ });
517
520
  }
518
521
  /**
519
522
  * Retrieves the quorum associated with the document
@@ -538,7 +541,6 @@ export class Container extends EventEmitterWithErrorHandling {
538
541
  assert(this._lifecycleState === "closed" || this._lifecycleState === "disposed", 0x314 /* Container properly closed */);
539
542
  }
540
543
  closeCore(error) {
541
- var _a;
542
544
  assert(!this.closed, 0x315 /* re-entrancy */);
543
545
  try {
544
546
  // Ensure that we raise all key events even if one of these throws
@@ -554,7 +556,7 @@ export class Container extends EventEmitterWithErrorHandling {
554
556
  : "generic",
555
557
  }, error);
556
558
  this._lifecycleState = "closing";
557
- (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
559
+ this._protocolHandler?.close();
558
560
  this.connectionStateHandler.dispose();
559
561
  }
560
562
  catch (exception) {
@@ -567,10 +569,13 @@ export class Container extends EventEmitterWithErrorHandling {
567
569
  }
568
570
  finally {
569
571
  this._lifecycleState = "closed";
572
+ // There is no user for summarizer, so we need to ensure dispose is called
573
+ if (this.client.details.type === summarizerClientType) {
574
+ this.dispose(error);
575
+ }
570
576
  }
571
577
  }
572
578
  disposeCore(error) {
573
- var _a, _b, _c;
574
579
  assert(!this._disposed, 0x54c /* Container already disposed */);
575
580
  this._disposed = true;
576
581
  try {
@@ -580,20 +585,22 @@ export class Container extends EventEmitterWithErrorHandling {
580
585
  // This gives us a chance to know what errors happened on open vs. on fully loaded container.
581
586
  this.mc.logger.sendTelemetryEvent({
582
587
  eventName: "ContainerDispose",
583
- category: "generic",
588
+ // Only log error if container isn't closed
589
+ category: !this.closed && error !== undefined ? "error" : "generic",
584
590
  }, error);
585
591
  // ! Progressing from "closed" to "disposing" is not allowed
586
592
  if (this._lifecycleState !== "closed") {
587
593
  this._lifecycleState = "disposing";
588
594
  }
589
- (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
595
+ this._protocolHandler?.close();
590
596
  this.connectionStateHandler.dispose();
591
- (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
597
+ const maybeError = error !== undefined ? new Error(error.message) : undefined;
598
+ this._runtime?.dispose(maybeError);
592
599
  this.storageAdapter.dispose();
593
600
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
594
601
  // about file, like file being overwritten in storage, but client having stale local cache.
595
602
  // Driver need to ensure all caches are cleared on critical errors
596
- (_c = this.service) === null || _c === void 0 ? void 0 : _c.dispose(error);
603
+ this.service?.dispose(error);
597
604
  }
598
605
  catch (exception) {
599
606
  this.mc.logger.sendErrorEvent({ eventName: "ContainerDisposeException" }, exception);
@@ -606,42 +613,58 @@ export class Container extends EventEmitterWithErrorHandling {
606
613
  }
607
614
  finally {
608
615
  this._lifecycleState = "disposed";
616
+ this._lifecycleEvents.emit("disposed");
609
617
  }
610
618
  }
611
- closeAndGetPendingLocalState() {
619
+ async closeAndGetPendingLocalState() {
612
620
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
613
621
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
614
622
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
615
- const pendingState = this.getPendingLocalState();
623
+ this.disconnectInternal({ text: "closeAndGetPendingLocalState" }); // TODO https://dev.azure.com/fluidframework/internal/_workitems/edit/5127
624
+ const pendingState = await this.getPendingLocalStateCore({ notifyImminentClosure: true });
616
625
  this.close();
617
626
  return pendingState;
618
627
  }
619
- getPendingLocalState() {
620
- if (!this.offlineLoadEnabled) {
621
- throw new UsageError("Can't get pending local state unless offline load is enabled");
622
- }
623
- assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
624
- assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
625
- assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
626
- assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
627
- const pendingState = {
628
- pendingRuntimeState: this.context.getPendingLocalState(),
629
- baseSnapshot: this.baseSnapshot,
630
- snapshotBlobs: this.baseSnapshotBlobs,
631
- savedOps: this.savedOps,
632
- url: this.resolvedUrl.url,
633
- term: OnlyValidTermValue,
628
+ async getPendingLocalState() {
629
+ return this.getPendingLocalStateCore({ notifyImminentClosure: false });
630
+ }
631
+ async getPendingLocalStateCore(props) {
632
+ return PerformanceEvent.timedExecAsync(this.mc.logger, {
633
+ eventName: "getPendingLocalState",
634
+ notifyImminentClosure: props.notifyImminentClosure,
635
+ savedOpsSize: this.savedOps.length,
634
636
  clientId: this.clientId,
635
- };
636
- this.mc.logger.sendTelemetryEvent({ eventName: "GetPendingLocalState" });
637
- return JSON.stringify(pendingState);
637
+ }, async () => {
638
+ if (!this.offlineLoadEnabled) {
639
+ throw new UsageError("Can't get pending local state unless offline load is enabled");
640
+ }
641
+ if (this.closed || this._disposed) {
642
+ throw new UsageError("Pending state cannot be retried if the container is closed or disposed");
643
+ }
644
+ assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
645
+ assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
646
+ assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
647
+ assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
648
+ const pendingRuntimeState = await this.runtime.getPendingLocalState(props);
649
+ const pendingState = {
650
+ pendingRuntimeState,
651
+ baseSnapshot: this.baseSnapshot,
652
+ snapshotBlobs: this.baseSnapshotBlobs,
653
+ savedOps: this.savedOps,
654
+ url: this.resolvedUrl.url,
655
+ term: OnlyValidTermValue,
656
+ // no need to save this if there is no pending runtime state
657
+ clientId: pendingRuntimeState !== undefined ? this.clientId : undefined,
658
+ };
659
+ return JSON.stringify(pendingState);
660
+ });
638
661
  }
639
662
  get attachState() {
640
663
  return this._attachState;
641
664
  }
642
665
  serialize() {
643
666
  assert(this.attachState === AttachState.Detached, 0x0d3 /* "Should only be called in detached container" */);
644
- const appSummary = this.context.createSummary();
667
+ const appSummary = this.runtime.createSummary();
645
668
  const protocolSummary = this.captureProtocolSummary();
646
669
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
647
670
  if (this.detachedBlobStorage && this.detachedBlobStorage.size > 0) {
@@ -652,9 +675,8 @@ export class Container extends EventEmitterWithErrorHandling {
652
675
  }
653
676
  return JSON.stringify(combinedSummary);
654
677
  }
655
- async attach(request) {
678
+ async attach(request, attachProps) {
656
679
  await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
657
- var _a;
658
680
  if (this._lifecycleState !== "loaded") {
659
681
  // pre-0.58 error message: containerNotValidForAttach
660
682
  throw new UsageError(`The Container is not in a valid state for attach [${this._lifecycleState}]`);
@@ -670,7 +692,7 @@ export class Container extends EventEmitterWithErrorHandling {
670
692
  if (!hasAttachmentBlobs) {
671
693
  // Get the document state post attach - possibly can just call attach but we need to change the
672
694
  // semantics around what the attach means as far as async code goes.
673
- const appSummary = this.context.createSummary();
695
+ const appSummary = this.runtime.createSummary();
674
696
  const protocolSummary = this.captureProtocolSummary();
675
697
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
676
698
  // Set the state as attaching as we are starting the process of attaching container.
@@ -678,6 +700,7 @@ export class Container extends EventEmitterWithErrorHandling {
678
700
  // starting to attach the container to storage.
679
701
  // Also, this should only be fired in detached container.
680
702
  this._attachState = AttachState.Attaching;
703
+ this.runtime.setAttachState(AttachState.Attaching);
681
704
  this.emit("attaching");
682
705
  if (this.offlineLoadEnabled) {
683
706
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -692,7 +715,7 @@ export class Container extends EventEmitterWithErrorHandling {
692
715
  assert(this.client.details.type !== summarizerClientType &&
693
716
  createNewResolvedUrl !== undefined, 0x2c4 /* "client should not be summarizer before container is created" */);
694
717
  this.service = await runWithRetry(async () => this.serviceFactory.createContainer(summary, createNewResolvedUrl, this.subLogger, false), "containerAttach", this.mc.logger, {
695
- cancel: this.closeSignal,
718
+ cancel: this._deltaManager.closeAbortController.signal,
696
719
  });
697
720
  }
698
721
  await this.storageAdapter.connectToService(this.service);
@@ -714,10 +737,11 @@ export class Container extends EventEmitterWithErrorHandling {
714
737
  }
715
738
  }
716
739
  // take summary and upload
717
- const appSummary = this.context.createSummary(redirectTable);
740
+ const appSummary = this.runtime.createSummary(redirectTable);
718
741
  const protocolSummary = this.captureProtocolSummary();
719
742
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
720
743
  this._attachState = AttachState.Attaching;
744
+ this.runtime.setAttachState(AttachState.Attaching);
721
745
  this.emit("attaching");
722
746
  if (this.offlineLoadEnabled) {
723
747
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -732,27 +756,28 @@ export class Container extends EventEmitterWithErrorHandling {
732
756
  });
733
757
  }
734
758
  this._attachState = AttachState.Attached;
759
+ this.runtime.setAttachState(AttachState.Attached);
735
760
  this.emit("attached");
736
761
  if (!this.closed) {
737
- this.resumeInternal({
762
+ this.handleDeltaConnectionArg({
738
763
  fetchOpsFromStorage: false,
739
- reason: "createDetached",
740
- });
764
+ reason: { text: "createDetached" },
765
+ }, attachProps?.deltaConnection);
741
766
  }
742
767
  }
743
768
  catch (error) {
744
769
  // add resolved URL on error object so that host has the ability to find this document and delete it
745
770
  const newError = normalizeError(error);
746
- newError.addTelemetryProperties({ resolvedUrl: (_a = this.resolvedUrl) === null || _a === void 0 ? void 0 : _a.url });
771
+ newError.addTelemetryProperties({ resolvedUrl: this.resolvedUrl?.url });
747
772
  this.close(newError);
748
773
  throw newError;
749
774
  }
750
775
  }, { start: true, end: true, cancel: "generic" });
751
776
  }
752
777
  async request(path) {
753
- return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Request" }, async () => this.context.request(path), { end: true, cancel: "error" });
778
+ return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Request" }, async () => this.runtime.request(path), { end: true, cancel: "error" });
754
779
  }
755
- setAutoReconnectInternal(mode) {
780
+ setAutoReconnectInternal(mode, reason) {
756
781
  const currentMode = this._deltaManager.connectionManager.reconnectMode;
757
782
  if (currentMode === mode) {
758
783
  return;
@@ -766,7 +791,7 @@ export class Container extends EventEmitterWithErrorHandling {
766
791
  connectionState: ConnectionState[this.connectionState],
767
792
  duration,
768
793
  });
769
- this._deltaManager.connectionManager.setAutoReconnect(mode);
794
+ this._deltaManager.connectionManager.setAutoReconnect(mode, reason);
770
795
  }
771
796
  connect() {
772
797
  if (this.closed) {
@@ -779,7 +804,10 @@ export class Container extends EventEmitterWithErrorHandling {
779
804
  // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
780
805
  // If there is gap, we will learn about it once connected, but the gap should be small (if any),
781
806
  // assuming that connect() is called quickly after initial container boot.
782
- this.connectInternal({ reason: "DocumentConnect", fetchOpsFromStorage: false });
807
+ this.connectInternal({
808
+ reason: { text: "DocumentConnect" },
809
+ fetchOpsFromStorage: false,
810
+ });
783
811
  }
784
812
  }
785
813
  connectInternal(args) {
@@ -789,21 +817,21 @@ export class Container extends EventEmitterWithErrorHandling {
789
817
  this.resumeInternal(args);
790
818
  // Set Auto Reconnect Mode
791
819
  const mode = ReconnectMode.Enabled;
792
- this.setAutoReconnectInternal(mode);
820
+ this.setAutoReconnectInternal(mode, args.reason);
793
821
  }
794
822
  disconnect() {
795
823
  if (this.closed) {
796
824
  throw new UsageError(`The Container is closed and cannot be disconnected`);
797
825
  }
798
826
  else {
799
- this.disconnectInternal();
827
+ this.disconnectInternal({ text: "DocumentDisconnect" });
800
828
  }
801
829
  }
802
- disconnectInternal() {
830
+ disconnectInternal(reason) {
803
831
  assert(!this.closed, 0x2c7 /* "Attempting to disconnect() a closed Container" */);
804
832
  // Set Auto Reconnect Mode
805
833
  const mode = ReconnectMode.Disabled;
806
- this.setAutoReconnectInternal(mode);
834
+ this.setAutoReconnectInternal(mode, reason);
807
835
  }
808
836
  resumeInternal(args) {
809
837
  assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
@@ -816,13 +844,6 @@ export class Container extends EventEmitterWithErrorHandling {
816
844
  // Ensure connection to web socket
817
845
  this.connectToDeltaStream(args);
818
846
  }
819
- async getAbsoluteUrl(relativeUrl) {
820
- var _a;
821
- if (this.resolvedUrl === undefined) {
822
- return undefined;
823
- }
824
- return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName((_a = this._context) === null || _a === void 0 ? void 0 : _a.codeDetails));
825
- }
826
847
  async proposeCodeDetails(codeDetails) {
827
848
  if (!isFluidCodeDetails(codeDetails)) {
828
849
  throw new Error("Provided codeDetails are not IFluidCodeDetails");
@@ -844,7 +865,7 @@ export class Container extends EventEmitterWithErrorHandling {
844
865
  this.deltaManager.inbound.pause(),
845
866
  this.deltaManager.inboundSignal.pause(),
846
867
  ]);
847
- if ((await this.context.satisfies(codeDetails)) === true) {
868
+ if ((await this.satisfies(codeDetails)) === true) {
848
869
  this.deltaManager.inbound.resume();
849
870
  this.deltaManager.inboundSignal.resume();
850
871
  return;
@@ -853,6 +874,37 @@ export class Container extends EventEmitterWithErrorHandling {
853
874
  const error = new GenericError("Existing context does not satisfy incoming proposal");
854
875
  this.close(error);
855
876
  }
877
+ /**
878
+ * Determines if the currently loaded module satisfies the incoming constraint code details
879
+ */
880
+ async satisfies(constraintCodeDetails) {
881
+ // If we have no module, it can't satisfy anything.
882
+ if (this._loadedModule === undefined) {
883
+ return false;
884
+ }
885
+ const comparers = [];
886
+ const maybeCompareCodeLoader = this.codeLoader;
887
+ if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
888
+ comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
889
+ }
890
+ const maybeCompareExport = this._loadedModule?.module.fluidExport;
891
+ if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
892
+ comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
893
+ }
894
+ // If there are no comparers, then it's impossible to know if the currently loaded package satisfies
895
+ // the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
896
+ // rather than potentially running with incompatible code.
897
+ if (comparers.length === 0) {
898
+ return false;
899
+ }
900
+ for (const comparer of comparers) {
901
+ const satisfies = await comparer.satisfies(this._loadedModule?.details, constraintCodeDetails);
902
+ if (satisfies === false) {
903
+ return false;
904
+ }
905
+ }
906
+ return true;
907
+ }
856
908
  async getVersion(version) {
857
909
  const versions = await this.storageAdapter.getVersions(version, 1);
858
910
  return versions[0];
@@ -869,8 +921,8 @@ export class Container extends EventEmitterWithErrorHandling {
869
921
  *
870
922
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
871
923
  */
872
- async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState) {
873
- var _a;
924
+ async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
925
+ const timings = { phase1: performance.now() };
874
926
  this.service = await this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType);
875
927
  // Ideally we always connect as "read" by default.
876
928
  // Currently that works with SPO & r11s, because we get "write" connection when connecting to non-existing file.
@@ -882,7 +934,7 @@ export class Container extends EventEmitterWithErrorHandling {
882
934
  // A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
883
935
  // B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
884
936
  const connectionArgs = {
885
- reason: "DocumentOpen",
937
+ reason: { text: "DocumentOpen" },
886
938
  mode: "write",
887
939
  fetchOpsFromStorage: false,
888
940
  };
@@ -901,6 +953,7 @@ export class Container extends EventEmitterWithErrorHandling {
901
953
  });
902
954
  }
903
955
  this._attachState = AttachState.Attached;
956
+ timings.phase2 = performance.now();
904
957
  // Fetch specified snapshot.
905
958
  const { snapshot, versionId } = pendingLocalState === undefined
906
959
  ? await this.fetchSnapshotTree(specifiedVersion)
@@ -914,14 +967,56 @@ export class Container extends EventEmitterWithErrorHandling {
914
967
  if (this.offlineLoadEnabled) {
915
968
  this.baseSnapshot = snapshot;
916
969
  // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
917
- this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
970
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storageAdapter);
918
971
  }
919
972
  }
920
973
  const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshot);
921
974
  // If we saved ops, we will replay them and don't need DeltaManager to fetch them
922
- const sequenceNumber = (_a = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.savedOps[pendingLocalState.savedOps.length - 1]) === null || _a === void 0 ? void 0 : _a.sequenceNumber;
923
- const dmAttributes = sequenceNumber !== undefined ? Object.assign(Object.assign({}, attributes), { sequenceNumber }) : attributes;
975
+ const sequenceNumber = pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber;
976
+ const dmAttributes = sequenceNumber !== undefined ? { ...attributes, sequenceNumber } : attributes;
924
977
  let opsBeforeReturnP;
978
+ if (loadMode.pauseAfterLoad === true) {
979
+ // If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
980
+ if (loadMode.opsBeforeReturn === "sequenceNumber") {
981
+ assert(loadToSequenceNumber !== undefined, 0x727 /* sequenceNumber should be defined */);
982
+ // Note: It is possible that we think the latest snapshot is newer than the specified sequence number
983
+ // due to saved ops that may be replayed after the snapshot.
984
+ // https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
985
+ if (dmAttributes.sequenceNumber > loadToSequenceNumber) {
986
+ throw new Error("Cannot satisfy request to pause the container at the specified sequence number. Most recent snapshot is newer than the specified sequence number.");
987
+ }
988
+ }
989
+ // Force readonly mode - this will ensure we don't receive an error for the lack of join op
990
+ this.forceReadonly(true);
991
+ // We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
992
+ const opHandler = () => {
993
+ if (loadToSequenceNumber === undefined) {
994
+ // If there is no specified sequence number, pause after the inbound queue is empty.
995
+ if (this.deltaManager.inbound.length !== 0) {
996
+ return;
997
+ }
998
+ }
999
+ else {
1000
+ // If there is a specified sequence number, keep processing until we reach it.
1001
+ if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
1002
+ return;
1003
+ }
1004
+ }
1005
+ // Pause op processing once we have processed the desired number of ops.
1006
+ void this.deltaManager.inbound.pause();
1007
+ void this.deltaManager.outbound.pause();
1008
+ this.off("op", opHandler);
1009
+ };
1010
+ if ((loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
1011
+ this.deltaManager.lastSequenceNumber === loadToSequenceNumber) {
1012
+ // If we have already reached the desired sequence number, call opHandler() to pause immediately.
1013
+ opHandler();
1014
+ }
1015
+ else {
1016
+ // If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
1017
+ this.on("op", opHandler);
1018
+ }
1019
+ }
925
1020
  // Attach op handlers to finish initialization and be able to start processing ops
926
1021
  // Kick off any ops fetching if required.
927
1022
  switch (loadMode.opsBeforeReturn) {
@@ -930,11 +1025,10 @@ export class Container extends EventEmitterWithErrorHandling {
930
1025
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
931
1026
  this.attachDeltaManagerOpHandler(dmAttributes, loadMode.deltaConnection !== "none" ? "all" : "none");
932
1027
  break;
1028
+ case "sequenceNumber":
933
1029
  case "cached":
934
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
935
- break;
936
1030
  case "all":
937
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
1031
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, loadMode.opsBeforeReturn);
938
1032
  break;
939
1033
  default:
940
1034
  unreachableCase(loadMode.opsBeforeReturn);
@@ -942,20 +1036,19 @@ export class Container extends EventEmitterWithErrorHandling {
942
1036
  // ...load in the existing quorum
943
1037
  // Initialize the protocol handler
944
1038
  await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
1039
+ timings.phase3 = performance.now();
945
1040
  const codeDetails = this.getCodeDetailsFromQuorum();
946
- await this.instantiateContext(true, // existing
947
- codeDetails, snapshot, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.pendingRuntimeState);
1041
+ await this.instantiateRuntime(codeDetails, snapshot,
1042
+ // give runtime a dummy value so it knows we're loading from a stash blob
1043
+ pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined);
948
1044
  // replay saved ops
949
1045
  if (pendingLocalState) {
950
1046
  for (const message of pendingLocalState.savedOps) {
951
1047
  this.processRemoteMessage(message);
952
1048
  // allow runtime to apply stashed ops at this op's sequence number
953
- await this.context.notifyOpReplay(message);
1049
+ await this.runtime.notifyOpReplay?.(message);
954
1050
  }
955
1051
  pendingLocalState.savedOps = [];
956
- // now set clientId to stashed clientId so live ops are correctly processed as local
957
- assert(this.clientId === undefined, 0x5d6 /* Unexpected clientId when setting stashed clientId */);
958
- this._clientId = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.clientId;
959
1052
  }
960
1053
  // We might have hit some failure that did not manifest itself in exception in this flow,
961
1054
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
@@ -967,24 +1060,20 @@ export class Container extends EventEmitterWithErrorHandling {
967
1060
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
968
1061
  this._deltaManager.inbound.pause();
969
1062
  }
970
- switch (loadMode.deltaConnection) {
971
- case undefined:
972
- if (pendingLocalState) {
973
- // connect to delta stream now since we did not before
974
- this.connectToDeltaStream(connectionArgs);
1063
+ this.handleDeltaConnectionArg(connectionArgs, loadMode.deltaConnection, pendingLocalState !== undefined);
1064
+ }
1065
+ // If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
1066
+ if (loadToSequenceNumber !== undefined &&
1067
+ this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
1068
+ await new Promise((resolve, reject) => {
1069
+ const opHandler = (message) => {
1070
+ if (message.sequenceNumber >= loadToSequenceNumber) {
1071
+ resolve();
1072
+ this.off("op", opHandler);
975
1073
  }
976
- // intentional fallthrough
977
- case "delayed":
978
- assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
979
- this.inboundQueuePausedFromInit = false;
980
- this._deltaManager.inbound.resume();
981
- this._deltaManager.inboundSignal.resume();
982
- break;
983
- case "none":
984
- break;
985
- default:
986
- unreachableCase(loadMode.deltaConnection);
987
- }
1074
+ };
1075
+ this.on("op", opHandler);
1076
+ });
988
1077
  }
989
1078
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
990
1079
  // But if that did not happen for some reason, fail load for sure.
@@ -996,6 +1085,11 @@ export class Container extends EventEmitterWithErrorHandling {
996
1085
  }
997
1086
  // Internal context is fully loaded at this point
998
1087
  this.setLoaded();
1088
+ timings.end = performance.now();
1089
+ this.subLogger.sendTelemetryEvent({
1090
+ eventName: "LoadStagesTimings",
1091
+ details: JSON.stringify(timings),
1092
+ }, undefined, LogLevel.verbose);
999
1093
  return {
1000
1094
  sequenceNumber: attributes.sequenceNumber,
1001
1095
  version: versionId,
@@ -1003,7 +1097,7 @@ export class Container extends EventEmitterWithErrorHandling {
1003
1097
  dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
1004
1098
  };
1005
1099
  }
1006
- async createDetached(source) {
1100
+ async createDetached(codeDetails) {
1007
1101
  const attributes = {
1008
1102
  sequenceNumber: detachedContainerRefSeqNumber,
1009
1103
  term: OnlyValidTermValue,
@@ -1011,14 +1105,13 @@ export class Container extends EventEmitterWithErrorHandling {
1011
1105
  };
1012
1106
  await this.attachDeltaManagerOpHandler(attributes);
1013
1107
  // Need to just seed the source data in the code quorum. Quorum itself is empty
1014
- const qValues = initQuorumValuesFromCodeDetails(source);
1108
+ const qValues = initQuorumValuesFromCodeDetails(codeDetails);
1015
1109
  this.initializeProtocolState(attributes, {
1016
1110
  members: [],
1017
1111
  proposals: [],
1018
1112
  values: qValues,
1019
1113
  });
1020
- // The load context - given we seeded the quorum - will be great
1021
- await this.instantiateContextDetached(false);
1114
+ await this.instantiateRuntime(codeDetails, undefined);
1022
1115
  this.setLoaded();
1023
1116
  }
1024
1117
  async rehydrateDetachedFromSnapshot(detachedContainerSnapshot) {
@@ -1033,14 +1126,13 @@ export class Container extends EventEmitterWithErrorHandling {
1033
1126
  // Initialize the protocol handler
1034
1127
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1035
1128
  const qValues = await readAndParse(this.storageAdapter, baseTree.blobs.quorumValues);
1036
- const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1037
1129
  this.initializeProtocolState(attributes, {
1038
1130
  members: [],
1039
1131
  proposals: [],
1040
- values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1132
+ values: qValues,
1041
1133
  });
1042
- await this.instantiateContextDetached(true, // existing
1043
- snapshotTree);
1134
+ const codeDetails = this.getCodeDetailsFromQuorum();
1135
+ await this.instantiateRuntime(codeDetails, snapshotTree);
1044
1136
  this.setLoaded();
1045
1137
  }
1046
1138
  async getDocumentAttributes(storage, tree) {
@@ -1077,7 +1169,10 @@ export class Container extends EventEmitterWithErrorHandling {
1077
1169
  }
1078
1170
  initializeProtocolState(attributes, quorumSnapshot) {
1079
1171
  const protocol = this.protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })));
1080
- const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1172
+ const protocolLogger = createChildLogger({
1173
+ logger: this.subLogger,
1174
+ namespace: "ProtocolHandler",
1175
+ });
1081
1176
  protocol.quorum.on("error", (error) => {
1082
1177
  protocolLogger.sendErrorEvent(error);
1083
1178
  });
@@ -1139,8 +1234,7 @@ export class Container extends EventEmitterWithErrorHandling {
1139
1234
  return pkg;
1140
1235
  }
1141
1236
  get client() {
1142
- var _a;
1143
- const client = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.client) !== undefined
1237
+ const client = this.options?.client !== undefined
1144
1238
  ? this.options.client
1145
1239
  : {
1146
1240
  details: {
@@ -1171,7 +1265,7 @@ export class Container extends EventEmitterWithErrorHandling {
1171
1265
  }
1172
1266
  createDeltaManager() {
1173
1267
  const serviceProvider = () => this.service;
1174
- const deltaManager = new DeltaManager(serviceProvider, ChildLogger.create(this.subLogger, "DeltaManager"), () => this.activeConnection(), (props) => new ConnectionManager(serviceProvider, () => this.isDirty, this.client, this._canReconnect, ChildLogger.create(this.subLogger, "ConnectionManager"), props));
1268
+ const deltaManager = new DeltaManager(serviceProvider, createChildLogger({ logger: this.subLogger, namespace: "DeltaManager" }), () => this.activeConnection(), (props) => new ConnectionManager(serviceProvider, () => this.isDirty, this.client, this._canReconnect, createChildLogger({ logger: this.subLogger, namespace: "ConnectionManager" }), props));
1175
1269
  // Disable inbound queues as Container is not ready to accept any ops until we are fully loaded!
1176
1270
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1177
1271
  deltaManager.inbound.pause();
@@ -1187,11 +1281,10 @@ export class Container extends EventEmitterWithErrorHandling {
1187
1281
  deltaManager.on("cancelEstablishingConnection", (reason) => {
1188
1282
  this.connectionStateHandler.cancelEstablishingConnection(reason);
1189
1283
  });
1190
- deltaManager.on("disconnect", (reason, error) => {
1191
- var _a;
1192
- (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1284
+ deltaManager.on("disconnect", (reason) => {
1285
+ this.noopHeuristic?.notifyDisconnect();
1193
1286
  if (!this.closed) {
1194
- this.connectionStateHandler.receivedDisconnectEvent(reason, error);
1287
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1195
1288
  }
1196
1289
  });
1197
1290
  deltaManager.on("throttled", (warning) => {
@@ -1223,8 +1316,7 @@ export class Container extends EventEmitterWithErrorHandling {
1223
1316
  },
1224
1317
  }, prefetchType);
1225
1318
  }
1226
- logConnectionStateChangeTelemetry(value, oldState, reason, error) {
1227
- var _a;
1319
+ logConnectionStateChangeTelemetry(value, oldState, reason) {
1228
1320
  // Log actual event
1229
1321
  const time = performance.now();
1230
1322
  this.connectionTransitionTimes[value] = time;
@@ -1241,30 +1333,44 @@ export class Container extends EventEmitterWithErrorHandling {
1241
1333
  if (value === ConnectionState.Connected) {
1242
1334
  durationFromDisconnected =
1243
1335
  time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1244
- durationFromDisconnected = TelemetryLogger.formatTick(durationFromDisconnected);
1336
+ durationFromDisconnected = formatTick(durationFromDisconnected);
1245
1337
  }
1246
1338
  else if (value === ConnectionState.CatchingUp) {
1247
1339
  // This info is of most interesting while Catching Up.
1248
1340
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1249
- if (this.deltaManager.hasCheckpointSequenceNumber) {
1341
+ // Need to check that we have already loaded and fetched the snapshot.
1342
+ if (this.deltaManager.hasCheckpointSequenceNumber &&
1343
+ this._lifecycleState === "loaded") {
1250
1344
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1251
1345
  }
1252
1346
  }
1253
1347
  connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1254
1348
  }
1255
- this.mc.logger.sendPerformanceEvent(Object.assign({ eventName: `ConnectionStateChange_${ConnectionState[value]}`, from: ConnectionState[oldState], duration,
1349
+ this.mc.logger.sendPerformanceEvent({
1350
+ eventName: `ConnectionStateChange_${ConnectionState[value]}`,
1351
+ from: ConnectionState[oldState],
1352
+ duration,
1256
1353
  durationFromDisconnected,
1257
- reason,
1258
- connectionInitiationReason, pendingClientId: this.connectionStateHandler.pendingClientId, clientId: this.clientId, autoReconnect,
1259
- opsBehind, online: OnlineStatus[isOnline()], lastVisible: this.lastVisible !== undefined
1354
+ reason: reason?.text,
1355
+ connectionInitiationReason,
1356
+ pendingClientId: this.connectionStateHandler.pendingClientId,
1357
+ clientId: this.clientId,
1358
+ autoReconnect,
1359
+ opsBehind,
1360
+ online: OnlineStatus[isOnline()],
1361
+ lastVisible: this.lastVisible !== undefined
1260
1362
  ? performance.now() - this.lastVisible
1261
- : undefined, checkpointSequenceNumber, quorumSize: (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.quorum.getMembers().size }, this._deltaManager.connectionProps), error);
1363
+ : undefined,
1364
+ checkpointSequenceNumber,
1365
+ quorumSize: this._protocolHandler?.quorum.getMembers().size,
1366
+ isDirty: this.isDirty,
1367
+ ...this._deltaManager.connectionProps,
1368
+ }, reason?.error);
1262
1369
  if (value === ConnectionState.Connected) {
1263
1370
  this.firstConnection = false;
1264
1371
  }
1265
1372
  }
1266
1373
  propagateConnectionState(initialTransition, disconnectedReason) {
1267
- var _a;
1268
1374
  // When container loaded, we want to propagate initial connection state.
1269
1375
  // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1270
1376
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
@@ -1274,22 +1380,10 @@ export class Container extends EventEmitterWithErrorHandling {
1274
1380
  return;
1275
1381
  }
1276
1382
  const state = this.connectionState === ConnectionState.Connected;
1277
- const logOpsOnReconnect = this.connectionState === ConnectionState.Connected &&
1278
- !this.firstConnection &&
1279
- this.connectionMode === "write";
1280
- if (logOpsOnReconnect) {
1281
- this.messageCountAfterDisconnection = 0;
1282
- }
1283
1383
  // Both protocol and context should not be undefined if we got so far.
1284
- this.setContextConnectedState(state, (_a = this.readOnlyInfo.readonly) !== null && _a !== void 0 ? _a : false);
1384
+ this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
1285
1385
  this.protocolHandler.setConnectionState(state, this.clientId);
1286
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1287
- if (logOpsOnReconnect) {
1288
- this.mc.logger.sendTelemetryEvent({
1289
- eventName: "OpsSentOnReconnect",
1290
- count: this.messageCountAfterDisconnection,
1291
- });
1292
- }
1386
+ raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
1293
1387
  }
1294
1388
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1295
1389
  submitContainerMessage(type, contents, batch, metadata) {
@@ -1327,13 +1421,11 @@ export class Container extends EventEmitterWithErrorHandling {
1327
1421
  return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */, undefined /* metadata */, undefined /* compression */, referenceSequenceNumber);
1328
1422
  }
1329
1423
  submitMessage(type, contents, batch, metadata, compression, referenceSequenceNumber) {
1330
- var _a;
1331
1424
  if (this.connectionState !== ConnectionState.Connected) {
1332
1425
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1333
1426
  return -1;
1334
1427
  }
1335
- this.messageCountAfterDisconnection += 1;
1336
- (_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
1428
+ this.noopHeuristic?.notifyMessageSent();
1337
1429
  return this._deltaManager.submit(type, contents, batch, metadata, compression, referenceSequenceNumber);
1338
1430
  }
1339
1431
  processRemoteMessage(message) {
@@ -1344,21 +1436,31 @@ export class Container extends EventEmitterWithErrorHandling {
1344
1436
  // Allow the protocol handler to process the message
1345
1437
  const result = this.protocolHandler.processMessage(message, local);
1346
1438
  // Forward messages to the loaded runtime for processing
1347
- this.context.process(message, local);
1439
+ this.runtime.process(message, local);
1348
1440
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1349
1441
  if (this.activeConnection()) {
1350
- if (this.collabWindowTracker === undefined) {
1442
+ if (this.noopHeuristic === undefined) {
1443
+ const serviceConfiguration = this.deltaManager.serviceConfiguration;
1351
1444
  // Note that config from first connection will be used for this container's lifetime.
1352
1445
  // That means that if relay service changes settings, such changes will impact only newly booted
1353
1446
  // clients.
1354
1447
  // All existing will continue to use settings they got earlier.
1355
- assert(this.serviceConfiguration !== undefined, 0x2e4 /* "there should be service config for active connection" */);
1356
- this.collabWindowTracker = new CollabWindowTracker((type) => {
1357
- assert(this.activeConnection(), 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1358
- this.submitMessage(type);
1359
- }, this.serviceConfiguration.noopTimeFrequency, this.serviceConfiguration.noopCountFrequency);
1448
+ assert(serviceConfiguration !== undefined, 0x2e4 /* "there should be service config for active connection" */);
1449
+ this.noopHeuristic = new NoopHeuristic(serviceConfiguration.noopTimeFrequency, serviceConfiguration.noopCountFrequency);
1450
+ this.noopHeuristic.on("wantsNoop", () => {
1451
+ // On disconnect we notify the heuristic which should prevent it from wanting a noop.
1452
+ // Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
1453
+ // running the microtask that the heuristic queued in response.
1454
+ assert(this.activeConnection(), 0x241 /* "Trying to send noop without active connection" */);
1455
+ this.submitMessage(MessageType.NoOp);
1456
+ });
1457
+ }
1458
+ this.noopHeuristic.notifyMessageProcessed(message);
1459
+ // The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
1460
+ if (result.immediateNoOp === true) {
1461
+ // ADO:1385: Remove cast and use MessageType once definition changes propagate
1462
+ this.submitMessage(MessageType2.Accept);
1360
1463
  }
1361
- this.collabWindowTracker.scheduleSequenceNumberUpdate(message, result.immediateNoOp === true);
1362
1464
  }
1363
1465
  this.emit("op", message);
1364
1466
  }
@@ -1367,12 +1469,12 @@ export class Container extends EventEmitterWithErrorHandling {
1367
1469
  }
1368
1470
  processSignal(message) {
1369
1471
  // No clientId indicates a system signal message.
1370
- if (message.clientId === null) {
1472
+ if (protocolHandlerShouldProcessSignal(message)) {
1371
1473
  this.protocolHandler.processSignal(message);
1372
1474
  }
1373
1475
  else {
1374
1476
  const local = this.clientId === message.clientId;
1375
- this.context.processSignal(message, local);
1477
+ this.runtime.processSignal(message, local);
1376
1478
  }
1377
1479
  }
1378
1480
  /**
@@ -1381,8 +1483,7 @@ export class Container extends EventEmitterWithErrorHandling {
1381
1483
  * @returns The snapshot requested, or the latest snapshot if no version was specified, plus version ID
1382
1484
  */
1383
1485
  async fetchSnapshotTree(specifiedVersion) {
1384
- var _a;
1385
- const version = await this.getVersion(specifiedVersion !== null && specifiedVersion !== void 0 ? specifiedVersion : null);
1486
+ const version = await this.getVersion(specifiedVersion ?? null);
1386
1487
  if (version === undefined && specifiedVersion !== undefined) {
1387
1488
  // We should have a defined version to load from if specified version requested
1388
1489
  this.mc.logger.sendErrorEvent({
@@ -1391,35 +1492,38 @@ export class Container extends EventEmitterWithErrorHandling {
1391
1492
  });
1392
1493
  }
1393
1494
  this._loadedFromVersion = version;
1394
- const snapshot = (_a = (await this.storageAdapter.getSnapshotTree(version))) !== null && _a !== void 0 ? _a : undefined;
1495
+ const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
1395
1496
  if (snapshot === undefined && version !== undefined) {
1396
1497
  this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1397
1498
  }
1398
- return { snapshot, versionId: version === null || version === void 0 ? void 0 : version.id };
1399
- }
1400
- async instantiateContextDetached(existing, snapshot) {
1401
- const codeDetails = this.getCodeDetailsFromQuorum();
1402
- if (codeDetails === undefined) {
1403
- throw new Error("pkg should be provided in create flow!!");
1404
- }
1405
- await this.instantiateContext(existing, codeDetails, snapshot);
1499
+ return { snapshot, versionId: version?.id };
1406
1500
  }
1407
- async instantiateContext(existing, codeDetails, snapshot, pendingLocalState) {
1408
- var _a;
1409
- assert(((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) !== false, 0x0dd /* "Existing context not disposed" */);
1501
+ async instantiateRuntime(codeDetails, snapshot, pendingLocalState) {
1502
+ assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
1410
1503
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
1411
1504
  // are set. Global requests will still go directly to the loader
1412
1505
  const maybeLoader = this.scope;
1413
1506
  const loader = new RelativeLoader(this, maybeLoader.ILoader);
1414
- 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) => this.dispose(error), (error) => this.close(error), Container.version, (dirty) => this.updateDirtyContainerState(dirty), existing, pendingLocalState);
1415
- this.emit("contextChanged", codeDetails);
1416
- }
1417
- updateDirtyContainerState(dirty) {
1418
- if (this._dirtyContainer === dirty) {
1419
- return;
1507
+ const loadCodeResult = await PerformanceEvent.timedExecAsync(this.subLogger, { eventName: "CodeLoad" }, async () => this.codeLoader.load(codeDetails));
1508
+ this._loadedModule = {
1509
+ module: loadCodeResult.module,
1510
+ // An older interface ICodeLoader could return an IFluidModule which didn't have details.
1511
+ // If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
1512
+ // TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
1513
+ details: loadCodeResult.details ?? codeDetails,
1514
+ };
1515
+ const fluidExport = this._loadedModule.module.fluidExport;
1516
+ const runtimeFactory = fluidExport?.IRuntimeFactory;
1517
+ if (runtimeFactory === undefined) {
1518
+ throw new Error(packageNotFactoryError);
1420
1519
  }
1421
- this._dirtyContainer = dirty;
1422
- this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
1520
+ const getSpecifiedCodeDetails = () => (this.protocolHandler.quorum.get("code") ??
1521
+ this.protocolHandler.quorum.get("code2"));
1522
+ const existing = snapshot !== undefined;
1523
+ const context = new ContainerContext(this.options, this.scope, snapshot, this._loadedFromVersion, this._deltaManager, this.storageAdapter, this.protocolHandler.quorum, this.protocolHandler.audience, 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) => this.dispose(error), (error) => this.close(error), this.updateDirtyContainerState, this.getAbsoluteUrl, () => this.resolvedUrl?.id, () => this.clientId, () => this.attachState, () => this.connected, getSpecifiedCodeDetails, this._deltaManager.clientDetails, existing, this.subLogger, pendingLocalState);
1524
+ this._runtime = await PerformanceEvent.timedExecAsync(this.subLogger, { eventName: "InstantiateRuntime" }, async () => runtimeFactory.instantiateRuntime(context, existing));
1525
+ this._lifecycleEvents.emit("runtimeInstantiated");
1526
+ this._loadedCodeDetails = codeDetails;
1423
1527
  }
1424
1528
  /**
1425
1529
  * Set the connected state of the ContainerContext
@@ -1428,17 +1532,35 @@ export class Container extends EventEmitterWithErrorHandling {
1428
1532
  * @param readonly - Is the container in readonly mode?
1429
1533
  */
1430
1534
  setContextConnectedState(state, readonly) {
1431
- var _a;
1432
- if (((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) === false) {
1535
+ if (this._runtime?.disposed === false) {
1433
1536
  /**
1434
1537
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1435
1538
  * ops getting through to the DeltaManager.
1436
1539
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
1437
1540
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1438
1541
  */
1439
- this.context.setConnectionState(state && !readonly, this.clientId);
1542
+ this.runtime.setConnectionState(state && !readonly, this.clientId);
1543
+ }
1544
+ }
1545
+ handleDeltaConnectionArg(connectionArgs, deltaConnectionArg, canConnect = true) {
1546
+ switch (deltaConnectionArg) {
1547
+ case undefined:
1548
+ if (canConnect) {
1549
+ // connect to delta stream now since we did not before
1550
+ this.connectToDeltaStream(connectionArgs);
1551
+ }
1552
+ // intentional fallthrough
1553
+ case "delayed":
1554
+ assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
1555
+ this.inboundQueuePausedFromInit = false;
1556
+ this._deltaManager.inbound.resume();
1557
+ this._deltaManager.inboundSignal.resume();
1558
+ break;
1559
+ case "none":
1560
+ break;
1561
+ default:
1562
+ unreachableCase(deltaConnectionArg);
1440
1563
  }
1441
1564
  }
1442
1565
  }
1443
- Container.version = "^0.1.0";
1444
1566
  //# sourceMappingURL=container.js.map