@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/src/container.ts CHANGED
@@ -7,48 +7,58 @@
7
7
  import merge from "lodash/merge";
8
8
 
9
9
  import { v4 as uuid } from "uuid";
10
- import { ITelemetryProperties, TelemetryEventCategory } from "@fluidframework/common-definitions";
11
- import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
12
- import { IRequest, IResponse, IFluidRouter, FluidObject } from "@fluidframework/core-interfaces";
10
+ import { assert, unreachableCase } from "@fluidframework/core-utils";
11
+ import { TypedEventEmitter, performance } from "@fluid-internal/client-utils";
13
12
  import {
13
+ IEvent,
14
+ ITelemetryProperties,
15
+ TelemetryEventCategory,
16
+ IRequest,
17
+ IResponse,
18
+ IFluidRouter,
19
+ FluidObject,
20
+ LogLevel,
21
+ } from "@fluidframework/core-interfaces";
22
+ import {
23
+ AttachState,
24
+ ContainerWarning,
14
25
  IAudience,
15
- IConnectionDetailsInternal,
26
+ IBatchMessage,
27
+ ICodeDetailsLoader,
16
28
  IContainer,
17
29
  IContainerEvents,
18
- IDeltaManager,
19
- ICriticalContainerError,
20
- ContainerWarning,
21
- AttachState,
22
- IThrottlingWarning,
23
- ReadOnlyInfo,
24
30
  IContainerLoadMode,
31
+ ICriticalContainerError,
32
+ IDeltaManager,
25
33
  IFluidCodeDetails,
26
- isFluidCodeDetails,
27
- IBatchMessage,
28
- ICodeDetailsLoader,
29
34
  IHostLoader,
35
+ IFluidModuleWithDetails,
36
+ IProvideRuntimeFactory,
37
+ IProvideFluidCodeDetailsComparer,
38
+ IFluidCodeDetailsComparer,
39
+ IRuntime,
40
+ ReadOnlyInfo,
41
+ isFluidCodeDetails,
30
42
  } from "@fluidframework/container-definitions";
31
- import { GenericError, UsageError } from "@fluidframework/container-utils";
32
43
  import {
33
- IAnyDriverError,
34
44
  IDocumentService,
35
45
  IDocumentServiceFactory,
36
46
  IDocumentStorageService,
37
47
  IResolvedUrl,
48
+ IThrottlingWarning,
38
49
  IUrlResolver,
39
50
  } from "@fluidframework/driver-definitions";
40
51
  import {
41
52
  readAndParse,
42
53
  OnlineStatus,
43
54
  isOnline,
44
- combineAppAndProtocolSummary,
45
55
  runWithRetry,
46
56
  isCombinedAppAndProtocolSummary,
57
+ MessageType2,
47
58
  } from "@fluidframework/driver-utils";
48
59
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
49
60
  import {
50
61
  IClient,
51
- IClientConfiguration,
52
62
  IClientDetails,
53
63
  ICommittedProposal,
54
64
  IDocumentAttributes,
@@ -67,23 +77,30 @@ import {
67
77
  SummaryType,
68
78
  } from "@fluidframework/protocol-definitions";
69
79
  import {
70
- ChildLogger,
80
+ createChildLogger,
71
81
  EventEmitterWithErrorHandling,
72
82
  PerformanceEvent,
73
83
  raiseConnectedEvent,
74
- TelemetryLogger,
75
84
  connectedEventName,
76
85
  normalizeError,
77
86
  MonitoringContext,
78
- loggerToMonitoringContext,
87
+ createChildMonitoringContext,
79
88
  wrapError,
80
89
  ITelemetryLoggerExt,
90
+ formatTick,
91
+ GenericError,
92
+ UsageError,
81
93
  } from "@fluidframework/telemetry-utils";
82
94
  import { Audience } from "./audience";
83
95
  import { ContainerContext } from "./containerContext";
84
- import { ReconnectMode, IConnectionManagerFactoryArgs, getPackageName } from "./contracts";
96
+ import {
97
+ ReconnectMode,
98
+ IConnectionManagerFactoryArgs,
99
+ getPackageName,
100
+ IConnectionDetailsInternal,
101
+ IConnectionStateChangeReason,
102
+ } from "./contracts";
85
103
  import { DeltaManager, IConnectionArgs } from "./deltaManager";
86
- import { DeltaManagerProxy } from "./deltaManagerProxy";
87
104
  import { IDetachedBlobStorage, ILoaderOptions, RelativeLoader } from "./loader";
88
105
  import { pkgVersion } from "./packageVersion";
89
106
  import {
@@ -93,20 +110,21 @@ import {
93
110
  ISerializableBlobContents,
94
111
  } from "./containerStorageAdapter";
95
112
  import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
96
- import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
97
113
  import {
98
- initQuorumValuesFromCodeDetails,
99
- getCodeDetailsFromQuorumValues,
100
- QuorumProxy,
101
- } from "./quorum";
102
- import { CollabWindowTracker } from "./collabWindowTracker";
114
+ combineAppAndProtocolSummary,
115
+ getProtocolSnapshotTree,
116
+ getSnapshotTreeFromSerializedContainer,
117
+ } from "./utils";
118
+ import { initQuorumValuesFromCodeDetails } from "./quorum";
119
+ import { NoopHeuristic } from "./noopHeuristic";
103
120
  import { ConnectionManager } from "./connectionManager";
104
121
  import { ConnectionState } from "./connectionState";
105
122
  import {
106
- OnlyValidTermValue,
107
123
  IProtocolHandler,
124
+ OnlyValidTermValue,
108
125
  ProtocolHandler,
109
126
  ProtocolHandlerBuilder,
127
+ protocolHandlerShouldProcessSignal,
110
128
  } from "./protocol";
111
129
 
112
130
  const detachedContainerRefSeqNumber = 0;
@@ -114,6 +132,8 @@ const detachedContainerRefSeqNumber = 0;
114
132
  const dirtyContainerEvent = "dirty";
115
133
  const savedContainerEvent = "saved";
116
134
 
135
+ const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
136
+
117
137
  /**
118
138
  * @internal
119
139
  */
@@ -135,6 +155,11 @@ export interface IContainerLoadProps {
135
155
  * The pending state serialized from a pervious container instance
136
156
  */
137
157
  readonly pendingLocalState?: IPendingContainerState;
158
+
159
+ /**
160
+ * Load the container to at least this sequence number.
161
+ */
162
+ readonly loadToSequenceNumber?: number;
138
163
  }
139
164
 
140
165
  /**
@@ -190,6 +215,10 @@ export interface IContainerCreateProps {
190
215
  */
191
216
  readonly detachedBlobStorage?: IDetachedBlobStorage;
192
217
 
218
+ /**
219
+ * Optional property for allowing the container to use a custom
220
+ * protocol implementation for handling the quorum and/or the audience.
221
+ */
193
222
  readonly protocolHandlerBuilder?: ProtocolHandlerBuilder;
194
223
  }
195
224
 
@@ -336,12 +365,15 @@ export interface IPendingContainerState {
336
365
 
337
366
  const summarizerClientType = "summarizer";
338
367
 
368
+ interface IContainerLifecycleEvents extends IEvent {
369
+ (event: "runtimeInstantiated", listener: () => void): void;
370
+ (event: "disposed", listener: () => void): void;
371
+ }
372
+
339
373
  export class Container
340
374
  extends EventEmitterWithErrorHandling<IContainerEvents>
341
375
  implements IContainer, IContainerExperimental
342
376
  {
343
- public static version = "^0.1.0";
344
-
345
377
  /**
346
378
  * Load an existing container.
347
379
  * @internal
@@ -350,7 +382,8 @@ export class Container
350
382
  loadProps: IContainerLoadProps,
351
383
  createProps: IContainerCreateProps,
352
384
  ): Promise<Container> {
353
- const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
385
+ const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } =
386
+ loadProps;
354
387
 
355
388
  const container = new Container(createProps, loadProps);
356
389
 
@@ -379,7 +412,7 @@ export class Container
379
412
  container.on("closed", onClosed);
380
413
 
381
414
  container
382
- .load(version, mode, resolvedUrl, pendingLocalState)
415
+ .load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
383
416
  .finally(() => {
384
417
  container.removeListener("closed", onClosed);
385
418
  })
@@ -393,7 +426,11 @@ export class Container
393
426
  // Depending where error happens, we can be attempting to connect to web socket
394
427
  // and continuously retrying (consider offline mode)
395
428
  // Host has no container to close, so it's prudent to do it here
429
+ // Note: We could only dispose the container instead of just close but that would
430
+ // the telemetry where users sometimes search for ContainerClose event to look
431
+ // for load failures. So not removing this at this time.
396
432
  container.close(err);
433
+ container.dispose(err);
397
434
  onClosed(err);
398
435
  },
399
436
  );
@@ -452,9 +489,9 @@ export class Container
452
489
  private readonly urlResolver: IUrlResolver;
453
490
  private readonly serviceFactory: IDocumentServiceFactory;
454
491
  private readonly codeLoader: ICodeDetailsLoader;
455
- public readonly options: ILoaderOptions;
492
+ private readonly options: ILoaderOptions;
456
493
  private readonly scope: FluidObject;
457
- public subLogger: TelemetryLogger;
494
+ private readonly subLogger: ITelemetryLoggerExt;
458
495
  private readonly detachedBlobStorage: IDetachedBlobStorage | undefined;
459
496
  private readonly protocolHandlerBuilder: ProtocolHandlerBuilder;
460
497
 
@@ -504,29 +541,27 @@ export class Container
504
541
 
505
542
  public get closed(): boolean {
506
543
  return (
507
- this._lifecycleState === "closing" ||
508
- this._lifecycleState === "closed" ||
509
- this._lifecycleState === "disposing" ||
510
- this._lifecycleState === "disposed"
544
+ this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed
511
545
  );
512
546
  }
513
547
 
548
+ public get disposed(): boolean {
549
+ return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
550
+ }
551
+
514
552
  private _attachState = AttachState.Detached;
515
553
 
516
554
  private readonly storageAdapter: ContainerStorageAdapter;
517
- public get storage(): IDocumentStorageService {
518
- return this.storageAdapter;
519
- }
520
555
 
521
556
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
522
557
  private service: IDocumentService | undefined;
523
558
 
524
- private _context: ContainerContext | undefined;
525
- private get context() {
526
- if (this._context === undefined) {
527
- throw new GenericError("Attempted to access context before it was defined");
559
+ private _runtime: IRuntime | undefined;
560
+ private get runtime() {
561
+ if (this._runtime === undefined) {
562
+ throw new Error("Attempted to access runtime before it was defined");
528
563
  }
529
- return this._context;
564
+ return this._runtime;
530
565
  }
531
566
  private _protocolHandler: IProtocolHandler | undefined;
532
567
  private get protocolHandler() {
@@ -540,7 +575,6 @@ export class Container
540
575
  private inboundQueuePausedFromInit = true;
541
576
  private firstConnection = true;
542
577
  private readonly connectionTransitionTimes: number[] = [];
543
- private messageCountAfterDisconnection: number = 0;
544
578
  private _loadedFromVersion: IVersion | undefined;
545
579
  private attachStarted = false;
546
580
  private _dirtyContainer = false;
@@ -551,10 +585,11 @@ export class Container
551
585
  private lastVisible: number | undefined;
552
586
  private readonly visibilityEventHandler: (() => void) | undefined;
553
587
  private readonly connectionStateHandler: IConnectionStateHandler;
588
+ private readonly clientsWhoShouldHaveLeft = new Set<string>();
554
589
 
555
590
  private setAutoReconnectTime = performance.now();
556
591
 
557
- private collabWindowTracker: CollabWindowTracker | undefined;
592
+ private noopHeuristic: NoopHeuristic | undefined;
558
593
 
559
594
  private get connectionMode() {
560
595
  return this._deltaManager.connectionManager.connectionMode;
@@ -579,18 +614,10 @@ export class Container
579
614
  return this.service?.resolvedUrl;
580
615
  }
581
616
 
582
- public get loadedFromVersion(): IVersion | undefined {
583
- return this._loadedFromVersion;
584
- }
585
-
586
617
  public get readOnlyInfo(): ReadOnlyInfo {
587
618
  return this._deltaManager.readOnlyInfo;
588
619
  }
589
620
 
590
- public get closeSignal(): AbortSignal {
591
- return this._deltaManager.closeAbortController.signal;
592
- }
593
-
594
621
  /**
595
622
  * Tracks host requiring read-only mode.
596
623
  */
@@ -606,18 +633,10 @@ export class Container
606
633
  return this.connectionStateHandler.connectionState;
607
634
  }
608
635
 
609
- public get connected(): boolean {
636
+ private get connected(): boolean {
610
637
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
611
638
  }
612
639
 
613
- /**
614
- * Service configuration details. If running in offline mode will be undefined otherwise will contain service
615
- * configuration details returned as part of the initial connection.
616
- */
617
- public get serviceConfiguration(): IClientConfiguration | undefined {
618
- return this._deltaManager.serviceConfiguration;
619
- }
620
-
621
640
  private _clientId: string | undefined;
622
641
 
623
642
  /**
@@ -628,24 +647,12 @@ export class Container
628
647
  return this._clientId;
629
648
  }
630
649
 
631
- /**
632
- * The server provided claims of the client.
633
- * Set once this.connected is true, otherwise undefined
634
- */
635
- public get scopes(): string[] | undefined {
636
- return this._deltaManager.connectionManager.scopes;
637
- }
638
-
639
- public get clientDetails(): IClientDetails {
640
- return this._deltaManager.clientDetails;
641
- }
642
-
643
650
  private get offlineLoadEnabled(): boolean {
644
651
  const enabled =
645
652
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
646
653
  this.options?.enableOfflineLoad === true;
647
654
  // summarizer will not have any pending state we want to save
648
- return enabled && this.clientDetails.capabilities.interactive;
655
+ return enabled && this.deltaManager.clientDetails.capabilities.interactive;
649
656
  }
650
657
 
651
658
  /**
@@ -656,15 +663,18 @@ export class Container
656
663
  return this.getCodeDetailsFromQuorum();
657
664
  }
658
665
 
666
+ private _loadedCodeDetails: IFluidCodeDetails | undefined;
659
667
  /**
660
668
  * Get the code details that were used to load the container.
661
669
  * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
662
670
  * loaded.
663
671
  */
664
672
  public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
665
- return this._context?.codeDetails;
673
+ return this._loadedCodeDetails;
666
674
  }
667
675
 
676
+ private _loadedModule: IFluidModuleWithDetails | undefined;
677
+
668
678
  /**
669
679
  * Retrieves the audience associated with the document
670
680
  */
@@ -684,38 +694,33 @@ export class Container
684
694
  /**
685
695
  * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
686
696
  */
687
- public async getEntryPoint?(): Promise<FluidObject | undefined> {
688
- // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
689
- // allow it since they mean a kind of read-only state for the Container.
690
- // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
691
- if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
692
- throw new UsageError("The container is disposing or disposed");
697
+ public async getEntryPoint(): Promise<FluidObject | undefined> {
698
+ if (this._disposed) {
699
+ throw new UsageError("The context is already disposed");
693
700
  }
694
- while (this._context === undefined) {
695
- await new Promise<void>((resolve, reject) => {
696
- const contextChangedHandler = () => {
697
- resolve();
698
- this.off("disposed", disposedHandler);
699
- };
700
- const disposedHandler = (error) => {
701
- reject(error ?? "The Container is disposed");
702
- this.off("contextChanged", contextChangedHandler);
703
- };
704
- this.once("contextChanged", contextChangedHandler);
705
- this.once("disposed", disposedHandler);
706
- });
707
- // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
708
- // should have set this._context; making sure.
709
- assert(
710
- this._context !== undefined,
711
- 0x5a2 /* Context still not defined after contextChanged event */,
712
- );
701
+ if (this._runtime !== undefined) {
702
+ return this._runtime.getEntryPoint?.();
713
703
  }
714
- // Disable lint rule for the sake of more complete stack traces
715
- // eslint-disable-next-line no-return-await
716
- return await this._context.getEntryPoint?.();
704
+ return new Promise<FluidObject | undefined>((resolve, reject) => {
705
+ const runtimeInstantiatedHandler = () => {
706
+ assert(
707
+ this._runtime !== undefined,
708
+ 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */,
709
+ );
710
+ resolve(this._runtime.getEntryPoint?.());
711
+ this._lifecycleEvents.off("disposed", disposedHandler);
712
+ };
713
+ const disposedHandler = () => {
714
+ reject(new Error("ContainerContext was disposed"));
715
+ this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
716
+ };
717
+ this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
718
+ this._lifecycleEvents.once("disposed", disposedHandler);
719
+ });
717
720
  }
718
721
 
722
+ private readonly _lifecycleEvents = new TypedEventEmitter<IContainerLifecycleEvents>();
723
+
719
724
  /**
720
725
  * @internal
721
726
  */
@@ -748,6 +753,7 @@ export class Container
748
753
 
749
754
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
750
755
  const pendingLocalState = loadProps?.pendingLocalState;
756
+ this._clientId = pendingLocalState?.clientId;
751
757
 
752
758
  this._canReconnect = canReconnect ?? true;
753
759
  this.clientDetailsOverride = clientDetailsOverride;
@@ -761,7 +767,19 @@ export class Container
761
767
  this.scope = scope;
762
768
  this.detachedBlobStorage = detachedBlobStorage;
763
769
  this.protocolHandlerBuilder =
764
- protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
770
+ protocolHandlerBuilder ??
771
+ ((
772
+ attributes: IDocumentAttributes,
773
+ quorumSnapshot: IQuorumSnapshot,
774
+ sendProposal: (key: string, value: any) => number,
775
+ ) =>
776
+ new ProtocolHandler(
777
+ attributes,
778
+ quorumSnapshot,
779
+ sendProposal,
780
+ new Audience(),
781
+ (clientId: string) => this.clientsWhoShouldHaveLeft.has(clientId),
782
+ ));
765
783
 
766
784
  // Note that we capture the createProps here so we can replicate the creation call when we want to clone.
767
785
  this.clone = async (
@@ -782,50 +800,56 @@ export class Container
782
800
  }`;
783
801
  // Need to use the property getter for docId because for detached flow we don't have the docId initially.
784
802
  // We assign the id later so property getter is used.
785
- this.subLogger = ChildLogger.create(subLogger, undefined, {
786
- all: {
787
- clientType, // Differentiating summarizer container from main container
788
- containerId: uuid(),
789
- docId: () => this.resolvedUrl?.id,
790
- containerAttachState: () => this._attachState,
791
- containerLifecycleState: () => this._lifecycleState,
792
- containerConnectionState: () => ConnectionState[this.connectionState],
793
- serializedContainer: pendingLocalState !== undefined,
794
- },
795
- // we need to be judicious with our logging here to avoid generating too much data
796
- // all data logged here should be broadly applicable, and not specific to a
797
- // specific error or class of errors
798
- error: {
799
- // load information to associate errors with the specific load point
800
- dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
801
- dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
802
- dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
803
- containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
804
- containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
805
- // message information to associate errors with the specific execution state
806
- // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
807
- dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
808
- dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
809
- dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
810
- dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
811
- connectionStateDuration: () =>
812
- performance.now() - this.connectionTransitionTimes[this.connectionState],
803
+ this.subLogger = createChildLogger({
804
+ logger: subLogger,
805
+ properties: {
806
+ all: {
807
+ clientType, // Differentiating summarizer container from main container
808
+ containerId: uuid(),
809
+ docId: () => this.resolvedUrl?.id,
810
+ containerAttachState: () => this._attachState,
811
+ containerLifecycleState: () => this._lifecycleState,
812
+ containerConnectionState: () => ConnectionState[this.connectionState],
813
+ serializedContainer: pendingLocalState !== undefined,
814
+ },
815
+ // we need to be judicious with our logging here to avoid generating too much data
816
+ // all data logged here should be broadly applicable, and not specific to a
817
+ // specific error or class of errors
818
+ error: {
819
+ // load information to associate errors with the specific load point
820
+ dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
821
+ dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
822
+ dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
823
+ containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
824
+ containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
825
+ // message information to associate errors with the specific execution state
826
+ // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
827
+ dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
828
+ dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
829
+ dmLastMsqSeqClientId: () =>
830
+ this.deltaManager?.lastMessage?.clientId === null
831
+ ? "null"
832
+ : this.deltaManager?.lastMessage?.clientId,
833
+ dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
834
+ connectionStateDuration: () =>
835
+ performance.now() - this.connectionTransitionTimes[this.connectionState],
836
+ },
813
837
  },
814
838
  });
815
839
 
816
840
  // Prefix all events in this file with container-loader
817
- this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
841
+ this.mc = createChildMonitoringContext({ logger: this.subLogger, namespace: "Container" });
818
842
 
819
843
  this._deltaManager = this.createDeltaManager();
820
844
 
821
845
  this.connectionStateHandler = createConnectionStateHandler(
822
846
  {
823
847
  logger: this.mc.logger,
824
- connectionStateChanged: (value, oldState, reason, error) => {
848
+ connectionStateChanged: (value, oldState, reason) => {
825
849
  if (value === ConnectionState.Connected) {
826
850
  this._clientId = this.connectionStateHandler.pendingClientId;
827
851
  }
828
- this.logConnectionStateChangeTelemetry(value, oldState, reason, error);
852
+ this.logConnectionStateChangeTelemetry(value, oldState, reason);
829
853
  if (this._lifecycleState === "loaded") {
830
854
  this.propagateConnectionState(
831
855
  false /* initial transition */,
@@ -867,10 +891,14 @@ export class Container
867
891
  // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
868
892
  // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
869
893
  if (mode === "read") {
870
- this.disconnect();
871
- this.connect();
894
+ const reason = { text: "NoJoinSignal" };
895
+ this.disconnectInternal(reason);
896
+ this.connectInternal({ reason, fetchOpsFromStorage: false });
872
897
  }
873
898
  },
899
+ clientShouldHaveLeft: (clientId: string) => {
900
+ this.clientsWhoShouldHaveLeft.add(clientId);
901
+ },
874
902
  },
875
903
  this.deltaManager,
876
904
  pendingLocalState?.clientId,
@@ -907,8 +935,8 @@ export class Container
907
935
  document !== null &&
908
936
  typeof document.addEventListener === "function" &&
909
937
  document.addEventListener !== null;
910
- // keep track of last time page was visible for telemetry
911
- if (isDomAvailable) {
938
+ // keep track of last time page was visible for telemetry (on interactive clients only)
939
+ if (isDomAvailable && interactive) {
912
940
  this.lastVisible = document.hidden ? performance.now() : undefined;
913
941
  this.visibilityEventHandler = () => {
914
942
  if (document.hidden) {
@@ -994,6 +1022,11 @@ export class Container
994
1022
  }
995
1023
  } finally {
996
1024
  this._lifecycleState = "closed";
1025
+
1026
+ // There is no user for summarizer, so we need to ensure dispose is called
1027
+ if (this.client.details.type === summarizerClientType) {
1028
+ this.dispose(error);
1029
+ }
997
1030
  }
998
1031
  }
999
1032
 
@@ -1010,7 +1043,8 @@ export class Container
1010
1043
  this.mc.logger.sendTelemetryEvent(
1011
1044
  {
1012
1045
  eventName: "ContainerDispose",
1013
- category: "generic",
1046
+ // Only log error if container isn't closed
1047
+ category: !this.closed && error !== undefined ? "error" : "generic",
1014
1048
  },
1015
1049
  error,
1016
1050
  );
@@ -1024,7 +1058,8 @@ export class Container
1024
1058
 
1025
1059
  this.connectionStateHandler.dispose();
1026
1060
 
1027
- this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
1061
+ const maybeError = error !== undefined ? new Error(error.message) : undefined;
1062
+ this._runtime?.dispose(maybeError);
1028
1063
 
1029
1064
  this.storageAdapter.dispose();
1030
1065
 
@@ -1047,45 +1082,69 @@ export class Container
1047
1082
  }
1048
1083
  } finally {
1049
1084
  this._lifecycleState = "disposed";
1085
+ this._lifecycleEvents.emit("disposed");
1050
1086
  }
1051
1087
  }
1052
1088
 
1053
- public closeAndGetPendingLocalState(): string {
1089
+ public async closeAndGetPendingLocalState(): Promise<string> {
1054
1090
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
1055
1091
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
1056
1092
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
1057
- const pendingState = this.getPendingLocalState();
1093
+ this.disconnectInternal({ text: "closeAndGetPendingLocalState" }); // TODO https://dev.azure.com/fluidframework/internal/_workitems/edit/5127
1094
+ const pendingState = await this.getPendingLocalStateCore({ notifyImminentClosure: true });
1058
1095
  this.close();
1059
1096
  return pendingState;
1060
1097
  }
1061
1098
 
1062
- public getPendingLocalState(): string {
1063
- if (!this.offlineLoadEnabled) {
1064
- throw new UsageError("Can't get pending local state unless offline load is enabled");
1065
- }
1066
- assert(
1067
- this.attachState === AttachState.Attached,
1068
- 0x0d1 /* "Container should be attached before close" */,
1069
- );
1070
- assert(
1071
- this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
1072
- 0x0d2 /* "resolved url should be valid Fluid url" */,
1073
- );
1074
- assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1075
- assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1076
- const pendingState: IPendingContainerState = {
1077
- pendingRuntimeState: this.context.getPendingLocalState(),
1078
- baseSnapshot: this.baseSnapshot,
1079
- snapshotBlobs: this.baseSnapshotBlobs,
1080
- savedOps: this.savedOps,
1081
- url: this.resolvedUrl.url,
1082
- term: OnlyValidTermValue,
1083
- clientId: this.clientId,
1084
- };
1099
+ public async getPendingLocalState(): Promise<string> {
1100
+ return this.getPendingLocalStateCore({ notifyImminentClosure: false });
1101
+ }
1085
1102
 
1086
- this.mc.logger.sendTelemetryEvent({ eventName: "GetPendingLocalState" });
1103
+ private async getPendingLocalStateCore(props: { notifyImminentClosure: boolean }) {
1104
+ return PerformanceEvent.timedExecAsync(
1105
+ this.mc.logger,
1106
+ {
1107
+ eventName: "getPendingLocalState",
1108
+ notifyImminentClosure: props.notifyImminentClosure,
1109
+ savedOpsSize: this.savedOps.length,
1110
+ clientId: this.clientId,
1111
+ },
1112
+ async () => {
1113
+ if (!this.offlineLoadEnabled) {
1114
+ throw new UsageError(
1115
+ "Can't get pending local state unless offline load is enabled",
1116
+ );
1117
+ }
1118
+ if (this.closed || this._disposed) {
1119
+ throw new UsageError(
1120
+ "Pending state cannot be retried if the container is closed or disposed",
1121
+ );
1122
+ }
1123
+ assert(
1124
+ this.attachState === AttachState.Attached,
1125
+ 0x0d1 /* "Container should be attached before close" */,
1126
+ );
1127
+ assert(
1128
+ this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
1129
+ 0x0d2 /* "resolved url should be valid Fluid url" */,
1130
+ );
1131
+ assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1132
+ assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1133
+ const pendingRuntimeState = await this.runtime.getPendingLocalState(props);
1134
+ const pendingState: IPendingContainerState = {
1135
+ pendingRuntimeState,
1136
+ baseSnapshot: this.baseSnapshot,
1137
+ snapshotBlobs: this.baseSnapshotBlobs,
1138
+ savedOps: this.savedOps,
1139
+ url: this.resolvedUrl.url,
1140
+ term: OnlyValidTermValue,
1141
+ // no need to save this if there is no pending runtime state
1142
+ clientId: pendingRuntimeState !== undefined ? this.clientId : undefined,
1143
+ };
1087
1144
 
1088
- return JSON.stringify(pendingState);
1145
+ return JSON.stringify(pendingState);
1146
+ },
1147
+ );
1089
1148
  }
1090
1149
 
1091
1150
  public get attachState(): AttachState {
@@ -1098,7 +1157,7 @@ export class Container
1098
1157
  0x0d3 /* "Should only be called in detached container" */,
1099
1158
  );
1100
1159
 
1101
- const appSummary: ISummaryTree = this.context.createSummary();
1160
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1102
1161
  const protocolSummary = this.captureProtocolSummary();
1103
1162
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1104
1163
 
@@ -1111,7 +1170,10 @@ export class Container
1111
1170
  return JSON.stringify(combinedSummary);
1112
1171
  }
1113
1172
 
1114
- public async attach(request: IRequest): Promise<void> {
1173
+ public async attach(
1174
+ request: IRequest,
1175
+ attachProps?: { deltaConnection?: "none" | "delayed" },
1176
+ ): Promise<void> {
1115
1177
  await PerformanceEvent.timedExecAsync(
1116
1178
  this.mc.logger,
1117
1179
  { eventName: "Attach" },
@@ -1144,7 +1206,7 @@ export class Container
1144
1206
  if (!hasAttachmentBlobs) {
1145
1207
  // Get the document state post attach - possibly can just call attach but we need to change the
1146
1208
  // semantics around what the attach means as far as async code goes.
1147
- const appSummary: ISummaryTree = this.context.createSummary();
1209
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1148
1210
  const protocolSummary = this.captureProtocolSummary();
1149
1211
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1150
1212
 
@@ -1153,6 +1215,7 @@ export class Container
1153
1215
  // starting to attach the container to storage.
1154
1216
  // Also, this should only be fired in detached container.
1155
1217
  this._attachState = AttachState.Attaching;
1218
+ this.runtime.setAttachState(AttachState.Attaching);
1156
1219
  this.emit("attaching");
1157
1220
  if (this.offlineLoadEnabled) {
1158
1221
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1181,7 +1244,7 @@ export class Container
1181
1244
  "containerAttach",
1182
1245
  this.mc.logger,
1183
1246
  {
1184
- cancel: this.closeSignal,
1247
+ cancel: this._deltaManager.closeAbortController.signal,
1185
1248
  }, // progress
1186
1249
  );
1187
1250
  }
@@ -1210,11 +1273,12 @@ export class Container
1210
1273
  }
1211
1274
 
1212
1275
  // take summary and upload
1213
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
1276
+ const appSummary: ISummaryTree = this.runtime.createSummary(redirectTable);
1214
1277
  const protocolSummary = this.captureProtocolSummary();
1215
1278
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1216
1279
 
1217
1280
  this._attachState = AttachState.Attaching;
1281
+ this.runtime.setAttachState(AttachState.Attaching);
1218
1282
  this.emit("attaching");
1219
1283
  if (this.offlineLoadEnabled) {
1220
1284
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1231,13 +1295,17 @@ export class Container
1231
1295
  }
1232
1296
 
1233
1297
  this._attachState = AttachState.Attached;
1298
+ this.runtime.setAttachState(AttachState.Attached);
1234
1299
  this.emit("attached");
1235
1300
 
1236
1301
  if (!this.closed) {
1237
- this.resumeInternal({
1238
- fetchOpsFromStorage: false,
1239
- reason: "createDetached",
1240
- });
1302
+ this.handleDeltaConnectionArg(
1303
+ {
1304
+ fetchOpsFromStorage: false,
1305
+ reason: { text: "createDetached" },
1306
+ },
1307
+ attachProps?.deltaConnection,
1308
+ );
1241
1309
  }
1242
1310
  } catch (error) {
1243
1311
  // add resolved URL on error object so that host has the ability to find this document and delete it
@@ -1255,12 +1323,12 @@ export class Container
1255
1323
  return PerformanceEvent.timedExecAsync(
1256
1324
  this.mc.logger,
1257
1325
  { eventName: "Request" },
1258
- async () => this.context.request(path),
1326
+ async () => this.runtime.request(path),
1259
1327
  { end: true, cancel: "error" },
1260
1328
  );
1261
1329
  }
1262
1330
 
1263
- private setAutoReconnectInternal(mode: ReconnectMode) {
1331
+ private setAutoReconnectInternal(mode: ReconnectMode, reason: IConnectionStateChangeReason) {
1264
1332
  const currentMode = this._deltaManager.connectionManager.reconnectMode;
1265
1333
 
1266
1334
  if (currentMode === mode) {
@@ -1279,7 +1347,7 @@ export class Container
1279
1347
  duration,
1280
1348
  });
1281
1349
 
1282
- this._deltaManager.connectionManager.setAutoReconnect(mode);
1350
+ this._deltaManager.connectionManager.setAutoReconnect(mode, reason);
1283
1351
  }
1284
1352
 
1285
1353
  public connect() {
@@ -1291,7 +1359,10 @@ export class Container
1291
1359
  // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
1292
1360
  // If there is gap, we will learn about it once connected, but the gap should be small (if any),
1293
1361
  // assuming that connect() is called quickly after initial container boot.
1294
- this.connectInternal({ reason: "DocumentConnect", fetchOpsFromStorage: false });
1362
+ this.connectInternal({
1363
+ reason: { text: "DocumentConnect" },
1364
+ fetchOpsFromStorage: false,
1365
+ });
1295
1366
  }
1296
1367
  }
1297
1368
 
@@ -1307,23 +1378,23 @@ export class Container
1307
1378
 
1308
1379
  // Set Auto Reconnect Mode
1309
1380
  const mode = ReconnectMode.Enabled;
1310
- this.setAutoReconnectInternal(mode);
1381
+ this.setAutoReconnectInternal(mode, args.reason);
1311
1382
  }
1312
1383
 
1313
1384
  public disconnect() {
1314
1385
  if (this.closed) {
1315
1386
  throw new UsageError(`The Container is closed and cannot be disconnected`);
1316
1387
  } else {
1317
- this.disconnectInternal();
1388
+ this.disconnectInternal({ text: "DocumentDisconnect" });
1318
1389
  }
1319
1390
  }
1320
1391
 
1321
- private disconnectInternal() {
1392
+ private disconnectInternal(reason: IConnectionStateChangeReason) {
1322
1393
  assert(!this.closed, 0x2c7 /* "Attempting to disconnect() a closed Container" */);
1323
1394
 
1324
1395
  // Set Auto Reconnect Mode
1325
1396
  const mode = ReconnectMode.Disabled;
1326
- this.setAutoReconnectInternal(mode);
1397
+ this.setAutoReconnectInternal(mode, reason);
1327
1398
  }
1328
1399
 
1329
1400
  private resumeInternal(args: IConnectionArgs) {
@@ -1340,7 +1411,7 @@ export class Container
1340
1411
  this.connectToDeltaStream(args);
1341
1412
  }
1342
1413
 
1343
- public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1414
+ public readonly getAbsoluteUrl = async (relativeUrl: string): Promise<string | undefined> => {
1344
1415
  if (this.resolvedUrl === undefined) {
1345
1416
  return undefined;
1346
1417
  }
@@ -1348,9 +1419,9 @@ export class Container
1348
1419
  return this.urlResolver.getAbsoluteUrl(
1349
1420
  this.resolvedUrl,
1350
1421
  relativeUrl,
1351
- getPackageName(this._context?.codeDetails),
1422
+ getPackageName(this._loadedCodeDetails),
1352
1423
  );
1353
- }
1424
+ };
1354
1425
 
1355
1426
  public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1356
1427
  if (!isFluidCodeDetails(codeDetails)) {
@@ -1381,7 +1452,7 @@ export class Container
1381
1452
  this.deltaManager.inboundSignal.pause(),
1382
1453
  ]);
1383
1454
 
1384
- if ((await this.context.satisfies(codeDetails)) === true) {
1455
+ if ((await this.satisfies(codeDetails)) === true) {
1385
1456
  this.deltaManager.inbound.resume();
1386
1457
  this.deltaManager.inboundSignal.resume();
1387
1458
  return;
@@ -1392,6 +1463,47 @@ export class Container
1392
1463
  this.close(error);
1393
1464
  }
1394
1465
 
1466
+ /**
1467
+ * Determines if the currently loaded module satisfies the incoming constraint code details
1468
+ */
1469
+ private async satisfies(constraintCodeDetails: IFluidCodeDetails) {
1470
+ // If we have no module, it can't satisfy anything.
1471
+ if (this._loadedModule === undefined) {
1472
+ return false;
1473
+ }
1474
+
1475
+ const comparers: IFluidCodeDetailsComparer[] = [];
1476
+
1477
+ const maybeCompareCodeLoader = this.codeLoader;
1478
+ if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
1479
+ comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
1480
+ }
1481
+
1482
+ const maybeCompareExport: Partial<IProvideFluidCodeDetailsComparer> | undefined =
1483
+ this._loadedModule?.module.fluidExport;
1484
+ if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
1485
+ comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
1486
+ }
1487
+
1488
+ // If there are no comparers, then it's impossible to know if the currently loaded package satisfies
1489
+ // the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
1490
+ // rather than potentially running with incompatible code.
1491
+ if (comparers.length === 0) {
1492
+ return false;
1493
+ }
1494
+
1495
+ for (const comparer of comparers) {
1496
+ const satisfies = await comparer.satisfies(
1497
+ this._loadedModule?.details,
1498
+ constraintCodeDetails,
1499
+ );
1500
+ if (satisfies === false) {
1501
+ return false;
1502
+ }
1503
+ }
1504
+ return true;
1505
+ }
1506
+
1395
1507
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
1396
1508
  const versions = await this.storageAdapter.getVersions(version, 1);
1397
1509
  return versions[0];
@@ -1415,8 +1527,10 @@ export class Container
1415
1527
  specifiedVersion: string | undefined,
1416
1528
  loadMode: IContainerLoadMode,
1417
1529
  resolvedUrl: IResolvedUrl,
1418
- pendingLocalState?: IPendingContainerState,
1530
+ pendingLocalState: IPendingContainerState | undefined,
1531
+ loadToSequenceNumber: number | undefined,
1419
1532
  ) {
1533
+ const timings: Record<string, number> = { phase1: performance.now() };
1420
1534
  this.service = await this.serviceFactory.createDocumentService(
1421
1535
  resolvedUrl,
1422
1536
  this.subLogger,
@@ -1433,7 +1547,7 @@ export class Container
1433
1547
  // A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
1434
1548
  // B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
1435
1549
  const connectionArgs: IConnectionArgs = {
1436
- reason: "DocumentOpen",
1550
+ reason: { text: "DocumentOpen" },
1437
1551
  mode: "write",
1438
1552
  fetchOpsFromStorage: false,
1439
1553
  };
@@ -1455,6 +1569,7 @@ export class Container
1455
1569
 
1456
1570
  this._attachState = AttachState.Attached;
1457
1571
 
1572
+ timings.phase2 = performance.now();
1458
1573
  // Fetch specified snapshot.
1459
1574
  const { snapshot, versionId } =
1460
1575
  pendingLocalState === undefined
@@ -1469,7 +1584,10 @@ export class Container
1469
1584
  if (this.offlineLoadEnabled) {
1470
1585
  this.baseSnapshot = snapshot;
1471
1586
  // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1472
- this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1587
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(
1588
+ snapshot,
1589
+ this.storageAdapter,
1590
+ );
1473
1591
  }
1474
1592
  }
1475
1593
 
@@ -1486,6 +1604,57 @@ export class Container
1486
1604
 
1487
1605
  let opsBeforeReturnP: Promise<void> | undefined;
1488
1606
 
1607
+ if (loadMode.pauseAfterLoad === true) {
1608
+ // If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
1609
+ if (loadMode.opsBeforeReturn === "sequenceNumber") {
1610
+ assert(
1611
+ loadToSequenceNumber !== undefined,
1612
+ 0x727 /* sequenceNumber should be defined */,
1613
+ );
1614
+ // Note: It is possible that we think the latest snapshot is newer than the specified sequence number
1615
+ // due to saved ops that may be replayed after the snapshot.
1616
+ // https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
1617
+ if (dmAttributes.sequenceNumber > loadToSequenceNumber) {
1618
+ throw new Error(
1619
+ "Cannot satisfy request to pause the container at the specified sequence number. Most recent snapshot is newer than the specified sequence number.",
1620
+ );
1621
+ }
1622
+ }
1623
+
1624
+ // Force readonly mode - this will ensure we don't receive an error for the lack of join op
1625
+ this.forceReadonly(true);
1626
+
1627
+ // We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
1628
+ const opHandler = () => {
1629
+ if (loadToSequenceNumber === undefined) {
1630
+ // If there is no specified sequence number, pause after the inbound queue is empty.
1631
+ if (this.deltaManager.inbound.length !== 0) {
1632
+ return;
1633
+ }
1634
+ } else {
1635
+ // If there is a specified sequence number, keep processing until we reach it.
1636
+ if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
1637
+ return;
1638
+ }
1639
+ }
1640
+
1641
+ // Pause op processing once we have processed the desired number of ops.
1642
+ void this.deltaManager.inbound.pause();
1643
+ void this.deltaManager.outbound.pause();
1644
+ this.off("op", opHandler);
1645
+ };
1646
+ if (
1647
+ (loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
1648
+ this.deltaManager.lastSequenceNumber === loadToSequenceNumber
1649
+ ) {
1650
+ // If we have already reached the desired sequence number, call opHandler() to pause immediately.
1651
+ opHandler();
1652
+ } else {
1653
+ // If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
1654
+ this.on("op", opHandler);
1655
+ }
1656
+ }
1657
+
1489
1658
  // Attach op handlers to finish initialization and be able to start processing ops
1490
1659
  // Kick off any ops fetching if required.
1491
1660
  switch (loadMode.opsBeforeReturn) {
@@ -1497,11 +1666,13 @@ export class Container
1497
1666
  loadMode.deltaConnection !== "none" ? "all" : "none",
1498
1667
  );
1499
1668
  break;
1669
+ case "sequenceNumber":
1500
1670
  case "cached":
1501
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
1502
- break;
1503
1671
  case "all":
1504
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
1672
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(
1673
+ dmAttributes,
1674
+ loadMode.opsBeforeReturn,
1675
+ );
1505
1676
  break;
1506
1677
  default:
1507
1678
  unreachableCase(loadMode.opsBeforeReturn);
@@ -1511,12 +1682,13 @@ export class Container
1511
1682
  // Initialize the protocol handler
1512
1683
  await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
1513
1684
 
1685
+ timings.phase3 = performance.now();
1514
1686
  const codeDetails = this.getCodeDetailsFromQuorum();
1515
- await this.instantiateContext(
1516
- true, // existing
1687
+ await this.instantiateRuntime(
1517
1688
  codeDetails,
1518
1689
  snapshot,
1519
- pendingLocalState?.pendingRuntimeState,
1690
+ // give runtime a dummy value so it knows we're loading from a stash blob
1691
+ pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined,
1520
1692
  );
1521
1693
 
1522
1694
  // replay saved ops
@@ -1525,16 +1697,9 @@ export class Container
1525
1697
  this.processRemoteMessage(message);
1526
1698
 
1527
1699
  // allow runtime to apply stashed ops at this op's sequence number
1528
- await this.context.notifyOpReplay(message);
1700
+ await this.runtime.notifyOpReplay?.(message);
1529
1701
  }
1530
1702
  pendingLocalState.savedOps = [];
1531
-
1532
- // now set clientId to stashed clientId so live ops are correctly processed as local
1533
- assert(
1534
- this.clientId === undefined,
1535
- 0x5d6 /* Unexpected clientId when setting stashed clientId */,
1536
- );
1537
- this._clientId = pendingLocalState?.clientId;
1538
1703
  }
1539
1704
 
1540
1705
  // We might have hit some failure that did not manifest itself in exception in this flow,
@@ -1558,27 +1723,27 @@ export class Container
1558
1723
  this._deltaManager.inbound.pause();
1559
1724
  }
1560
1725
 
1561
- switch (loadMode.deltaConnection) {
1562
- case undefined:
1563
- if (pendingLocalState) {
1564
- // connect to delta stream now since we did not before
1565
- this.connectToDeltaStream(connectionArgs);
1726
+ this.handleDeltaConnectionArg(
1727
+ connectionArgs,
1728
+ loadMode.deltaConnection,
1729
+ pendingLocalState !== undefined,
1730
+ );
1731
+ }
1732
+
1733
+ // If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
1734
+ if (
1735
+ loadToSequenceNumber !== undefined &&
1736
+ this.deltaManager.lastSequenceNumber < loadToSequenceNumber
1737
+ ) {
1738
+ await new Promise<void>((resolve, reject) => {
1739
+ const opHandler = (message: ISequencedDocumentMessage) => {
1740
+ if (message.sequenceNumber >= loadToSequenceNumber) {
1741
+ resolve();
1742
+ this.off("op", opHandler);
1566
1743
  }
1567
- // intentional fallthrough
1568
- case "delayed":
1569
- assert(
1570
- this.inboundQueuePausedFromInit,
1571
- 0x346 /* inboundQueuePausedFromInit should be true */,
1572
- );
1573
- this.inboundQueuePausedFromInit = false;
1574
- this._deltaManager.inbound.resume();
1575
- this._deltaManager.inboundSignal.resume();
1576
- break;
1577
- case "none":
1578
- break;
1579
- default:
1580
- unreachableCase(loadMode.deltaConnection);
1581
- }
1744
+ };
1745
+ this.on("op", opHandler);
1746
+ });
1582
1747
  }
1583
1748
 
1584
1749
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
@@ -1592,7 +1757,15 @@ export class Container
1592
1757
 
1593
1758
  // Internal context is fully loaded at this point
1594
1759
  this.setLoaded();
1595
-
1760
+ timings.end = performance.now();
1761
+ this.subLogger.sendTelemetryEvent(
1762
+ {
1763
+ eventName: "LoadStagesTimings",
1764
+ details: JSON.stringify(timings),
1765
+ },
1766
+ undefined,
1767
+ LogLevel.verbose,
1768
+ );
1596
1769
  return {
1597
1770
  sequenceNumber: attributes.sequenceNumber,
1598
1771
  version: versionId,
@@ -1601,7 +1774,7 @@ export class Container
1601
1774
  };
1602
1775
  }
1603
1776
 
1604
- private async createDetached(source: IFluidCodeDetails) {
1777
+ private async createDetached(codeDetails: IFluidCodeDetails) {
1605
1778
  const attributes: IDocumentAttributes = {
1606
1779
  sequenceNumber: detachedContainerRefSeqNumber,
1607
1780
  term: OnlyValidTermValue,
@@ -1611,7 +1784,7 @@ export class Container
1611
1784
  await this.attachDeltaManagerOpHandler(attributes);
1612
1785
 
1613
1786
  // Need to just seed the source data in the code quorum. Quorum itself is empty
1614
- const qValues = initQuorumValuesFromCodeDetails(source);
1787
+ const qValues = initQuorumValuesFromCodeDetails(codeDetails);
1615
1788
  this.initializeProtocolState(
1616
1789
  attributes,
1617
1790
  {
@@ -1621,10 +1794,7 @@ export class Container
1621
1794
  }, // IQuorumSnapShot
1622
1795
  );
1623
1796
 
1624
- // The load context - given we seeded the quorum - will be great
1625
- await this.instantiateContextDetached(
1626
- false, // existing
1627
- );
1797
+ await this.instantiateRuntime(codeDetails, undefined);
1628
1798
 
1629
1799
  this.setLoaded();
1630
1800
  }
@@ -1650,21 +1820,17 @@ export class Container
1650
1820
  this.storageAdapter,
1651
1821
  baseTree.blobs.quorumValues,
1652
1822
  );
1653
- const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1654
1823
  this.initializeProtocolState(
1655
1824
  attributes,
1656
1825
  {
1657
1826
  members: [],
1658
1827
  proposals: [],
1659
- values:
1660
- codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1828
+ values: qValues,
1661
1829
  }, // IQuorumSnapShot
1662
1830
  );
1831
+ const codeDetails = this.getCodeDetailsFromQuorum();
1663
1832
 
1664
- await this.instantiateContextDetached(
1665
- true, // existing
1666
- snapshotTree,
1667
- );
1833
+ await this.instantiateRuntime(codeDetails, snapshotTree);
1668
1834
 
1669
1835
  this.setLoaded();
1670
1836
  }
@@ -1733,7 +1899,10 @@ export class Container
1733
1899
  this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1734
1900
  );
1735
1901
 
1736
- const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1902
+ const protocolLogger = createChildLogger({
1903
+ logger: this.subLogger,
1904
+ namespace: "ProtocolHandler",
1905
+ });
1737
1906
 
1738
1907
  protocol.quorum.on("error", (error) => {
1739
1908
  protocolLogger.sendErrorEvent(error);
@@ -1844,7 +2013,7 @@ export class Container
1844
2013
  const serviceProvider = () => this.service;
1845
2014
  const deltaManager = new DeltaManager<ConnectionManager>(
1846
2015
  serviceProvider,
1847
- ChildLogger.create(this.subLogger, "DeltaManager"),
2016
+ createChildLogger({ logger: this.subLogger, namespace: "DeltaManager" }),
1848
2017
  () => this.activeConnection(),
1849
2018
  (props: IConnectionManagerFactoryArgs) =>
1850
2019
  new ConnectionManager(
@@ -1852,7 +2021,7 @@ export class Container
1852
2021
  () => this.isDirty,
1853
2022
  this.client,
1854
2023
  this._canReconnect,
1855
- ChildLogger.create(this.subLogger, "ConnectionManager"),
2024
+ createChildLogger({ logger: this.subLogger, namespace: "ConnectionManager" }),
1856
2025
  props,
1857
2026
  ),
1858
2027
  );
@@ -1868,18 +2037,18 @@ export class Container
1868
2037
  this.connectionStateHandler.receivedConnectEvent(details);
1869
2038
  });
1870
2039
 
1871
- deltaManager.on("establishingConnection", (reason: string) => {
2040
+ deltaManager.on("establishingConnection", (reason: IConnectionStateChangeReason) => {
1872
2041
  this.connectionStateHandler.establishingConnection(reason);
1873
2042
  });
1874
2043
 
1875
- deltaManager.on("cancelEstablishingConnection", (reason: string) => {
2044
+ deltaManager.on("cancelEstablishingConnection", (reason: IConnectionStateChangeReason) => {
1876
2045
  this.connectionStateHandler.cancelEstablishingConnection(reason);
1877
2046
  });
1878
2047
 
1879
- deltaManager.on("disconnect", (reason: string, error?: IAnyDriverError) => {
1880
- this.collabWindowTracker?.stopSequenceNumberUpdate();
2048
+ deltaManager.on("disconnect", (reason: IConnectionStateChangeReason) => {
2049
+ this.noopHeuristic?.notifyDisconnect();
1881
2050
  if (!this.closed) {
1882
- this.connectionStateHandler.receivedDisconnectEvent(reason, error);
2051
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1883
2052
  }
1884
2053
  });
1885
2054
 
@@ -1914,7 +2083,7 @@ export class Container
1914
2083
 
1915
2084
  private async attachDeltaManagerOpHandler(
1916
2085
  attributes: IDocumentAttributes,
1917
- prefetchType?: "cached" | "all" | "none",
2086
+ prefetchType?: "sequenceNumber" | "cached" | "all" | "none",
1918
2087
  ) {
1919
2088
  return this._deltaManager.attachOpHandler(
1920
2089
  attributes.minimumSequenceNumber,
@@ -1932,8 +2101,7 @@ export class Container
1932
2101
  private logConnectionStateChangeTelemetry(
1933
2102
  value: ConnectionState,
1934
2103
  oldState: ConnectionState,
1935
- reason?: string,
1936
- error?: IAnyDriverError,
2104
+ reason?: IConnectionStateChangeReason,
1937
2105
  ) {
1938
2106
  // Log actual event
1939
2107
  const time = performance.now();
@@ -1951,11 +2119,15 @@ export class Container
1951
2119
  if (value === ConnectionState.Connected) {
1952
2120
  durationFromDisconnected =
1953
2121
  time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1954
- durationFromDisconnected = TelemetryLogger.formatTick(durationFromDisconnected);
2122
+ durationFromDisconnected = formatTick(durationFromDisconnected);
1955
2123
  } else if (value === ConnectionState.CatchingUp) {
1956
2124
  // This info is of most interesting while Catching Up.
1957
2125
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1958
- if (this.deltaManager.hasCheckpointSequenceNumber) {
2126
+ // Need to check that we have already loaded and fetched the snapshot.
2127
+ if (
2128
+ this.deltaManager.hasCheckpointSequenceNumber &&
2129
+ this._lifecycleState === "loaded"
2130
+ ) {
1959
2131
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1960
2132
  }
1961
2133
  }
@@ -1968,7 +2140,7 @@ export class Container
1968
2140
  from: ConnectionState[oldState],
1969
2141
  duration,
1970
2142
  durationFromDisconnected,
1971
- reason,
2143
+ reason: reason?.text,
1972
2144
  connectionInitiationReason,
1973
2145
  pendingClientId: this.connectionStateHandler.pendingClientId,
1974
2146
  clientId: this.clientId,
@@ -1981,9 +2153,10 @@ export class Container
1981
2153
  : undefined,
1982
2154
  checkpointSequenceNumber,
1983
2155
  quorumSize: this._protocolHandler?.quorum.getMembers().size,
2156
+ isDirty: this.isDirty,
1984
2157
  ...this._deltaManager.connectionProps,
1985
2158
  },
1986
- error,
2159
+ reason?.error,
1987
2160
  );
1988
2161
 
1989
2162
  if (value === ConnectionState.Connected) {
@@ -1991,7 +2164,10 @@ export class Container
1991
2164
  }
1992
2165
  }
1993
2166
 
1994
- private propagateConnectionState(initialTransition: boolean, disconnectedReason?: string) {
2167
+ private propagateConnectionState(
2168
+ initialTransition: boolean,
2169
+ disconnectedReason?: IConnectionStateChangeReason,
2170
+ ) {
1995
2171
  // When container loaded, we want to propagate initial connection state.
1996
2172
  // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1997
2173
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
@@ -2004,26 +2180,11 @@ export class Container
2004
2180
  }
2005
2181
  const state = this.connectionState === ConnectionState.Connected;
2006
2182
 
2007
- const logOpsOnReconnect: boolean =
2008
- this.connectionState === ConnectionState.Connected &&
2009
- !this.firstConnection &&
2010
- this.connectionMode === "write";
2011
- if (logOpsOnReconnect) {
2012
- this.messageCountAfterDisconnection = 0;
2013
- }
2014
-
2015
2183
  // Both protocol and context should not be undefined if we got so far.
2016
2184
 
2017
2185
  this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
2018
2186
  this.protocolHandler.setConnectionState(state, this.clientId);
2019
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
2020
-
2021
- if (logOpsOnReconnect) {
2022
- this.mc.logger.sendTelemetryEvent({
2023
- eventName: "OpsSentOnReconnect",
2024
- count: this.messageCountAfterDisconnection,
2025
- });
2026
- }
2187
+ raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
2027
2188
  }
2028
2189
 
2029
2190
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
@@ -2099,8 +2260,7 @@ export class Container
2099
2260
  return -1;
2100
2261
  }
2101
2262
 
2102
- this.messageCountAfterDisconnection += 1;
2103
- this.collabWindowTracker?.stopSequenceNumberUpdate();
2263
+ this.noopHeuristic?.notifyMessageSent();
2104
2264
  return this._deltaManager.submit(
2105
2265
  type,
2106
2266
  contents,
@@ -2121,35 +2281,41 @@ export class Container
2121
2281
  const result = this.protocolHandler.processMessage(message, local);
2122
2282
 
2123
2283
  // Forward messages to the loaded runtime for processing
2124
- this.context.process(message, local);
2284
+ this.runtime.process(message, local);
2125
2285
 
2126
2286
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
2127
2287
  if (this.activeConnection()) {
2128
- if (this.collabWindowTracker === undefined) {
2288
+ if (this.noopHeuristic === undefined) {
2289
+ const serviceConfiguration = this.deltaManager.serviceConfiguration;
2129
2290
  // Note that config from first connection will be used for this container's lifetime.
2130
2291
  // That means that if relay service changes settings, such changes will impact only newly booted
2131
2292
  // clients.
2132
2293
  // All existing will continue to use settings they got earlier.
2133
2294
  assert(
2134
- this.serviceConfiguration !== undefined,
2295
+ serviceConfiguration !== undefined,
2135
2296
  0x2e4 /* "there should be service config for active connection" */,
2136
2297
  );
2137
- this.collabWindowTracker = new CollabWindowTracker(
2138
- (type) => {
2139
- assert(
2140
- this.activeConnection(),
2141
- 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */,
2142
- );
2143
- this.submitMessage(type);
2144
- },
2145
- this.serviceConfiguration.noopTimeFrequency,
2146
- this.serviceConfiguration.noopCountFrequency,
2298
+ this.noopHeuristic = new NoopHeuristic(
2299
+ serviceConfiguration.noopTimeFrequency,
2300
+ serviceConfiguration.noopCountFrequency,
2147
2301
  );
2302
+ this.noopHeuristic.on("wantsNoop", () => {
2303
+ // On disconnect we notify the heuristic which should prevent it from wanting a noop.
2304
+ // Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
2305
+ // running the microtask that the heuristic queued in response.
2306
+ assert(
2307
+ this.activeConnection(),
2308
+ 0x241 /* "Trying to send noop without active connection" */,
2309
+ );
2310
+ this.submitMessage(MessageType.NoOp);
2311
+ });
2312
+ }
2313
+ this.noopHeuristic.notifyMessageProcessed(message);
2314
+ // The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
2315
+ if (result.immediateNoOp === true) {
2316
+ // ADO:1385: Remove cast and use MessageType once definition changes propagate
2317
+ this.submitMessage(MessageType2.Accept as unknown as MessageType);
2148
2318
  }
2149
- this.collabWindowTracker.scheduleSequenceNumberUpdate(
2150
- message,
2151
- result.immediateNoOp === true,
2152
- );
2153
2319
  }
2154
2320
 
2155
2321
  this.emit("op", message);
@@ -2161,11 +2327,11 @@ export class Container
2161
2327
 
2162
2328
  private processSignal(message: ISignalMessage) {
2163
2329
  // No clientId indicates a system signal message.
2164
- if (message.clientId === null) {
2330
+ if (protocolHandlerShouldProcessSignal(message)) {
2165
2331
  this.protocolHandler.processSignal(message);
2166
2332
  } else {
2167
2333
  const local = this.clientId === message.clientId;
2168
- this.context.processSignal(message, local);
2334
+ this.runtime.processSignal(message, local);
2169
2335
  }
2170
2336
  }
2171
2337
 
@@ -2195,35 +2361,54 @@ export class Container
2195
2361
  return { snapshot, versionId: version?.id };
2196
2362
  }
2197
2363
 
2198
- private async instantiateContextDetached(existing: boolean, snapshot?: ISnapshotTree) {
2199
- const codeDetails = this.getCodeDetailsFromQuorum();
2200
- if (codeDetails === undefined) {
2201
- throw new Error("pkg should be provided in create flow!!");
2202
- }
2203
-
2204
- await this.instantiateContext(existing, codeDetails, snapshot);
2205
- }
2206
-
2207
- private async instantiateContext(
2208
- existing: boolean,
2364
+ private async instantiateRuntime(
2209
2365
  codeDetails: IFluidCodeDetails,
2210
- snapshot?: ISnapshotTree,
2366
+ snapshot: ISnapshotTree | undefined,
2211
2367
  pendingLocalState?: unknown,
2212
2368
  ) {
2213
- assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
2369
+ assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
2214
2370
 
2215
2371
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
2216
2372
  // are set. Global requests will still go directly to the loader
2217
2373
  const maybeLoader: FluidObject<IHostLoader> = this.scope;
2218
2374
  const loader = new RelativeLoader(this, maybeLoader.ILoader);
2219
- this._context = await ContainerContext.createOrLoad(
2220
- this,
2375
+
2376
+ const loadCodeResult = await PerformanceEvent.timedExecAsync(
2377
+ this.subLogger,
2378
+ { eventName: "CodeLoad" },
2379
+ async () => this.codeLoader.load(codeDetails),
2380
+ );
2381
+
2382
+ this._loadedModule = {
2383
+ module: loadCodeResult.module,
2384
+ // An older interface ICodeLoader could return an IFluidModule which didn't have details.
2385
+ // If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
2386
+ // TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
2387
+ details: loadCodeResult.details ?? codeDetails,
2388
+ };
2389
+
2390
+ const fluidExport: FluidObject<IProvideRuntimeFactory> | undefined =
2391
+ this._loadedModule.module.fluidExport;
2392
+ const runtimeFactory = fluidExport?.IRuntimeFactory;
2393
+ if (runtimeFactory === undefined) {
2394
+ throw new Error(packageNotFactoryError);
2395
+ }
2396
+
2397
+ const getSpecifiedCodeDetails = () =>
2398
+ (this.protocolHandler.quorum.get("code") ??
2399
+ this.protocolHandler.quorum.get("code2")) as IFluidCodeDetails | undefined;
2400
+
2401
+ const existing = snapshot !== undefined;
2402
+
2403
+ const context = new ContainerContext(
2404
+ this.options,
2221
2405
  this.scope,
2222
- this.codeLoader,
2223
- codeDetails,
2224
2406
  snapshot,
2225
- new DeltaManagerProxy(this._deltaManager),
2226
- new QuorumProxy(this.protocolHandler.quorum),
2407
+ this._loadedFromVersion,
2408
+ this._deltaManager,
2409
+ this.storageAdapter,
2410
+ this.protocolHandler.quorum,
2411
+ this.protocolHandler.audience,
2227
2412
  loader,
2228
2413
  (type, contents, batch, metadata) =>
2229
2414
  this.submitContainerMessage(type, contents, batch, metadata),
@@ -2234,22 +2419,36 @@ export class Container
2234
2419
  (message) => this.submitSignal(message),
2235
2420
  (error?: ICriticalContainerError) => this.dispose(error),
2236
2421
  (error?: ICriticalContainerError) => this.close(error),
2237
- Container.version,
2238
- (dirty: boolean) => this.updateDirtyContainerState(dirty),
2422
+ this.updateDirtyContainerState,
2423
+ this.getAbsoluteUrl,
2424
+ () => this.resolvedUrl?.id,
2425
+ () => this.clientId,
2426
+ () => this.attachState,
2427
+ () => this.connected,
2428
+ getSpecifiedCodeDetails,
2429
+ this._deltaManager.clientDetails,
2239
2430
  existing,
2431
+ this.subLogger,
2240
2432
  pendingLocalState,
2241
2433
  );
2242
2434
 
2243
- this.emit("contextChanged", codeDetails);
2435
+ this._runtime = await PerformanceEvent.timedExecAsync(
2436
+ this.subLogger,
2437
+ { eventName: "InstantiateRuntime" },
2438
+ async () => runtimeFactory.instantiateRuntime(context, existing),
2439
+ );
2440
+ this._lifecycleEvents.emit("runtimeInstantiated");
2441
+
2442
+ this._loadedCodeDetails = codeDetails;
2244
2443
  }
2245
2444
 
2246
- private updateDirtyContainerState(dirty: boolean) {
2445
+ private readonly updateDirtyContainerState = (dirty: boolean) => {
2247
2446
  if (this._dirtyContainer === dirty) {
2248
2447
  return;
2249
2448
  }
2250
2449
  this._dirtyContainer = dirty;
2251
2450
  this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
2252
- }
2451
+ };
2253
2452
 
2254
2453
  /**
2255
2454
  * Set the connected state of the ContainerContext
@@ -2258,21 +2457,49 @@ export class Container
2258
2457
  * @param readonly - Is the container in readonly mode?
2259
2458
  */
2260
2459
  private setContextConnectedState(state: boolean, readonly: boolean): void {
2261
- if (this._context?.disposed === false) {
2460
+ if (this._runtime?.disposed === false) {
2262
2461
  /**
2263
2462
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2264
2463
  * ops getting through to the DeltaManager.
2265
2464
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2266
2465
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2267
2466
  */
2268
- this.context.setConnectionState(state && !readonly, this.clientId);
2467
+ this.runtime.setConnectionState(state && !readonly, this.clientId);
2468
+ }
2469
+ }
2470
+
2471
+ private handleDeltaConnectionArg(
2472
+ connectionArgs: IConnectionArgs,
2473
+ deltaConnectionArg?: "none" | "delayed",
2474
+ canConnect: boolean = true,
2475
+ ) {
2476
+ switch (deltaConnectionArg) {
2477
+ case undefined:
2478
+ if (canConnect) {
2479
+ // connect to delta stream now since we did not before
2480
+ this.connectToDeltaStream(connectionArgs);
2481
+ }
2482
+ // intentional fallthrough
2483
+ case "delayed":
2484
+ assert(
2485
+ this.inboundQueuePausedFromInit,
2486
+ 0x346 /* inboundQueuePausedFromInit should be true */,
2487
+ );
2488
+ this.inboundQueuePausedFromInit = false;
2489
+ this._deltaManager.inbound.resume();
2490
+ this._deltaManager.inboundSignal.resume();
2491
+ break;
2492
+ case "none":
2493
+ break;
2494
+ default:
2495
+ unreachableCase(deltaConnectionArg);
2269
2496
  }
2270
2497
  }
2271
2498
  }
2272
2499
 
2273
2500
  /**
2274
2501
  * IContainer interface that includes experimental features still under development.
2275
- * @internal
2502
+ * @experimental
2276
2503
  */
2277
2504
  export interface IContainerExperimental extends IContainer {
2278
2505
  /**
@@ -2283,5 +2510,13 @@ export interface IContainerExperimental extends IContainer {
2283
2510
  * @experimental misuse of this API can result in duplicate op submission and potential document corruption
2284
2511
  * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2285
2512
  */
2286
- getPendingLocalState(): string;
2513
+ getPendingLocalState?(): Promise<string>;
2514
+
2515
+ /**
2516
+ * Closes the container and returns serialized local state intended to be
2517
+ * given to a newly loaded container.
2518
+ * @experimental
2519
+ * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2520
+ */
2521
+ closeAndGetPendingLocalState?(): Promise<string>;
2287
2522
  }