@fluidframework/container-loader 2.0.0-dev.4.4.0.162574 → 2.0.0-dev.5.3.2.178189

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 (179) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +27 -3
  3. package/dist/catchUpMonitor.d.ts +1 -1
  4. package/dist/catchUpMonitor.d.ts.map +1 -1
  5. package/dist/catchUpMonitor.js.map +1 -1
  6. package/dist/connectionManager.d.ts +3 -2
  7. package/dist/connectionManager.d.ts.map +1 -1
  8. package/dist/connectionManager.js +32 -13
  9. package/dist/connectionManager.js.map +1 -1
  10. package/dist/connectionStateHandler.d.ts +18 -3
  11. package/dist/connectionStateHandler.d.ts.map +1 -1
  12. package/dist/connectionStateHandler.js +34 -9
  13. package/dist/connectionStateHandler.js.map +1 -1
  14. package/dist/container.d.ts +99 -70
  15. package/dist/container.d.ts.map +1 -1
  16. package/dist/container.js +260 -218
  17. package/dist/container.js.map +1 -1
  18. package/dist/containerContext.d.ts +24 -67
  19. package/dist/containerContext.d.ts.map +1 -1
  20. package/dist/containerContext.js +28 -217
  21. package/dist/containerContext.js.map +1 -1
  22. package/dist/containerStorageAdapter.d.ts +3 -3
  23. package/dist/containerStorageAdapter.d.ts.map +1 -1
  24. package/dist/containerStorageAdapter.js +29 -6
  25. package/dist/containerStorageAdapter.js.map +1 -1
  26. package/dist/contracts.d.ts +9 -3
  27. package/dist/contracts.d.ts.map +1 -1
  28. package/dist/contracts.js.map +1 -1
  29. package/dist/deltaManager.d.ts +22 -9
  30. package/dist/deltaManager.d.ts.map +1 -1
  31. package/dist/deltaManager.js +42 -31
  32. package/dist/deltaManager.js.map +1 -1
  33. package/dist/deltaQueue.d.ts +2 -3
  34. package/dist/deltaQueue.d.ts.map +1 -1
  35. package/dist/deltaQueue.js +2 -3
  36. package/dist/deltaQueue.js.map +1 -1
  37. package/dist/disposal.d.ts +13 -0
  38. package/dist/disposal.d.ts.map +1 -0
  39. package/dist/disposal.js +25 -0
  40. package/dist/disposal.js.map +1 -0
  41. package/dist/index.d.ts +1 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/loader.d.ts +9 -8
  45. package/dist/loader.d.ts.map +1 -1
  46. package/dist/loader.js +47 -61
  47. package/dist/loader.js.map +1 -1
  48. package/dist/noopHeuristic.d.ts +23 -0
  49. package/dist/noopHeuristic.d.ts.map +1 -0
  50. package/dist/{collabWindowTracker.js → noopHeuristic.js} +30 -42
  51. package/dist/noopHeuristic.js.map +1 -0
  52. package/dist/packageVersion.d.ts +1 -1
  53. package/dist/packageVersion.js +1 -1
  54. package/dist/packageVersion.js.map +1 -1
  55. package/dist/protocol.d.ts +7 -12
  56. package/dist/protocol.d.ts.map +1 -1
  57. package/dist/protocol.js +17 -19
  58. package/dist/protocol.js.map +1 -1
  59. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  60. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  61. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  62. package/dist/quorum.d.ts +1 -17
  63. package/dist/quorum.d.ts.map +1 -1
  64. package/dist/quorum.js +1 -17
  65. package/dist/quorum.js.map +1 -1
  66. package/dist/retriableDocumentStorageService.d.ts +3 -2
  67. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  68. package/dist/retriableDocumentStorageService.js.map +1 -1
  69. package/dist/tsdoc-metadata.json +11 -0
  70. package/dist/utils.d.ts +2 -0
  71. package/dist/utils.d.ts.map +1 -1
  72. package/dist/utils.js +8 -1
  73. package/dist/utils.js.map +1 -1
  74. package/lib/catchUpMonitor.d.ts +1 -1
  75. package/lib/catchUpMonitor.d.ts.map +1 -1
  76. package/lib/catchUpMonitor.js.map +1 -1
  77. package/lib/connectionManager.d.ts +3 -2
  78. package/lib/connectionManager.d.ts.map +1 -1
  79. package/lib/connectionManager.js +33 -14
  80. package/lib/connectionManager.js.map +1 -1
  81. package/lib/connectionStateHandler.d.ts +18 -3
  82. package/lib/connectionStateHandler.d.ts.map +1 -1
  83. package/lib/connectionStateHandler.js +35 -10
  84. package/lib/connectionStateHandler.js.map +1 -1
  85. package/lib/container.d.ts +99 -70
  86. package/lib/container.d.ts.map +1 -1
  87. package/lib/container.js +264 -222
  88. package/lib/container.js.map +1 -1
  89. package/lib/containerContext.d.ts +24 -67
  90. package/lib/containerContext.d.ts.map +1 -1
  91. package/lib/containerContext.js +28 -217
  92. package/lib/containerContext.js.map +1 -1
  93. package/lib/containerStorageAdapter.d.ts +3 -3
  94. package/lib/containerStorageAdapter.d.ts.map +1 -1
  95. package/lib/containerStorageAdapter.js +29 -6
  96. package/lib/containerStorageAdapter.js.map +1 -1
  97. package/lib/contracts.d.ts +9 -3
  98. package/lib/contracts.d.ts.map +1 -1
  99. package/lib/contracts.js.map +1 -1
  100. package/lib/deltaManager.d.ts +22 -9
  101. package/lib/deltaManager.d.ts.map +1 -1
  102. package/lib/deltaManager.js +44 -33
  103. package/lib/deltaManager.js.map +1 -1
  104. package/lib/deltaQueue.d.ts +2 -3
  105. package/lib/deltaQueue.d.ts.map +1 -1
  106. package/lib/deltaQueue.js +2 -3
  107. package/lib/deltaQueue.js.map +1 -1
  108. package/lib/disposal.d.ts +13 -0
  109. package/lib/disposal.d.ts.map +1 -0
  110. package/lib/disposal.js +21 -0
  111. package/lib/disposal.js.map +1 -0
  112. package/lib/index.d.ts +1 -2
  113. package/lib/index.d.ts.map +1 -1
  114. package/lib/index.js +1 -1
  115. package/lib/index.js.map +1 -1
  116. package/lib/loader.d.ts +9 -8
  117. package/lib/loader.d.ts.map +1 -1
  118. package/lib/loader.js +47 -61
  119. package/lib/loader.js.map +1 -1
  120. package/lib/noopHeuristic.d.ts +23 -0
  121. package/lib/noopHeuristic.d.ts.map +1 -0
  122. package/lib/{collabWindowTracker.js → noopHeuristic.js} +30 -42
  123. package/lib/noopHeuristic.js.map +1 -0
  124. package/lib/packageVersion.d.ts +1 -1
  125. package/lib/packageVersion.js +1 -1
  126. package/lib/packageVersion.js.map +1 -1
  127. package/lib/protocol.d.ts +7 -12
  128. package/lib/protocol.d.ts.map +1 -1
  129. package/lib/protocol.js +15 -18
  130. package/lib/protocol.js.map +1 -1
  131. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  132. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  133. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  134. package/lib/quorum.d.ts +1 -17
  135. package/lib/quorum.d.ts.map +1 -1
  136. package/lib/quorum.js +1 -16
  137. package/lib/quorum.js.map +1 -1
  138. package/lib/retriableDocumentStorageService.d.ts +3 -2
  139. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  140. package/lib/retriableDocumentStorageService.js.map +1 -1
  141. package/lib/utils.d.ts +2 -0
  142. package/lib/utils.d.ts.map +1 -1
  143. package/lib/utils.js +7 -1
  144. package/lib/utils.js.map +1 -1
  145. package/package.json +22 -20
  146. package/src/catchUpMonitor.ts +1 -1
  147. package/src/connectionManager.ts +40 -22
  148. package/src/connectionStateHandler.ts +66 -17
  149. package/src/container.ts +464 -292
  150. package/src/containerContext.ts +33 -341
  151. package/src/containerStorageAdapter.ts +40 -10
  152. package/src/contracts.ts +11 -3
  153. package/src/deltaManager.ts +74 -45
  154. package/src/deltaQueue.ts +2 -3
  155. package/src/disposal.ts +25 -0
  156. package/src/index.ts +1 -8
  157. package/src/loader.ts +85 -83
  158. package/src/{collabWindowTracker.ts → noopHeuristic.ts} +37 -47
  159. package/src/packageVersion.ts +1 -1
  160. package/src/protocol.ts +18 -39
  161. package/src/protocolTreeDocumentStorageService.ts +1 -1
  162. package/src/quorum.ts +2 -31
  163. package/src/retriableDocumentStorageService.ts +4 -2
  164. package/src/utils.ts +15 -1
  165. package/dist/collabWindowTracker.d.ts +0 -19
  166. package/dist/collabWindowTracker.d.ts.map +0 -1
  167. package/dist/collabWindowTracker.js.map +0 -1
  168. package/dist/deltaManagerProxy.d.ts +0 -42
  169. package/dist/deltaManagerProxy.d.ts.map +0 -1
  170. package/dist/deltaManagerProxy.js +0 -79
  171. package/dist/deltaManagerProxy.js.map +0 -1
  172. package/lib/collabWindowTracker.d.ts +0 -19
  173. package/lib/collabWindowTracker.d.ts.map +0 -1
  174. package/lib/collabWindowTracker.js.map +0 -1
  175. package/lib/deltaManagerProxy.d.ts +0 -42
  176. package/lib/deltaManagerProxy.d.ts.map +0 -1
  177. package/lib/deltaManagerProxy.js +0 -74
  178. package/lib/deltaManagerProxy.js.map +0 -1
  179. package/src/deltaManagerProxy.ts +0 -109
package/src/container.ts CHANGED
@@ -7,13 +7,21 @@
7
7
  import merge from "lodash/merge";
8
8
 
9
9
  import { v4 as uuid } from "uuid";
10
+ import { IEvent } from "@fluidframework/common-definitions";
11
+ import {
12
+ TypedEventEmitter,
13
+ assert,
14
+ performance,
15
+ unreachableCase,
16
+ } from "@fluidframework/common-utils";
10
17
  import {
11
- ITelemetryLogger,
12
18
  ITelemetryProperties,
13
19
  TelemetryEventCategory,
14
- } from "@fluidframework/common-definitions";
15
- import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
16
- import { IRequest, IResponse, IFluidRouter, FluidObject } from "@fluidframework/core-interfaces";
20
+ IRequest,
21
+ IResponse,
22
+ IFluidRouter,
23
+ FluidObject,
24
+ } from "@fluidframework/core-interfaces";
17
25
  import {
18
26
  IAudience,
19
27
  IConnectionDetailsInternal,
@@ -29,29 +37,36 @@ import {
29
37
  IFluidCodeDetails,
30
38
  isFluidCodeDetails,
31
39
  IBatchMessage,
40
+ ICodeDetailsLoader,
41
+ IHostLoader,
42
+ IFluidModuleWithDetails,
43
+ IProvideRuntimeFactory,
44
+ IProvideFluidCodeDetailsComparer,
45
+ IFluidCodeDetailsComparer,
46
+ IRuntime,
32
47
  } from "@fluidframework/container-definitions";
33
48
  import { GenericError, UsageError } from "@fluidframework/container-utils";
34
49
  import {
35
50
  IAnyDriverError,
36
51
  IDocumentService,
52
+ IDocumentServiceFactory,
37
53
  IDocumentStorageService,
38
- IFluidResolvedUrl,
39
54
  IResolvedUrl,
55
+ IUrlResolver,
40
56
  } from "@fluidframework/driver-definitions";
41
57
  import {
42
58
  readAndParse,
43
59
  OnlineStatus,
44
60
  isOnline,
45
- ensureFluidResolvedUrl,
46
61
  combineAppAndProtocolSummary,
47
62
  runWithRetry,
48
- isFluidResolvedUrl,
49
63
  isCombinedAppAndProtocolSummary,
64
+ MessageType2,
65
+ canBeCoalescedByService,
50
66
  } from "@fluidframework/driver-utils";
51
67
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
52
68
  import {
53
69
  IClient,
54
- IClientConfiguration,
55
70
  IClientDetails,
56
71
  ICommittedProposal,
57
72
  IDocumentAttributes,
@@ -80,13 +95,13 @@ import {
80
95
  MonitoringContext,
81
96
  loggerToMonitoringContext,
82
97
  wrapError,
98
+ ITelemetryLoggerExt,
83
99
  } from "@fluidframework/telemetry-utils";
84
100
  import { Audience } from "./audience";
85
101
  import { ContainerContext } from "./containerContext";
86
102
  import { ReconnectMode, IConnectionManagerFactoryArgs, getPackageName } from "./contracts";
87
103
  import { DeltaManager, IConnectionArgs } from "./deltaManager";
88
- import { DeltaManagerProxy } from "./deltaManagerProxy";
89
- import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
104
+ import { IDetachedBlobStorage, ILoaderOptions, RelativeLoader } from "./loader";
90
105
  import { pkgVersion } from "./packageVersion";
91
106
  import {
92
107
  ContainerStorageAdapter,
@@ -96,19 +111,16 @@ import {
96
111
  } from "./containerStorageAdapter";
97
112
  import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
98
113
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
99
- import {
100
- initQuorumValuesFromCodeDetails,
101
- getCodeDetailsFromQuorumValues,
102
- QuorumProxy,
103
- } from "./quorum";
104
- import { CollabWindowTracker } from "./collabWindowTracker";
114
+ import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues } from "./quorum";
115
+ import { NoopHeuristic } from "./noopHeuristic";
105
116
  import { ConnectionManager } from "./connectionManager";
106
117
  import { ConnectionState } from "./connectionState";
107
118
  import {
108
- OnlyValidTermValue,
109
119
  IProtocolHandler,
120
+ OnlyValidTermValue,
110
121
  ProtocolHandler,
111
122
  ProtocolHandlerBuilder,
123
+ protocolHandlerShouldProcessSignal,
112
124
  } from "./protocol";
113
125
 
114
126
  const detachedContainerRefSeqNumber = 0;
@@ -116,45 +128,85 @@ const detachedContainerRefSeqNumber = 0;
116
128
  const dirtyContainerEvent = "dirty";
117
129
  const savedContainerEvent = "saved";
118
130
 
131
+ const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
132
+
119
133
  /**
120
- * @deprecated this is an internal interface and will not longer be exported in future versions
121
134
  * @internal
122
135
  */
123
- export interface IContainerLoadOptions {
124
- /**
125
- * Disables the Container from reconnecting if false, allows reconnect otherwise.
126
- */
127
- canReconnect?: boolean;
136
+ export interface IContainerLoadProps {
128
137
  /**
129
- * Client details provided in the override will be merged over the default client.
138
+ * The resolved url of the container being loaded
130
139
  */
131
- clientDetailsOverride?: IClientDetails;
132
- resolvedUrl: IFluidResolvedUrl;
140
+ readonly resolvedUrl: IResolvedUrl;
133
141
  /**
134
142
  * Control which snapshot version to load from. See IParsedUrl for detailed information.
135
143
  */
136
- version: string | undefined;
144
+ readonly version: string | undefined;
137
145
  /**
138
146
  * Loads the Container in paused state if true, unpaused otherwise.
139
147
  */
140
- loadMode?: IContainerLoadMode;
148
+ readonly loadMode?: IContainerLoadMode;
149
+
150
+ /**
151
+ * The pending state serialized from a pervious container instance
152
+ */
153
+ readonly pendingLocalState?: IPendingContainerState;
141
154
  }
142
155
 
143
156
  /**
144
- * @deprecated this is an internal interface and will not longer be exported in future versions
145
157
  * @internal
146
158
  */
147
- export interface IContainerConfig {
148
- resolvedUrl?: IFluidResolvedUrl;
149
- canReconnect?: boolean;
159
+ export interface IContainerCreateProps {
160
+ /**
161
+ * Disables the Container from reconnecting if false, allows reconnect otherwise.
162
+ */
163
+ readonly canReconnect?: boolean;
150
164
  /**
151
165
  * Client details provided in the override will be merged over the default client.
152
166
  */
153
- clientDetailsOverride?: IClientDetails;
167
+ readonly clientDetailsOverride?: IClientDetails;
168
+
169
+ /**
170
+ * The url resolver used by the loader for resolving external urls
171
+ * into Fluid urls such that the container specified by the
172
+ * external url can be loaded.
173
+ */
174
+ readonly urlResolver: IUrlResolver;
175
+ /**
176
+ * The document service factory take the Fluid url provided
177
+ * by the resolved url and constructs all the necessary services
178
+ * for communication with the container's server.
179
+ */
180
+ readonly documentServiceFactory: IDocumentServiceFactory;
181
+ /**
182
+ * The code loader handles loading the necessary code
183
+ * for running a container once it is loaded.
184
+ */
185
+ readonly codeLoader: ICodeDetailsLoader;
186
+
154
187
  /**
155
- * Serialized state from a previous instance of this container
188
+ * A property bag of options used by various layers
189
+ * to control features
156
190
  */
157
- serializedContainerState?: IPendingContainerState;
191
+ readonly options: ILoaderOptions;
192
+
193
+ /**
194
+ * Scope is provided to all container and is a set of shared
195
+ * services for container's to integrate with their host environment.
196
+ */
197
+ readonly scope: FluidObject;
198
+
199
+ /**
200
+ * The logger downstream consumers should construct their loggers from
201
+ */
202
+ readonly subLogger: ITelemetryLoggerExt;
203
+
204
+ /**
205
+ * Blobs storage for detached containers.
206
+ */
207
+ readonly detachedBlobStorage?: IDetachedBlobStorage;
208
+
209
+ readonly protocolHandlerBuilder?: ProtocolHandlerBuilder;
158
210
  }
159
211
 
160
212
  /**
@@ -260,7 +312,7 @@ const getCodeProposal =
260
312
  * @param action - functor to call and measure
261
313
  */
262
314
  export async function ReportIfTooLong(
263
- logger: ITelemetryLogger,
315
+ logger: ITelemetryLoggerExt,
264
316
  eventName: string,
265
317
  action: () => Promise<ITelemetryProperties>,
266
318
  ) {
@@ -274,7 +326,6 @@ export async function ReportIfTooLong(
274
326
  /**
275
327
  * State saved by a container at close time, to be used to load a new instance
276
328
  * of the container to the same state
277
- * @deprecated this is an internal interface and will not longer be exported in future versions
278
329
  * @internal
279
330
  */
280
331
  export interface IPendingContainerState {
@@ -301,34 +352,29 @@ export interface IPendingContainerState {
301
352
 
302
353
  const summarizerClientType = "summarizer";
303
354
 
304
- /**
305
- * @deprecated - In the next release Container will no longer be exported, IContainer should be used in its place.
306
- */
355
+ interface IContainerLifecycleEvents extends IEvent {
356
+ (event: "runtimeInstantiated", listener: () => void): void;
357
+ (event: "disposed", listener: () => void): void;
358
+ }
359
+
307
360
  export class Container
308
361
  extends EventEmitterWithErrorHandling<IContainerEvents>
309
362
  implements IContainer, IContainerExperimental
310
363
  {
311
- public static version = "^0.1.0";
312
-
313
364
  /**
314
365
  * Load an existing container.
315
366
  * @internal
316
367
  */
317
368
  public static async load(
318
- loader: Loader,
319
- loadOptions: IContainerLoadOptions,
320
- pendingLocalState?: IPendingContainerState,
321
- protocolHandlerBuilder?: ProtocolHandlerBuilder,
369
+ loadProps: IContainerLoadProps,
370
+ createProps: IContainerCreateProps,
322
371
  ): Promise<Container> {
323
- const container = new Container(
324
- loader,
325
- {
326
- clientDetailsOverride: loadOptions.clientDetailsOverride,
327
- resolvedUrl: loadOptions.resolvedUrl,
328
- canReconnect: loadOptions.canReconnect,
329
- serializedContainerState: pendingLocalState,
330
- },
331
- protocolHandlerBuilder,
372
+ const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
373
+
374
+ const container = new Container(createProps, loadProps);
375
+
376
+ const disableRecordHeapSize = container.mc.config.getBoolean(
377
+ "Fluid.Loader.DisableRecordHeapSize",
332
378
  );
333
379
 
334
380
  return PerformanceEvent.timedExecAsync(
@@ -336,14 +382,12 @@ export class Container
336
382
  { eventName: "Load" },
337
383
  async (event) =>
338
384
  new Promise<Container>((resolve, reject) => {
339
- const version = loadOptions.version;
340
-
341
385
  const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" };
342
386
  // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
343
387
  // to return container, so ignore this value and use undefined for opsBeforeReturn
344
388
  const mode: IContainerLoadMode = pendingLocalState
345
- ? { ...(loadOptions.loadMode ?? defaultMode), opsBeforeReturn: undefined }
346
- : loadOptions.loadMode ?? defaultMode;
389
+ ? { ...(loadMode ?? defaultMode), opsBeforeReturn: undefined }
390
+ : loadMode ?? defaultMode;
347
391
 
348
392
  const onClosed = (err?: ICriticalContainerError) => {
349
393
  // pre-0.58 error message: containerClosedWithoutErrorDuringLoad
@@ -354,13 +398,13 @@ export class Container
354
398
  container.on("closed", onClosed);
355
399
 
356
400
  container
357
- .load(version, mode, pendingLocalState)
401
+ .load(version, mode, resolvedUrl, pendingLocalState)
358
402
  .finally(() => {
359
403
  container.removeListener("closed", onClosed);
360
404
  })
361
405
  .then(
362
406
  (props) => {
363
- event.end({ ...props, ...loadOptions.loadMode });
407
+ event.end({ ...props, ...loadMode });
364
408
  resolve(container);
365
409
  },
366
410
  (error) => {
@@ -374,6 +418,7 @@ export class Container
374
418
  );
375
419
  }),
376
420
  { start: true, end: true, cancel: "generic" },
421
+ disableRecordHeapSize !== true /* recordHeapSize */,
377
422
  );
378
423
  }
379
424
 
@@ -381,11 +426,10 @@ export class Container
381
426
  * Create a new container in a detached state.
382
427
  */
383
428
  public static async createDetached(
384
- loader: Loader,
429
+ createProps: IContainerCreateProps,
385
430
  codeDetails: IFluidCodeDetails,
386
- protocolHandlerBuilder?: ProtocolHandlerBuilder,
387
431
  ): Promise<Container> {
388
- const container = new Container(loader, {}, protocolHandlerBuilder);
432
+ const container = new Container(createProps);
389
433
 
390
434
  return PerformanceEvent.timedExecAsync(
391
435
  container.mc.logger,
@@ -403,11 +447,10 @@ export class Container
403
447
  * snapshot from a previous detached container.
404
448
  */
405
449
  public static async rehydrateDetachedFromSnapshot(
406
- loader: Loader,
450
+ createProps: IContainerCreateProps,
407
451
  snapshot: string,
408
- protocolHandlerBuilder?: ProtocolHandlerBuilder,
409
452
  ): Promise<Container> {
410
- const container = new Container(loader, {}, protocolHandlerBuilder);
453
+ const container = new Container(createProps);
411
454
 
412
455
  return PerformanceEvent.timedExecAsync(
413
456
  container.mc.logger,
@@ -421,14 +464,30 @@ export class Container
421
464
  );
422
465
  }
423
466
 
424
- public subLogger: TelemetryLogger;
425
-
426
467
  // Tells if container can reconnect on losing fist connection
427
468
  // If false, container gets closed on loss of connection.
428
- private readonly _canReconnect: boolean = true;
469
+ private readonly _canReconnect: boolean;
470
+ private readonly clientDetailsOverride: IClientDetails | undefined;
471
+ private readonly urlResolver: IUrlResolver;
472
+ private readonly serviceFactory: IDocumentServiceFactory;
473
+ private readonly codeLoader: ICodeDetailsLoader;
474
+ private readonly options: ILoaderOptions;
475
+ private readonly scope: FluidObject;
476
+ private readonly subLogger: TelemetryLogger;
477
+ private readonly detachedBlobStorage: IDetachedBlobStorage | undefined;
478
+ private readonly protocolHandlerBuilder: ProtocolHandlerBuilder;
429
479
 
430
480
  private readonly mc: MonitoringContext;
431
481
 
482
+ /**
483
+ * Used by the RelativeLoader to spawn a new Container for the same document. Used to create the summarizing client.
484
+ * @internal
485
+ */
486
+ public readonly clone: (
487
+ loadProps: IContainerLoadProps,
488
+ createParamOverrides: Partial<IContainerCreateProps>,
489
+ ) => Promise<Container>;
490
+
432
491
  /**
433
492
  * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
434
493
  *
@@ -474,20 +533,16 @@ export class Container
474
533
  private _attachState = AttachState.Detached;
475
534
 
476
535
  private readonly storageAdapter: ContainerStorageAdapter;
477
- public get storage(): IDocumentStorageService {
478
- return this.storageAdapter;
479
- }
480
536
 
481
- private readonly clientDetailsOverride: IClientDetails | undefined;
482
537
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
483
538
  private service: IDocumentService | undefined;
484
539
 
485
- private _context: ContainerContext | undefined;
486
- private get context() {
487
- if (this._context === undefined) {
488
- throw new GenericError("Attempted to access context before it was defined");
540
+ private _runtime: IRuntime | undefined;
541
+ private get runtime() {
542
+ if (this._runtime === undefined) {
543
+ throw new Error("Attempted to access runtime before it was defined");
489
544
  }
490
- return this._context;
545
+ return this._runtime;
491
546
  }
492
547
  private _protocolHandler: IProtocolHandler | undefined;
493
548
  private get protocolHandler() {
@@ -503,7 +558,6 @@ export class Container
503
558
  private readonly connectionTransitionTimes: number[] = [];
504
559
  private messageCountAfterDisconnection: number = 0;
505
560
  private _loadedFromVersion: IVersion | undefined;
506
- private _resolvedUrl: IFluidResolvedUrl | undefined;
507
561
  private attachStarted = false;
508
562
  private _dirtyContainer = false;
509
563
  private readonly savedOps: ISequencedDocumentMessage[] = [];
@@ -513,10 +567,11 @@ export class Container
513
567
  private lastVisible: number | undefined;
514
568
  private readonly visibilityEventHandler: (() => void) | undefined;
515
569
  private readonly connectionStateHandler: IConnectionStateHandler;
570
+ private readonly clientsWhoShouldHaveLeft = new Set<string>();
516
571
 
517
572
  private setAutoReconnectTime = performance.now();
518
573
 
519
- private collabWindowTracker: CollabWindowTracker | undefined;
574
+ private noopHeuristic: NoopHeuristic | undefined;
520
575
 
521
576
  private get connectionMode() {
522
577
  return this._deltaManager.connectionManager.connectionMode;
@@ -527,21 +582,24 @@ export class Container
527
582
  }
528
583
 
529
584
  public get resolvedUrl(): IResolvedUrl | undefined {
530
- return this._resolvedUrl;
531
- }
532
-
533
- public get loadedFromVersion(): IVersion | undefined {
534
- return this._loadedFromVersion;
585
+ /**
586
+ * All attached containers will have a document service,
587
+ * this is required, as attached containers are attached to
588
+ * a service. Detached containers will neither have a document
589
+ * service or a resolved url as they only exist locally.
590
+ * in order to create a document service a resolved url must
591
+ * first be obtained, this is how the container is identified.
592
+ * Because of this, the document service's resolved url
593
+ * is always the same as the containers, as we had to
594
+ * obtain the resolved url, and then create the service from it.
595
+ */
596
+ return this.service?.resolvedUrl;
535
597
  }
536
598
 
537
599
  public get readOnlyInfo(): ReadOnlyInfo {
538
600
  return this._deltaManager.readOnlyInfo;
539
601
  }
540
602
 
541
- public get closeSignal(): AbortSignal {
542
- return this._deltaManager.closeAbortController.signal;
543
- }
544
-
545
603
  /**
546
604
  * Tracks host requiring read-only mode.
547
605
  */
@@ -557,18 +615,10 @@ export class Container
557
615
  return this.connectionStateHandler.connectionState;
558
616
  }
559
617
 
560
- public get connected(): boolean {
618
+ private get connected(): boolean {
561
619
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
562
620
  }
563
621
 
564
- /**
565
- * Service configuration details. If running in offline mode will be undefined otherwise will contain service
566
- * configuration details returned as part of the initial connection.
567
- */
568
- public get serviceConfiguration(): IClientConfiguration | undefined {
569
- return this._deltaManager.serviceConfiguration;
570
- }
571
-
572
622
  private _clientId: string | undefined;
573
623
 
574
624
  /**
@@ -579,24 +629,12 @@ export class Container
579
629
  return this._clientId;
580
630
  }
581
631
 
582
- /**
583
- * The server provided claims of the client.
584
- * Set once this.connected is true, otherwise undefined
585
- */
586
- public get scopes(): string[] | undefined {
587
- return this._deltaManager.connectionManager.scopes;
588
- }
589
-
590
- public get clientDetails(): IClientDetails {
591
- return this._deltaManager.clientDetails;
592
- }
593
-
594
632
  private get offlineLoadEnabled(): boolean {
595
633
  const enabled =
596
634
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
597
635
  this.options?.enableOfflineLoad === true;
598
636
  // summarizer will not have any pending state we want to save
599
- return enabled && this.clientDetails.capabilities.interactive;
637
+ return enabled && this.deltaManager.clientDetails.capabilities.interactive;
600
638
  }
601
639
 
602
640
  /**
@@ -607,15 +645,18 @@ export class Container
607
645
  return this.getCodeDetailsFromQuorum();
608
646
  }
609
647
 
648
+ private _loadedCodeDetails: IFluidCodeDetails | undefined;
610
649
  /**
611
650
  * Get the code details that were used to load the container.
612
651
  * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
613
652
  * loaded.
614
653
  */
615
654
  public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
616
- return this._context?.codeDetails;
655
+ return this._loadedCodeDetails;
617
656
  }
618
657
 
658
+ private _loadedModule: IFluidModuleWithDetails | undefined;
659
+
619
660
  /**
620
661
  * Retrieves the audience associated with the document
621
662
  */
@@ -632,62 +673,42 @@ export class Container
632
673
  return this._dirtyContainer;
633
674
  }
634
675
 
635
- private get serviceFactory() {
636
- return this.loader.services.documentServiceFactory;
637
- }
638
- private get urlResolver() {
639
- return this.loader.services.urlResolver;
640
- }
641
- public readonly options: ILoaderOptions;
642
- private get scope() {
643
- return this.loader.services.scope;
644
- }
645
- private get codeLoader() {
646
- return this.loader.services.codeLoader;
647
- }
648
-
649
676
  /**
650
677
  * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
651
678
  */
652
- public async getEntryPoint?(): Promise<FluidObject | undefined> {
653
- // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
654
- // allow it since they mean a kind of read-only state for the Container.
655
- // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
656
- if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
657
- throw new UsageError("The container is disposing or disposed");
679
+ public async getEntryPoint(): Promise<FluidObject | undefined> {
680
+ if (this._disposed) {
681
+ throw new UsageError("The context is already disposed");
658
682
  }
659
- while (this._context === undefined) {
660
- await new Promise<void>((resolve, reject) => {
661
- const contextChangedHandler = () => {
662
- resolve();
663
- this.off("disposed", disposedHandler);
664
- };
665
- const disposedHandler = (error) => {
666
- reject(error ?? "The Container is disposed");
667
- this.off("contextChanged", contextChangedHandler);
668
- };
669
- this.once("contextChanged", contextChangedHandler);
670
- this.once("disposed", disposedHandler);
671
- });
672
- // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
673
- // should have set this._context; making sure.
674
- assert(
675
- this._context !== undefined,
676
- 0x5a2 /* Context still not defined after contextChanged event */,
677
- );
683
+ if (this._runtime !== undefined) {
684
+ return this._runtime.getEntryPoint?.();
678
685
  }
679
- // Disable lint rule for the sake of more complete stack traces
680
- // eslint-disable-next-line no-return-await
681
- return await this._context.getEntryPoint?.();
686
+ return new Promise<FluidObject | undefined>((resolve, reject) => {
687
+ const runtimeInstantiatedHandler = () => {
688
+ assert(
689
+ this._runtime !== undefined,
690
+ 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */,
691
+ );
692
+ resolve(this._runtime.getEntryPoint?.());
693
+ this._lifecycleEvents.off("disposed", disposedHandler);
694
+ };
695
+ const disposedHandler = () => {
696
+ reject(new Error("ContainerContext was disposed"));
697
+ this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
698
+ };
699
+ this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
700
+ this._lifecycleEvents.once("disposed", disposedHandler);
701
+ });
682
702
  }
683
703
 
704
+ private readonly _lifecycleEvents = new TypedEventEmitter<IContainerLifecycleEvents>();
705
+
684
706
  /**
685
707
  * @internal
686
708
  */
687
709
  constructor(
688
- private readonly loader: Loader,
689
- config: IContainerConfig,
690
- private readonly protocolHandlerBuilder?: ProtocolHandlerBuilder,
710
+ createProps: IContainerCreateProps,
711
+ loadProps?: Pick<IContainerLoadProps, "pendingLocalState">,
691
712
  ) {
692
713
  super((name, error) => {
693
714
  this.mc.logger.sendErrorEvent(
@@ -699,11 +720,46 @@ export class Container
699
720
  );
700
721
  });
701
722
 
702
- this.clientDetailsOverride = config.clientDetailsOverride;
703
- this._resolvedUrl = config.resolvedUrl;
704
- if (config.canReconnect !== undefined) {
705
- this._canReconnect = config.canReconnect;
706
- }
723
+ const {
724
+ canReconnect,
725
+ clientDetailsOverride,
726
+ urlResolver,
727
+ documentServiceFactory,
728
+ codeLoader,
729
+ options,
730
+ scope,
731
+ subLogger,
732
+ detachedBlobStorage,
733
+ protocolHandlerBuilder,
734
+ } = createProps;
735
+
736
+ this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
737
+ const pendingLocalState = loadProps?.pendingLocalState;
738
+
739
+ this._canReconnect = canReconnect ?? true;
740
+ this.clientDetailsOverride = clientDetailsOverride;
741
+ this.urlResolver = urlResolver;
742
+ this.serviceFactory = documentServiceFactory;
743
+ this.codeLoader = codeLoader;
744
+ // Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
745
+ // all clients that were loaded from the same loader (including summarizer clients).
746
+ // Tracking alternative ways to handle this in AB#4129.
747
+ this.options = { ...options };
748
+ this.scope = scope;
749
+ this.detachedBlobStorage = detachedBlobStorage;
750
+ this.protocolHandlerBuilder =
751
+ protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
752
+
753
+ // Note that we capture the createProps here so we can replicate the creation call when we want to clone.
754
+ this.clone = async (
755
+ _loadProps: IContainerLoadProps,
756
+ createParamOverrides: Partial<IContainerCreateProps>,
757
+ ) => {
758
+ return Container.load(_loadProps, {
759
+ ...createProps,
760
+ ...createParamOverrides,
761
+ });
762
+ };
707
763
 
708
764
  // Create logger for data stores to use
709
765
  const type = this.client.details.type;
@@ -713,15 +769,15 @@ export class Container
713
769
  }`;
714
770
  // Need to use the property getter for docId because for detached flow we don't have the docId initially.
715
771
  // We assign the id later so property getter is used.
716
- this.subLogger = ChildLogger.create(loader.services.subLogger, undefined, {
772
+ this.subLogger = ChildLogger.create(subLogger, undefined, {
717
773
  all: {
718
774
  clientType, // Differentiating summarizer container from main container
719
775
  containerId: uuid(),
720
- docId: () => this._resolvedUrl?.id ?? undefined,
776
+ docId: () => this.resolvedUrl?.id,
721
777
  containerAttachState: () => this._attachState,
722
778
  containerLifecycleState: () => this._lifecycleState,
723
779
  containerConnectionState: () => ConnectionState[this.connectionState],
724
- serializedContainer: config.serializedContainerState !== undefined,
780
+ serializedContainer: pendingLocalState !== undefined,
725
781
  },
726
782
  // we need to be judicious with our logging here to avoid generating too much data
727
783
  // all data logged here should be broadly applicable, and not specific to a
@@ -731,13 +787,16 @@ export class Container
731
787
  dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
732
788
  dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
733
789
  dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
734
- containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
735
- containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
790
+ containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
791
+ containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
736
792
  // message information to associate errors with the specific execution state
737
793
  // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
738
794
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
739
795
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
740
- dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
796
+ dmLastMsqSeqClientId: () =>
797
+ this.deltaManager?.lastMessage?.clientId === null
798
+ ? "null"
799
+ : this.deltaManager?.lastMessage?.clientId,
741
800
  dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
742
801
  connectionStateDuration: () =>
743
802
  performance.now() - this.connectionTransitionTimes[this.connectionState],
@@ -747,13 +806,6 @@ export class Container
747
806
  // Prefix all events in this file with container-loader
748
807
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
749
808
 
750
- // Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
751
- // all clients that were loaded from the same loader (including summarizer clients).
752
- // Tracking alternative ways to handle this in AB#4129.
753
- this.options = {
754
- ...this.loader.services.options,
755
- };
756
-
757
809
  this._deltaManager = this.createDeltaManager();
758
810
 
759
811
  this.connectionStateHandler = createConnectionStateHandler(
@@ -774,7 +826,7 @@ export class Container
774
826
  }
775
827
  },
776
828
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
777
- maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
829
+ maxClientLeaveWaitTime: options.maxClientLeaveWaitTime,
778
830
  logConnectionIssue: (
779
831
  eventName: string,
780
832
  category: TelemetryEventCategory,
@@ -809,9 +861,12 @@ export class Container
809
861
  this.connect();
810
862
  }
811
863
  },
864
+ clientShouldHaveLeft: (clientId: string) => {
865
+ this.clientsWhoShouldHaveLeft.add(clientId);
866
+ },
812
867
  },
813
868
  this.deltaManager,
814
- config.serializedContainerState?.clientId,
869
+ pendingLocalState?.clientId,
815
870
  );
816
871
 
817
872
  this.on(savedContainerEvent, () => {
@@ -830,12 +885,12 @@ export class Container
830
885
  // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
831
886
  const forceEnableSummarizeProtocolTree =
832
887
  this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
833
- this.loader.services.options.summarizeProtocolTree;
888
+ options.summarizeProtocolTree;
834
889
 
835
890
  this.storageAdapter = new ContainerStorageAdapter(
836
- this.loader.services.detachedBlobStorage,
891
+ detachedBlobStorage,
837
892
  this.mc.logger,
838
- config.serializedContainerState?.snapshotBlobs,
893
+ pendingLocalState?.snapshotBlobs,
839
894
  addProtocolSummaryIfMissing,
840
895
  forceEnableSummarizeProtocolTree,
841
896
  );
@@ -869,8 +924,8 @@ export class Container
869
924
  return this.protocolHandler.quorum;
870
925
  }
871
926
 
872
- public dispose?(error?: ICriticalContainerError) {
873
- this._deltaManager.close(error, true /* doDispose */);
927
+ public dispose(error?: ICriticalContainerError) {
928
+ this._deltaManager.dispose(error);
874
929
  this.verifyClosed();
875
930
  }
876
931
 
@@ -921,15 +976,6 @@ export class Container
921
976
  this._protocolHandler?.close();
922
977
 
923
978
  this.connectionStateHandler.dispose();
924
-
925
- this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
926
-
927
- this.storageAdapter.dispose();
928
-
929
- // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
930
- // about file, like file being overwritten in storage, but client having stale local cache.
931
- // Driver need to ensure all caches are cleared on critical errors
932
- this.service?.dispose(error);
933
979
  } catch (exception) {
934
980
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
935
981
  }
@@ -941,6 +987,11 @@ export class Container
941
987
  }
942
988
  } finally {
943
989
  this._lifecycleState = "closed";
990
+
991
+ // There is no user for summarizer, so we need to ensure dispose is called
992
+ if (this.client.details.type === summarizerClientType) {
993
+ this.dispose(error);
994
+ }
944
995
  }
945
996
  }
946
997
 
@@ -957,7 +1008,8 @@ export class Container
957
1008
  this.mc.logger.sendTelemetryEvent(
958
1009
  {
959
1010
  eventName: "ContainerDispose",
960
- category: "generic",
1011
+ // Only log error if container isn't closed
1012
+ category: !this.closed && error !== undefined ? "error" : "generic",
961
1013
  },
962
1014
  error,
963
1015
  );
@@ -971,7 +1023,8 @@ export class Container
971
1023
 
972
1024
  this.connectionStateHandler.dispose();
973
1025
 
974
- this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
1026
+ const maybeError = error !== undefined ? new Error(error.message) : undefined;
1027
+ this._runtime?.dispose(maybeError);
975
1028
 
976
1029
  this.storageAdapter.dispose();
977
1030
 
@@ -994,6 +1047,7 @@ export class Container
994
1047
  }
995
1048
  } finally {
996
1049
  this._lifecycleState = "disposed";
1050
+ this._lifecycleEvents.emit("disposed");
997
1051
  }
998
1052
  }
999
1053
 
@@ -1010,6 +1064,11 @@ export class Container
1010
1064
  if (!this.offlineLoadEnabled) {
1011
1065
  throw new UsageError("Can't get pending local state unless offline load is enabled");
1012
1066
  }
1067
+ if (this.closed || this._disposed) {
1068
+ throw new UsageError(
1069
+ "Pending state cannot be retried if the container is closed or disposed",
1070
+ );
1071
+ }
1013
1072
  assert(
1014
1073
  this.attachState === AttachState.Attached,
1015
1074
  0x0d1 /* "Container should be attached before close" */,
@@ -1021,7 +1080,7 @@ export class Container
1021
1080
  assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1022
1081
  assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1023
1082
  const pendingState: IPendingContainerState = {
1024
- pendingRuntimeState: this.context.getPendingLocalState(),
1083
+ pendingRuntimeState: this.runtime.getPendingLocalState(),
1025
1084
  baseSnapshot: this.baseSnapshot,
1026
1085
  snapshotBlobs: this.baseSnapshotBlobs,
1027
1086
  savedOps: this.savedOps,
@@ -1045,14 +1104,11 @@ export class Container
1045
1104
  0x0d3 /* "Should only be called in detached container" */,
1046
1105
  );
1047
1106
 
1048
- const appSummary: ISummaryTree = this.context.createSummary();
1107
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1049
1108
  const protocolSummary = this.captureProtocolSummary();
1050
1109
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1051
1110
 
1052
- if (
1053
- this.loader.services.detachedBlobStorage &&
1054
- this.loader.services.detachedBlobStorage.size > 0
1055
- ) {
1111
+ if (this.detachedBlobStorage && this.detachedBlobStorage.size > 0) {
1056
1112
  combinedSummary.tree[".hasAttachmentBlobs"] = {
1057
1113
  type: SummaryType.Blob,
1058
1114
  content: "true",
@@ -1082,8 +1138,7 @@ export class Container
1082
1138
 
1083
1139
  // If attachment blobs were uploaded in detached state we will go through a different attach flow
1084
1140
  const hasAttachmentBlobs =
1085
- this.loader.services.detachedBlobStorage !== undefined &&
1086
- this.loader.services.detachedBlobStorage.size > 0;
1141
+ this.detachedBlobStorage !== undefined && this.detachedBlobStorage.size > 0;
1087
1142
 
1088
1143
  try {
1089
1144
  assert(
@@ -1095,7 +1150,7 @@ export class Container
1095
1150
  if (!hasAttachmentBlobs) {
1096
1151
  // Get the document state post attach - possibly can just call attach but we need to change the
1097
1152
  // semantics around what the attach means as far as async code goes.
1098
- const appSummary: ISummaryTree = this.context.createSummary();
1153
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1099
1154
  const protocolSummary = this.captureProtocolSummary();
1100
1155
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1101
1156
 
@@ -1104,6 +1159,7 @@ export class Container
1104
1159
  // starting to attach the container to storage.
1105
1160
  // Also, this should only be fired in detached container.
1106
1161
  this._attachState = AttachState.Attaching;
1162
+ this.runtime.setAttachState(AttachState.Attaching);
1107
1163
  this.emit("attaching");
1108
1164
  if (this.offlineLoadEnabled) {
1109
1165
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1114,11 +1170,11 @@ export class Container
1114
1170
  }
1115
1171
 
1116
1172
  // Actually go and create the resolved document
1117
- const createNewResolvedUrl = await this.urlResolver.resolve(request);
1118
- ensureFluidResolvedUrl(createNewResolvedUrl);
1119
1173
  if (this.service === undefined) {
1174
+ const createNewResolvedUrl = await this.urlResolver.resolve(request);
1120
1175
  assert(
1121
- this.client.details.type !== summarizerClientType,
1176
+ this.client.details.type !== summarizerClientType &&
1177
+ createNewResolvedUrl !== undefined,
1122
1178
  0x2c4 /* "client should not be summarizer before container is created" */,
1123
1179
  );
1124
1180
  this.service = await runWithRetry(
@@ -1132,19 +1188,16 @@ export class Container
1132
1188
  "containerAttach",
1133
1189
  this.mc.logger,
1134
1190
  {
1135
- cancel: this.closeSignal,
1191
+ cancel: this._deltaManager.closeAbortController.signal,
1136
1192
  }, // progress
1137
1193
  );
1138
1194
  }
1139
- const resolvedUrl = this.service.resolvedUrl;
1140
- ensureFluidResolvedUrl(resolvedUrl);
1141
- this._resolvedUrl = resolvedUrl;
1142
1195
  await this.storageAdapter.connectToService(this.service);
1143
1196
 
1144
1197
  if (hasAttachmentBlobs) {
1145
1198
  // upload blobs to storage
1146
1199
  assert(
1147
- !!this.loader.services.detachedBlobStorage,
1200
+ !!this.detachedBlobStorage,
1148
1201
  0x24e /* "assertion for type narrowing" */,
1149
1202
  );
1150
1203
 
@@ -1152,24 +1205,24 @@ export class Container
1152
1205
  // support blob handles that only know about the local IDs
1153
1206
  const redirectTable = new Map<string, string>();
1154
1207
  // if new blobs are added while uploading, upload them too
1155
- while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
1156
- const newIds = this.loader.services.detachedBlobStorage
1208
+ while (redirectTable.size < this.detachedBlobStorage.size) {
1209
+ const newIds = this.detachedBlobStorage
1157
1210
  .getBlobIds()
1158
1211
  .filter((id) => !redirectTable.has(id));
1159
1212
  for (const id of newIds) {
1160
- const blob =
1161
- await this.loader.services.detachedBlobStorage.readBlob(id);
1213
+ const blob = await this.detachedBlobStorage.readBlob(id);
1162
1214
  const response = await this.storageAdapter.createBlob(blob);
1163
1215
  redirectTable.set(id, response.id);
1164
1216
  }
1165
1217
  }
1166
1218
 
1167
1219
  // take summary and upload
1168
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
1220
+ const appSummary: ISummaryTree = this.runtime.createSummary(redirectTable);
1169
1221
  const protocolSummary = this.captureProtocolSummary();
1170
1222
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1171
1223
 
1172
1224
  this._attachState = AttachState.Attaching;
1225
+ this.runtime.setAttachState(AttachState.Attaching);
1173
1226
  this.emit("attaching");
1174
1227
  if (this.offlineLoadEnabled) {
1175
1228
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1186,6 +1239,7 @@ export class Container
1186
1239
  }
1187
1240
 
1188
1241
  this._attachState = AttachState.Attached;
1242
+ this.runtime.setAttachState(AttachState.Attached);
1189
1243
  this.emit("attached");
1190
1244
 
1191
1245
  if (!this.closed) {
@@ -1197,12 +1251,8 @@ export class Container
1197
1251
  } catch (error) {
1198
1252
  // add resolved URL on error object so that host has the ability to find this document and delete it
1199
1253
  const newError = normalizeError(error);
1200
- const resolvedUrl = this.resolvedUrl;
1201
- if (isFluidResolvedUrl(resolvedUrl)) {
1202
- newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
1203
- }
1254
+ newError.addTelemetryProperties({ resolvedUrl: this.resolvedUrl?.url });
1204
1255
  this.close(newError);
1205
- this.dispose?.(newError);
1206
1256
  throw newError;
1207
1257
  }
1208
1258
  },
@@ -1214,7 +1264,7 @@ export class Container
1214
1264
  return PerformanceEvent.timedExecAsync(
1215
1265
  this.mc.logger,
1216
1266
  { eventName: "Request" },
1217
- async () => this.context.request(path),
1267
+ async () => this.runtime.request(path),
1218
1268
  { end: true, cancel: "error" },
1219
1269
  );
1220
1270
  }
@@ -1299,7 +1349,7 @@ export class Container
1299
1349
  this.connectToDeltaStream(args);
1300
1350
  }
1301
1351
 
1302
- public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1352
+ public readonly getAbsoluteUrl = async (relativeUrl: string): Promise<string | undefined> => {
1303
1353
  if (this.resolvedUrl === undefined) {
1304
1354
  return undefined;
1305
1355
  }
@@ -1307,9 +1357,9 @@ export class Container
1307
1357
  return this.urlResolver.getAbsoluteUrl(
1308
1358
  this.resolvedUrl,
1309
1359
  relativeUrl,
1310
- getPackageName(this._context?.codeDetails),
1360
+ getPackageName(this._loadedCodeDetails),
1311
1361
  );
1312
- }
1362
+ };
1313
1363
 
1314
1364
  public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1315
1365
  if (!isFluidCodeDetails(codeDetails)) {
@@ -1340,7 +1390,7 @@ export class Container
1340
1390
  this.deltaManager.inboundSignal.pause(),
1341
1391
  ]);
1342
1392
 
1343
- if ((await this.context.satisfies(codeDetails)) === true) {
1393
+ if ((await this.satisfies(codeDetails)) === true) {
1344
1394
  this.deltaManager.inbound.resume();
1345
1395
  this.deltaManager.inboundSignal.resume();
1346
1396
  return;
@@ -1349,7 +1399,47 @@ export class Container
1349
1399
  // pre-0.58 error message: existingContextDoesNotSatisfyIncomingProposal
1350
1400
  const error = new GenericError("Existing context does not satisfy incoming proposal");
1351
1401
  this.close(error);
1352
- this.dispose?.(error);
1402
+ }
1403
+
1404
+ /**
1405
+ * Determines if the currently loaded module satisfies the incoming constraint code details
1406
+ */
1407
+ private async satisfies(constraintCodeDetails: IFluidCodeDetails) {
1408
+ // If we have no module, it can't satisfy anything.
1409
+ if (this._loadedModule === undefined) {
1410
+ return false;
1411
+ }
1412
+
1413
+ const comparers: IFluidCodeDetailsComparer[] = [];
1414
+
1415
+ const maybeCompareCodeLoader = this.codeLoader;
1416
+ if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
1417
+ comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
1418
+ }
1419
+
1420
+ const maybeCompareExport: Partial<IProvideFluidCodeDetailsComparer> | undefined =
1421
+ this._loadedModule?.module.fluidExport;
1422
+ if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
1423
+ comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
1424
+ }
1425
+
1426
+ // If there are no comparers, then it's impossible to know if the currently loaded package satisfies
1427
+ // the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
1428
+ // rather than potentially running with incompatible code.
1429
+ if (comparers.length === 0) {
1430
+ return false;
1431
+ }
1432
+
1433
+ for (const comparer of comparers) {
1434
+ const satisfies = await comparer.satisfies(
1435
+ this._loadedModule?.details,
1436
+ constraintCodeDetails,
1437
+ );
1438
+ if (satisfies === false) {
1439
+ return false;
1440
+ }
1441
+ }
1442
+ return true;
1353
1443
  }
1354
1444
 
1355
1445
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
@@ -1357,15 +1447,7 @@ export class Container
1357
1447
  return versions[0];
1358
1448
  }
1359
1449
 
1360
- private recordConnectStartTime() {
1361
- if (this.connectionTransitionTimes[ConnectionState.Disconnected] === undefined) {
1362
- this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
1363
- }
1364
- }
1365
-
1366
1450
  private connectToDeltaStream(args: IConnectionArgs) {
1367
- this.recordConnectStartTime();
1368
-
1369
1451
  // All agents need "write" access, including summarizer.
1370
1452
  if (!this._canReconnect || !this.client.details.capabilities.interactive) {
1371
1453
  args.mode = "write";
@@ -1382,13 +1464,11 @@ export class Container
1382
1464
  private async load(
1383
1465
  specifiedVersion: string | undefined,
1384
1466
  loadMode: IContainerLoadMode,
1467
+ resolvedUrl: IResolvedUrl,
1385
1468
  pendingLocalState?: IPendingContainerState,
1386
1469
  ) {
1387
- if (this._resolvedUrl === undefined) {
1388
- throw new Error("Attempting to load without a resolved url");
1389
- }
1390
1470
  this.service = await this.serviceFactory.createDocumentService(
1391
- this._resolvedUrl,
1471
+ resolvedUrl,
1392
1472
  this.subLogger,
1393
1473
  this.client.details.type === summarizerClientType,
1394
1474
  );
@@ -1420,7 +1500,6 @@ export class Container
1420
1500
  // if we have pendingLocalState we can load without storage; don't wait for connection
1421
1501
  this.storageAdapter.connectToService(this.service).catch((error) => {
1422
1502
  this.close(error);
1423
- this.dispose?.(error);
1424
1503
  });
1425
1504
  }
1426
1505
 
@@ -1440,7 +1519,10 @@ export class Container
1440
1519
  if (this.offlineLoadEnabled) {
1441
1520
  this.baseSnapshot = snapshot;
1442
1521
  // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1443
- this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1522
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(
1523
+ snapshot,
1524
+ this.storageAdapter,
1525
+ );
1444
1526
  }
1445
1527
  }
1446
1528
 
@@ -1496,7 +1578,7 @@ export class Container
1496
1578
  this.processRemoteMessage(message);
1497
1579
 
1498
1580
  // allow runtime to apply stashed ops at this op's sequence number
1499
- await this.context.notifyOpReplay(message);
1581
+ await this.runtime.notifyOpReplay?.(message);
1500
1582
  }
1501
1583
  pendingLocalState.savedOps = [];
1502
1584
 
@@ -1603,8 +1685,7 @@ export class Container
1603
1685
  private async rehydrateDetachedFromSnapshot(detachedContainerSnapshot: ISummaryTree) {
1604
1686
  if (detachedContainerSnapshot.tree[".hasAttachmentBlobs"] !== undefined) {
1605
1687
  assert(
1606
- !!this.loader.services.detachedBlobStorage &&
1607
- this.loader.services.detachedBlobStorage.size > 0,
1688
+ !!this.detachedBlobStorage && this.detachedBlobStorage.size > 0,
1608
1689
  0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */,
1609
1690
  );
1610
1691
  delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
@@ -1701,10 +1782,7 @@ export class Container
1701
1782
  attributes: IDocumentAttributes,
1702
1783
  quorumSnapshot: IQuorumSnapshot,
1703
1784
  ): void {
1704
- const protocolHandlerBuilder =
1705
- this.protocolHandlerBuilder ??
1706
- ((...args) => new ProtocolHandler(...args, new Audience()));
1707
- const protocol = protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) =>
1785
+ const protocol = this.protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) =>
1708
1786
  this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1709
1787
  );
1710
1788
 
@@ -1733,7 +1811,6 @@ export class Container
1733
1811
  this.processCodeProposal().catch((error) => {
1734
1812
  const normalizedError = normalizeError(error);
1735
1813
  this.close(normalizedError);
1736
- this.dispose?.(normalizedError);
1737
1814
  throw error;
1738
1815
  });
1739
1816
  }
@@ -1844,8 +1921,16 @@ export class Container
1844
1921
  this.connectionStateHandler.receivedConnectEvent(details);
1845
1922
  });
1846
1923
 
1924
+ deltaManager.on("establishingConnection", (reason: string) => {
1925
+ this.connectionStateHandler.establishingConnection(reason);
1926
+ });
1927
+
1928
+ deltaManager.on("cancelEstablishingConnection", (reason: string) => {
1929
+ this.connectionStateHandler.cancelEstablishingConnection(reason);
1930
+ });
1931
+
1847
1932
  deltaManager.on("disconnect", (reason: string, error?: IAnyDriverError) => {
1848
- this.collabWindowTracker?.stopSequenceNumberUpdate();
1933
+ this.noopHeuristic?.notifyDisconnect();
1849
1934
  if (!this.closed) {
1850
1935
  this.connectionStateHandler.receivedDisconnectEvent(reason, error);
1851
1936
  }
@@ -1920,10 +2005,14 @@ export class Container
1920
2005
  durationFromDisconnected =
1921
2006
  time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1922
2007
  durationFromDisconnected = TelemetryLogger.formatTick(durationFromDisconnected);
1923
- } else {
1924
- // This info is of most interest on establishing connection only.
2008
+ } else if (value === ConnectionState.CatchingUp) {
2009
+ // This info is of most interesting while Catching Up.
1925
2010
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1926
- if (this.deltaManager.hasCheckpointSequenceNumber) {
2011
+ // Need to check that we have already loaded and fetched the snapshot.
2012
+ if (
2013
+ this.deltaManager.hasCheckpointSequenceNumber &&
2014
+ this._lifecycleState === "loaded"
2015
+ ) {
1927
2016
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1928
2017
  }
1929
2018
  }
@@ -2013,7 +2102,6 @@ export class Container
2013
2102
  { messageType: type },
2014
2103
  );
2015
2104
  this.close(newError);
2016
- this.dispose?.(newError);
2017
2105
  return -1;
2018
2106
  }
2019
2107
  }
@@ -2069,7 +2157,7 @@ export class Container
2069
2157
  }
2070
2158
 
2071
2159
  this.messageCountAfterDisconnection += 1;
2072
- this.collabWindowTracker?.stopSequenceNumberUpdate();
2160
+ this.noopHeuristic?.notifyMessageSent();
2073
2161
  return this._deltaManager.submit(
2074
2162
  type,
2075
2163
  contents,
@@ -2086,39 +2174,67 @@ export class Container
2086
2174
  }
2087
2175
  const local = this.clientId === message.clientId;
2088
2176
 
2177
+ // Check and report if we're getting messages from a clientId that we previously
2178
+ // flagged should have left, or from a client that's not in the quorum but should be
2179
+ if (message.clientId != null) {
2180
+ const client = this.protocolHandler.quorum.getMember(message.clientId);
2181
+
2182
+ if (client === undefined && message.type !== MessageType.ClientJoin) {
2183
+ // pre-0.58 error message: messageClientIdMissingFromQuorum
2184
+ throw new Error("Remote message's clientId is missing from the quorum");
2185
+ }
2186
+
2187
+ // Here checking canBeCoalescedByService is used as an approximation of "is benign to process despite being unexpected".
2188
+ // It's still not good to see these messages from unexpected clientIds, but since they don't harm the integrity of the
2189
+ // document we don't need to blow up aggressively.
2190
+ if (
2191
+ this.clientsWhoShouldHaveLeft.has(message.clientId) &&
2192
+ !canBeCoalescedByService(message)
2193
+ ) {
2194
+ // pre-0.58 error message: messageClientIdShouldHaveLeft
2195
+ throw new Error("Remote message's clientId already should have left");
2196
+ }
2197
+ }
2198
+
2089
2199
  // Allow the protocol handler to process the message
2090
2200
  const result = this.protocolHandler.processMessage(message, local);
2091
2201
 
2092
2202
  // Forward messages to the loaded runtime for processing
2093
- this.context.process(message, local);
2203
+ this.runtime.process(message, local);
2094
2204
 
2095
2205
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
2096
2206
  if (this.activeConnection()) {
2097
- if (this.collabWindowTracker === undefined) {
2207
+ if (this.noopHeuristic === undefined) {
2208
+ const serviceConfiguration = this.deltaManager.serviceConfiguration;
2098
2209
  // Note that config from first connection will be used for this container's lifetime.
2099
2210
  // That means that if relay service changes settings, such changes will impact only newly booted
2100
2211
  // clients.
2101
2212
  // All existing will continue to use settings they got earlier.
2102
2213
  assert(
2103
- this.serviceConfiguration !== undefined,
2214
+ serviceConfiguration !== undefined,
2104
2215
  0x2e4 /* "there should be service config for active connection" */,
2105
2216
  );
2106
- this.collabWindowTracker = new CollabWindowTracker(
2107
- (type) => {
2108
- assert(
2109
- this.activeConnection(),
2110
- 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */,
2111
- );
2112
- this.submitMessage(type);
2113
- },
2114
- this.serviceConfiguration.noopTimeFrequency,
2115
- this.serviceConfiguration.noopCountFrequency,
2217
+ this.noopHeuristic = new NoopHeuristic(
2218
+ serviceConfiguration.noopTimeFrequency,
2219
+ serviceConfiguration.noopCountFrequency,
2116
2220
  );
2221
+ this.noopHeuristic.on("wantsNoop", () => {
2222
+ // On disconnect we notify the heuristic which should prevent it from wanting a noop.
2223
+ // Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
2224
+ // running the microtask that the heuristic queued in response.
2225
+ assert(
2226
+ this.activeConnection(),
2227
+ 0x241 /* "Trying to send noop without active connection" */,
2228
+ );
2229
+ this.submitMessage(MessageType.NoOp);
2230
+ });
2231
+ }
2232
+ this.noopHeuristic.notifyMessageProcessed(message);
2233
+ // The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
2234
+ if (result.immediateNoOp === true) {
2235
+ // ADO:1385: Remove cast and use MessageType once definition changes propagate
2236
+ this.submitMessage(MessageType2.Accept as unknown as MessageType);
2117
2237
  }
2118
- this.collabWindowTracker.scheduleSequenceNumberUpdate(
2119
- message,
2120
- result.immediateNoOp === true,
2121
- );
2122
2238
  }
2123
2239
 
2124
2240
  this.emit("op", message);
@@ -2130,11 +2246,11 @@ export class Container
2130
2246
 
2131
2247
  private processSignal(message: ISignalMessage) {
2132
2248
  // No clientId indicates a system signal message.
2133
- if (message.clientId === null) {
2249
+ if (protocolHandlerShouldProcessSignal(message)) {
2134
2250
  this.protocolHandler.processSignal(message);
2135
2251
  } else {
2136
2252
  const local = this.clientId === message.clientId;
2137
- this.context.processSignal(message, local);
2253
+ this.runtime.processSignal(message, local);
2138
2254
  }
2139
2255
  }
2140
2256
 
@@ -2176,22 +2292,50 @@ export class Container
2176
2292
  private async instantiateContext(
2177
2293
  existing: boolean,
2178
2294
  codeDetails: IFluidCodeDetails,
2179
- snapshot?: ISnapshotTree,
2295
+ snapshot: ISnapshotTree | undefined,
2180
2296
  pendingLocalState?: unknown,
2181
2297
  ) {
2182
- assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
2298
+ assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
2183
2299
 
2184
2300
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
2185
2301
  // are set. Global requests will still go directly to the loader
2186
- const loader = new RelativeLoader(this, this.loader);
2187
- this._context = await ContainerContext.createOrLoad(
2188
- this,
2302
+ const maybeLoader: FluidObject<IHostLoader> = this.scope;
2303
+ const loader = new RelativeLoader(this, maybeLoader.ILoader);
2304
+
2305
+ const loadCodeResult = await PerformanceEvent.timedExecAsync(
2306
+ this.subLogger,
2307
+ { eventName: "CodeLoad" },
2308
+ async () => this.codeLoader.load(codeDetails),
2309
+ );
2310
+
2311
+ this._loadedModule = {
2312
+ module: loadCodeResult.module,
2313
+ // An older interface ICodeLoader could return an IFluidModule which didn't have details.
2314
+ // If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
2315
+ // TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
2316
+ details: loadCodeResult.details ?? codeDetails,
2317
+ };
2318
+
2319
+ const fluidExport: FluidObject<IProvideRuntimeFactory> | undefined =
2320
+ this._loadedModule.module.fluidExport;
2321
+ const runtimeFactory = fluidExport?.IRuntimeFactory;
2322
+ if (runtimeFactory === undefined) {
2323
+ throw new Error(packageNotFactoryError);
2324
+ }
2325
+
2326
+ const getSpecifiedCodeDetails = () =>
2327
+ (this.protocolHandler.quorum.get("code") ??
2328
+ this.protocolHandler.quorum.get("code2")) as IFluidCodeDetails | undefined;
2329
+
2330
+ const context = new ContainerContext(
2331
+ this.options,
2189
2332
  this.scope,
2190
- this.codeLoader,
2191
- codeDetails,
2192
2333
  snapshot,
2193
- new DeltaManagerProxy(this._deltaManager),
2194
- new QuorumProxy(this.protocolHandler.quorum),
2334
+ this._loadedFromVersion,
2335
+ this._deltaManager,
2336
+ this.storageAdapter,
2337
+ this.protocolHandler.quorum,
2338
+ this.protocolHandler.audience,
2195
2339
  loader,
2196
2340
  (type, contents, batch, metadata) =>
2197
2341
  this.submitContainerMessage(type, contents, batch, metadata),
@@ -2200,24 +2344,44 @@ export class Container
2200
2344
  (batch: IBatchMessage[], referenceSequenceNumber?: number) =>
2201
2345
  this.submitBatch(batch, referenceSequenceNumber),
2202
2346
  (message) => this.submitSignal(message),
2203
- (error?: ICriticalContainerError) => this.dispose?.(error),
2347
+ (error?: ICriticalContainerError) => this.dispose(error),
2204
2348
  (error?: ICriticalContainerError) => this.close(error),
2205
- Container.version,
2206
- (dirty: boolean) => this.updateDirtyContainerState(dirty),
2349
+ this.updateDirtyContainerState,
2350
+ this.getAbsoluteUrl,
2351
+ () => this.resolvedUrl?.id,
2352
+ () => this.clientId,
2353
+ () => this._deltaManager.serviceConfiguration,
2354
+ () => this.attachState,
2355
+ () => this.connected,
2356
+ getSpecifiedCodeDetails,
2357
+ this._deltaManager.clientDetails,
2207
2358
  existing,
2359
+ this.subLogger,
2208
2360
  pendingLocalState,
2209
2361
  );
2362
+ this._lifecycleEvents.once("disposed", () => {
2363
+ context.dispose();
2364
+ });
2365
+
2366
+ this._runtime = await PerformanceEvent.timedExecAsync(
2367
+ this.subLogger,
2368
+ { eventName: "InstantiateRuntime" },
2369
+ async () => runtimeFactory.instantiateRuntime(context, existing),
2370
+ );
2371
+ this._lifecycleEvents.emit("runtimeInstantiated");
2372
+
2373
+ this._loadedCodeDetails = codeDetails;
2210
2374
 
2211
2375
  this.emit("contextChanged", codeDetails);
2212
2376
  }
2213
2377
 
2214
- private updateDirtyContainerState(dirty: boolean) {
2378
+ private readonly updateDirtyContainerState = (dirty: boolean) => {
2215
2379
  if (this._dirtyContainer === dirty) {
2216
2380
  return;
2217
2381
  }
2218
2382
  this._dirtyContainer = dirty;
2219
2383
  this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
2220
- }
2384
+ };
2221
2385
 
2222
2386
  /**
2223
2387
  * Set the connected state of the ContainerContext
@@ -2226,21 +2390,21 @@ export class Container
2226
2390
  * @param readonly - Is the container in readonly mode?
2227
2391
  */
2228
2392
  private setContextConnectedState(state: boolean, readonly: boolean): void {
2229
- if (this._context?.disposed === false) {
2393
+ if (this._runtime?.disposed === false) {
2230
2394
  /**
2231
2395
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2232
2396
  * ops getting through to the DeltaManager.
2233
2397
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2234
2398
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2235
2399
  */
2236
- this.context.setConnectionState(state && !readonly, this.clientId);
2400
+ this.runtime.setConnectionState(state && !readonly, this.clientId);
2237
2401
  }
2238
2402
  }
2239
2403
  }
2240
2404
 
2241
2405
  /**
2242
2406
  * IContainer interface that includes experimental features still under development.
2243
- * @internal
2407
+ * @experimental
2244
2408
  */
2245
2409
  export interface IContainerExperimental extends IContainer {
2246
2410
  /**
@@ -2251,5 +2415,13 @@ export interface IContainerExperimental extends IContainer {
2251
2415
  * @experimental misuse of this API can result in duplicate op submission and potential document corruption
2252
2416
  * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2253
2417
  */
2254
- getPendingLocalState(): string;
2418
+ getPendingLocalState?(): string;
2419
+
2420
+ /**
2421
+ * Closes the container and returns serialized local state intended to be
2422
+ * given to a newly loaded container.
2423
+ * @experimental
2424
+ * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2425
+ */
2426
+ closeAndGetPendingLocalState(): string;
2255
2427
  }