@fluidframework/container-loader 0.52.0 → 0.54.0-47413

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 (77) hide show
  1. package/dist/connectionManager.d.ts +153 -0
  2. package/dist/connectionManager.d.ts.map +1 -0
  3. package/dist/connectionManager.js +664 -0
  4. package/dist/connectionManager.js.map +1 -0
  5. package/dist/connectionStateHandler.d.ts +1 -0
  6. package/dist/connectionStateHandler.d.ts.map +1 -1
  7. package/dist/connectionStateHandler.js +6 -0
  8. package/dist/connectionStateHandler.js.map +1 -1
  9. package/dist/container.d.ts +2 -22
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js +121 -151
  12. package/dist/container.js.map +1 -1
  13. package/dist/containerContext.d.ts +1 -0
  14. package/dist/containerContext.d.ts.map +1 -1
  15. package/dist/containerContext.js +4 -0
  16. package/dist/containerContext.js.map +1 -1
  17. package/dist/contracts.d.ts +112 -0
  18. package/dist/contracts.d.ts.map +1 -0
  19. package/dist/contracts.js +14 -0
  20. package/dist/contracts.js.map +1 -0
  21. package/dist/deltaManager.d.ts +26 -142
  22. package/dist/deltaManager.d.ts.map +1 -1
  23. package/dist/deltaManager.js +143 -770
  24. package/dist/deltaManager.js.map +1 -1
  25. package/dist/loader.d.ts +14 -4
  26. package/dist/loader.d.ts.map +1 -1
  27. package/dist/loader.js +10 -4
  28. package/dist/loader.js.map +1 -1
  29. package/dist/packageVersion.d.ts +1 -1
  30. package/dist/packageVersion.d.ts.map +1 -1
  31. package/dist/packageVersion.js +1 -1
  32. package/dist/packageVersion.js.map +1 -1
  33. package/dist/protocolTreeDocumentStorageService.d.ts +2 -2
  34. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  35. package/lib/connectionManager.d.ts +153 -0
  36. package/lib/connectionManager.d.ts.map +1 -0
  37. package/lib/connectionManager.js +660 -0
  38. package/lib/connectionManager.js.map +1 -0
  39. package/lib/connectionStateHandler.d.ts +1 -0
  40. package/lib/connectionStateHandler.d.ts.map +1 -1
  41. package/lib/connectionStateHandler.js +6 -0
  42. package/lib/connectionStateHandler.js.map +1 -1
  43. package/lib/container.d.ts +2 -22
  44. package/lib/container.d.ts.map +1 -1
  45. package/lib/container.js +122 -152
  46. package/lib/container.js.map +1 -1
  47. package/lib/containerContext.d.ts +1 -0
  48. package/lib/containerContext.d.ts.map +1 -1
  49. package/lib/containerContext.js +4 -0
  50. package/lib/containerContext.js.map +1 -1
  51. package/lib/contracts.d.ts +112 -0
  52. package/lib/contracts.d.ts.map +1 -0
  53. package/lib/contracts.js +11 -0
  54. package/lib/contracts.js.map +1 -0
  55. package/lib/deltaManager.d.ts +26 -142
  56. package/lib/deltaManager.d.ts.map +1 -1
  57. package/lib/deltaManager.js +147 -774
  58. package/lib/deltaManager.js.map +1 -1
  59. package/lib/loader.d.ts +14 -4
  60. package/lib/loader.d.ts.map +1 -1
  61. package/lib/loader.js +11 -5
  62. package/lib/loader.js.map +1 -1
  63. package/lib/packageVersion.d.ts +1 -1
  64. package/lib/packageVersion.d.ts.map +1 -1
  65. package/lib/packageVersion.js +1 -1
  66. package/lib/packageVersion.js.map +1 -1
  67. package/lib/protocolTreeDocumentStorageService.d.ts +2 -2
  68. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  69. package/package.json +9 -9
  70. package/src/connectionManager.ts +892 -0
  71. package/src/connectionStateHandler.ts +8 -0
  72. package/src/container.ts +165 -187
  73. package/src/containerContext.ts +4 -0
  74. package/src/contracts.ts +156 -0
  75. package/src/deltaManager.ts +181 -978
  76. package/src/loader.ts +59 -27
  77. package/src/packageVersion.ts +1 -1
@@ -60,6 +60,8 @@ export class ConnectionStateHandler {
60
60
  // Default is 90 sec for which we are going to wait for its own "leave" message.
61
61
  this.handler.maxClientLeaveWaitTime ?? 90000,
62
62
  () => {
63
+ assert(!this.connected,
64
+ 0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */);
63
65
  this.applyForConnectedState("timeout");
64
66
  },
65
67
  );
@@ -89,6 +91,11 @@ export class ConnectionStateHandler {
89
91
  this.joinOpTimer.clear();
90
92
  }
91
93
 
94
+ public dispose() {
95
+ assert(!this.joinOpTimer.hasTimer, 0x2a5 /* "join timer" */);
96
+ this.prevClientLeftTimer.clear();
97
+ }
98
+
92
99
  public receivedAddMemberEvent(clientId: string) {
93
100
  // This is the only one that requires the pending client ID
94
101
  if (clientId === this.pendingClientId) {
@@ -181,6 +188,7 @@ export class ConnectionStateHandler {
181
188
  if ((protocolHandler !== undefined && protocolHandler.quorum.getMember(details.clientId) !== undefined)
182
189
  || connectionMode === "read"
183
190
  ) {
191
+ assert(!this.prevClientLeftTimer.hasTimer, 0x2a6 /* "there should be no timer for 'read' connections" */);
184
192
  this.setConnectionState(ConnectionState.Connected);
185
193
  } else if (connectionMode === "write") {
186
194
  this.startJoinOpTimer();
package/src/container.ts CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  ensureFluidResolvedUrl,
52
52
  combineAppAndProtocolSummary,
53
53
  runWithRetry,
54
- canRetryOnError,
54
+ isFluidResolvedUrl,
55
55
  } from "@fluidframework/driver-utils";
56
56
  import {
57
57
  isSystemMessage,
@@ -96,7 +96,8 @@ import {
96
96
  } from "@fluidframework/telemetry-utils";
97
97
  import { Audience } from "./audience";
98
98
  import { ContainerContext } from "./containerContext";
99
- import { IConnectionArgs, DeltaManager, ReconnectMode } from "./deltaManager";
99
+ import { ReconnectMode, IConnectionManagerFactoryArgs } from "./contracts";
100
+ import { DeltaManager, IConnectionArgs } from "./deltaManager";
100
101
  import { DeltaManagerProxy } from "./deltaManagerProxy";
101
102
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
102
103
  import { pkgVersion } from "./packageVersion";
@@ -107,6 +108,7 @@ import { BlobOnlyStorage, ContainerStorageAdapter } from "./containerStorageAdap
107
108
  import { getSnapshotTreeFromSerializedContainer } from "./utils";
108
109
  import { QuorumProxy } from "./quorum";
109
110
  import { CollabWindowTracker } from "./collabWindowTracker";
111
+ import { ConnectionManager } from "./connectionManager";
110
112
 
111
113
  const detachedContainerRefSeqNumber = 0;
112
114
 
@@ -169,7 +171,7 @@ export enum ConnectionState {
169
171
  * false: storage does not provide indication of how far the client is. Container processed
170
172
  * all the ops known to it, but it maybe still behind.
171
173
  */
172
- export async function waitContainerToCatchUp(container: Container) {
174
+ export async function waitContainerToCatchUp(container: IContainer) {
173
175
  // Make sure we stop waiting if container is closed.
174
176
  if (container.closed) {
175
177
  throw new Error("Container is closed");
@@ -216,7 +218,9 @@ export async function waitContainerToCatchUp(container: Container) {
216
218
  };
217
219
  container.on(connectedEventName, callback);
218
220
 
219
- container.resume();
221
+ // TODO: Remove null check after next release #8523
222
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
223
+ container.resume!();
220
224
  });
221
225
  }
222
226
 
@@ -279,7 +283,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
279
283
  onClosed(err);
280
284
  });
281
285
  }),
282
- { start: true, end: true, cancel: "error" },
286
+ { start: true, end: true, cancel: "generic" },
283
287
  );
284
288
  }
285
289
 
@@ -293,9 +297,16 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
293
297
  const container = new Container(
294
298
  loader,
295
299
  {});
296
- container._lifecycleState = "loading";
297
- await container.createDetached(codeDetails);
298
- return container;
300
+
301
+ return PerformanceEvent.timedExecAsync(
302
+ container.logger,
303
+ { eventName: "CreateDetached" },
304
+ async (_event) => {
305
+ container._lifecycleState = "loading";
306
+ await container.createDetached(codeDetails);
307
+ return container;
308
+ },
309
+ { start: true, end: true, cancel: "generic" });
299
310
  }
300
311
 
301
312
  /**
@@ -309,10 +320,16 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
309
320
  const container = new Container(
310
321
  loader,
311
322
  {});
312
- const deserializedSummary = JSON.parse(snapshot) as ISummaryTree;
313
- container._lifecycleState = "loading";
314
- await container.rehydrateDetachedFromSnapshot(deserializedSummary);
315
- return container;
323
+ return PerformanceEvent.timedExecAsync(
324
+ container.logger,
325
+ { eventName: "RehydrateDetachedFromSnapshot" },
326
+ async (_event) => {
327
+ const deserializedSummary = JSON.parse(snapshot) as ISummaryTree;
328
+ container._lifecycleState = "loading";
329
+ await container.rehydrateDetachedFromSnapshot(deserializedSummary);
330
+ return container;
331
+ },
332
+ { start: true, end: true, cancel: "generic" });
316
333
  }
317
334
 
318
335
  public subLogger: TelemetryLogger;
@@ -361,7 +378,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
361
378
  }
362
379
 
363
380
  private readonly clientDetailsOverride: IClientDetails | undefined;
364
- private readonly _deltaManager: DeltaManager;
381
+ private readonly _deltaManager: DeltaManager<ConnectionManager>;
365
382
  private service: IDocumentService | undefined;
366
383
  private readonly _audience: Audience;
367
384
 
@@ -402,6 +419,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
402
419
  this.loader.services.options?.noopCountFrequency,
403
420
  );
404
421
 
422
+ private get connectionMode() { return this._deltaManager.connectionManager.connectionMode; }
423
+
405
424
  public get IFluidRouter(): IFluidRouter { return this; }
406
425
 
407
426
  public get resolvedUrl(): IResolvedUrl | undefined {
@@ -412,33 +431,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
412
431
  return this._loadedFromVersion;
413
432
  }
414
433
 
415
- /**
416
- * Tells if container is in read-only mode.
417
- * Data stores should listen for "readonly" notifications and disallow user making changes to data stores.
418
- * Readonly state can be because of no storage write permission,
419
- * or due to host forcing readonly mode for container.
420
- *
421
- * We do not differentiate here between no write access to storage vs. host disallowing changes to container -
422
- * in all cases container runtime and data stores should respect readonly state and not allow local changes.
423
- *
424
- * It is undefined if we have not yet established websocket connection
425
- * and do not know if user has write access to a file.
426
- * @deprecated - use readOnlyInfo
427
- */
428
- public get readonly() {
429
- return this._deltaManager.readonly;
430
- }
431
-
432
- /**
433
- * Tells if user has no write permissions for file in storage
434
- * It is undefined if we have not yet established websocket connection
435
- * and do not know if user has write access to a file.
436
- * @deprecated - use readOnlyInfo
437
- */
438
- public get readonlyPermissions() {
439
- return this._deltaManager.readonlyPermissions;
440
- }
441
-
442
434
  public get readOnlyInfo(): ReadOnlyInfo {
443
435
  return this._deltaManager.readOnlyInfo;
444
436
  }
@@ -447,7 +439,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
447
439
  * Tracks host requiring read-only mode.
448
440
  */
449
441
  public forceReadonly(readonly: boolean) {
450
- this._deltaManager.forceReadonly(readonly);
442
+ this._deltaManager.connectionManager.forceReadonly(readonly);
451
443
  }
452
444
 
453
445
  public get id(): string {
@@ -487,7 +479,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
487
479
  * Set once this.connected is true, otherwise undefined
488
480
  */
489
481
  public get scopes(): string[] | undefined {
490
- return this._deltaManager.scopes;
482
+ return this._deltaManager.connectionManager.scopes;
491
483
  }
492
484
 
493
485
  public get clientDetails(): IClientDetails {
@@ -576,7 +568,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
576
568
  {
577
569
  all: {
578
570
  clientType, // Differentiating summarizer container from main container
579
- loaderVersion: pkgVersion,
580
571
  containerId: uuid(),
581
572
  docId: () => this.id,
582
573
  containerAttachState: () => this._attachState,
@@ -610,7 +601,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
610
601
  protocolHandler: () => this._protocolHandler,
611
602
  logConnectionStateChangeTelemetry: (value, oldState, reason) =>
612
603
  this.logConnectionStateChangeTelemetry(value, oldState, reason),
613
- shouldClientJoinWrite: () => this._deltaManager.shouldJoinWrite(),
604
+ shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
614
605
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
615
606
  logConnectionIssue: (eventName: string) => {
616
607
  // We get here when socket does not receive any ops on "write" connection, including
@@ -672,22 +663,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
672
663
  switch (event) {
673
664
  case dirtyContainerEvent:
674
665
  if (this._dirtyContainer) {
675
- listener(dirtyContainerEvent);
666
+ listener();
676
667
  }
677
668
  break;
678
669
  case savedContainerEvent:
679
670
  if (!this._dirtyContainer) {
680
- listener(savedContainerEvent);
671
+ listener();
681
672
  }
682
673
  break;
683
674
  case connectedEventName:
684
675
  if (this.connected) {
685
- listener(event, this.clientId);
676
+ listener(this.clientId);
686
677
  }
687
678
  break;
688
679
  case disconnectedEventName:
689
680
  if (!this.connected) {
690
- listener(event);
681
+ listener();
691
682
  }
692
683
  break;
693
684
  default:
@@ -719,6 +710,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
719
710
 
720
711
  this._protocolHandler?.close();
721
712
 
713
+ this.connectionStateHandler.dispose();
714
+
722
715
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
723
716
 
724
717
  assert(this.connectionState === ConnectionState.Disconnected,
@@ -786,113 +779,113 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
786
779
  }
787
780
 
788
781
  public async attach(request: IRequest): Promise<void> {
789
- if (this._lifecycleState !== "loaded") {
790
- throw new UsageError(`containerNotValidForAttach [${this._lifecycleState}]`);
791
- }
782
+ await PerformanceEvent.timedExecAsync(this.logger, { eventName: "Attach" }, async () => {
783
+ if (this._lifecycleState !== "loaded") {
784
+ throw new UsageError(`containerNotValidForAttach [${this._lifecycleState}]`);
785
+ }
792
786
 
793
- // If container is already attached or attach is in progress, throw an error.
794
- assert(this._attachState === AttachState.Detached && !this.attachStarted,
795
- 0x205 /* "attach() called more than once" */);
796
- this.attachStarted = true;
787
+ // If container is already attached or attach is in progress, throw an error.
788
+ assert(this._attachState === AttachState.Detached && !this.attachStarted,
789
+ 0x205 /* "attach() called more than once" */);
790
+ this.attachStarted = true;
797
791
 
798
- // If attachment blobs were uploaded in detached state we will go through a different attach flow
799
- const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined
800
- && this.loader.services.detachedBlobStorage.size > 0;
792
+ // If attachment blobs were uploaded in detached state we will go through a different attach flow
793
+ const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined
794
+ && this.loader.services.detachedBlobStorage.size > 0;
801
795
 
802
- try {
803
- assert(this.deltaManager.inbound.length === 0, 0x0d6 /* "Inbound queue should be empty when attaching" */);
804
-
805
- let summary: ISummaryTree;
806
- if (!hasAttachmentBlobs) {
807
- // Get the document state post attach - possibly can just call attach but we need to change the
808
- // semantics around what the attach means as far as async code goes.
809
- const appSummary: ISummaryTree = this.context.createSummary();
810
- const protocolSummary = this.captureProtocolSummary();
811
- summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
812
-
813
- // Set the state as attaching as we are starting the process of attaching container.
814
- // This should be fired after taking the summary because it is the place where we are
815
- // starting to attach the container to storage.
816
- // Also, this should only be fired in detached container.
817
- this._attachState = AttachState.Attaching;
818
- this.context.notifyAttaching();
819
- }
796
+ try {
797
+ assert(this.deltaManager.inbound.length === 0,
798
+ 0x0d6 /* "Inbound queue should be empty when attaching" */);
799
+
800
+ let summary: ISummaryTree;
801
+ if (!hasAttachmentBlobs) {
802
+ // Get the document state post attach - possibly can just call attach but we need to change the
803
+ // semantics around what the attach means as far as async code goes.
804
+ const appSummary: ISummaryTree = this.context.createSummary();
805
+ const protocolSummary = this.captureProtocolSummary();
806
+ summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
807
+
808
+ // Set the state as attaching as we are starting the process of attaching container.
809
+ // This should be fired after taking the summary because it is the place where we are
810
+ // starting to attach the container to storage.
811
+ // Also, this should only be fired in detached container.
812
+ this._attachState = AttachState.Attaching;
813
+ this.context.notifyAttaching();
814
+ }
820
815
 
821
- // Actually go and create the resolved document
822
- const createNewResolvedUrl = await this.urlResolver.resolve(request);
823
- ensureFluidResolvedUrl(createNewResolvedUrl);
824
- if (this.service === undefined) {
825
- this.service = await runWithRetry(
826
- async () => this.serviceFactory.createContainer(
827
- summary,
828
- createNewResolvedUrl,
829
- this.subLogger,
830
- ),
831
- "containerAttach",
832
- this.logger,
833
- {}, // progress
834
- );
835
- }
836
- const resolvedUrl = this.service.resolvedUrl;
837
- ensureFluidResolvedUrl(resolvedUrl);
838
- this._resolvedUrl = resolvedUrl;
839
- await this.connectStorageService();
840
-
841
- if (hasAttachmentBlobs) {
842
- // upload blobs to storage
843
- assert(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
844
-
845
- // build a table mapping IDs assigned locally to IDs assigned by storage and pass it to runtime to
846
- // support blob handles that only know about the local IDs
847
- const redirectTable = new Map<string, string>();
848
- // if new blobs are added while uploading, upload them too
849
- while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
850
- const newIds = this.loader.services.detachedBlobStorage.getBlobIds().filter(
851
- (id) => !redirectTable.has(id));
852
- for (const id of newIds) {
853
- const blob = await this.loader.services.detachedBlobStorage.readBlob(id);
854
- const response = await this.storageService.createBlob(blob);
855
- redirectTable.set(id, response.id);
856
- }
816
+ // Actually go and create the resolved document
817
+ const createNewResolvedUrl = await this.urlResolver.resolve(request);
818
+ ensureFluidResolvedUrl(createNewResolvedUrl);
819
+ if (this.service === undefined) {
820
+ this.service = await runWithRetry(
821
+ async () => this.serviceFactory.createContainer(
822
+ summary,
823
+ createNewResolvedUrl,
824
+ this.subLogger,
825
+ ),
826
+ "containerAttach",
827
+ this.logger,
828
+ {}, // progress
829
+ );
857
830
  }
831
+ const resolvedUrl = this.service.resolvedUrl;
832
+ ensureFluidResolvedUrl(resolvedUrl);
833
+ this._resolvedUrl = resolvedUrl;
834
+ await this.connectStorageService();
835
+
836
+ if (hasAttachmentBlobs) {
837
+ // upload blobs to storage
838
+ assert(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
839
+
840
+ // build a table mapping IDs assigned locally to IDs assigned by storage and pass it to runtime to
841
+ // support blob handles that only know about the local IDs
842
+ const redirectTable = new Map<string, string>();
843
+ // if new blobs are added while uploading, upload them too
844
+ while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
845
+ const newIds = this.loader.services.detachedBlobStorage.getBlobIds().filter(
846
+ (id) => !redirectTable.has(id));
847
+ for (const id of newIds) {
848
+ const blob = await this.loader.services.detachedBlobStorage.readBlob(id);
849
+ const response = await this.storageService.createBlob(blob);
850
+ redirectTable.set(id, response.id);
851
+ }
852
+ }
858
853
 
859
- // take summary and upload
860
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
861
- const protocolSummary = this.captureProtocolSummary();
862
- summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
854
+ // take summary and upload
855
+ const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
856
+ const protocolSummary = this.captureProtocolSummary();
857
+ summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
863
858
 
864
- this._attachState = AttachState.Attaching;
865
- this.context.notifyAttaching();
859
+ this._attachState = AttachState.Attaching;
860
+ this.context.notifyAttaching();
866
861
 
867
- await this.storageService.uploadSummaryWithContext(summary, {
868
- referenceSequenceNumber: 0,
869
- ackHandle: undefined,
870
- proposalHandle: undefined,
871
- });
872
- }
862
+ await this.storageService.uploadSummaryWithContext(summary, {
863
+ referenceSequenceNumber: 0,
864
+ ackHandle: undefined,
865
+ proposalHandle: undefined,
866
+ });
867
+ }
873
868
 
874
- this._attachState = AttachState.Attached;
875
- this.emit("attached");
869
+ this._attachState = AttachState.Attached;
870
+ this.emit("attached");
876
871
 
877
- // Propagate current connection state through the system.
878
- this.propagateConnectionState();
879
- if (!this.closed) {
880
- this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
881
- }
882
- } catch(error) {
883
- // we should retry upon any retriable errors, so we shouldn't see them here
884
- assert(!canRetryOnError(error), 0x24f /* "retriable error thrown from attach()" */);
885
-
886
- // add resolved URL on error object so that host has the ability to find this document and delete it
887
- const newError = normalizeError(error);
888
- const resolvedUrl = this.resolvedUrl;
889
- if (resolvedUrl) {
890
- ensureFluidResolvedUrl(resolvedUrl);
891
- newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
872
+ // Propagate current connection state through the system.
873
+ this.propagateConnectionState();
874
+ if (!this.closed) {
875
+ this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
876
+ }
877
+ } catch(error) {
878
+ // add resolved URL on error object so that host has the ability to find this document and delete it
879
+ const newError = normalizeError(error);
880
+ const resolvedUrl = this.resolvedUrl;
881
+ if (isFluidResolvedUrl(resolvedUrl)) {
882
+ newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
883
+ }
884
+ this.close(newError);
885
+ throw newError;
892
886
  }
893
- this.close(newError);
894
- throw newError;
895
- }
887
+ },
888
+ { start: true, end: true, cancel: "generic" });
896
889
  }
897
890
 
898
891
  public async request(path: IRequest): Promise<IResponse> {
@@ -928,7 +921,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
928
921
  throw new Error("Attempting to setAutoReconnect() a closed Container");
929
922
  }
930
923
  const mode = reconnect ? ReconnectMode.Enabled : ReconnectMode.Disabled;
931
- const currentMode = this._deltaManager.reconnectMode;
924
+ const currentMode = this._deltaManager.connectionManager.reconnectMode;
932
925
 
933
926
  if (currentMode === mode) {
934
927
  return;
@@ -940,12 +933,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
940
933
 
941
934
  this.logger.sendTelemetryEvent({
942
935
  eventName: reconnect ? "AutoReconnectEnabled" : "AutoReconnectDisabled",
943
- connectionMode: this._deltaManager.connectionMode,
936
+ connectionMode: this.connectionMode,
944
937
  connectionState: ConnectionState[this.connectionState],
945
938
  duration,
946
939
  });
947
940
 
948
- this._deltaManager.setAutoReconnect(mode);
941
+ this._deltaManager.connectionManager.setAutoReconnect(mode);
949
942
 
950
943
  // If container state is not attached and resumed, then don't connect to delta stream. Also don't set the
951
944
  // manual reconnection flag to true as we haven't made the initial connection yet.
@@ -956,13 +949,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
956
949
  }
957
950
 
958
951
  // Ensure connection to web socket
959
- this.connectToDeltaStream({ reason: "autoReconnect" }).catch((error) => {
960
- // All errors are reported through events ("error" / "disconnected") and telemetry in DeltaManager
961
- // So there shouldn't be a need to record error here.
962
- // But we have number of cases where reconnects do not happen, and no errors are recorded, so
963
- // adding this log point for easier diagnostics
964
- this.logger.sendTelemetryEvent({ eventName: "setAutoReconnectError" }, error);
965
- });
952
+ this.connectToDeltaStream({ reason: "autoReconnect" });
966
953
  }
967
954
  }
968
955
 
@@ -986,8 +973,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
986
973
  }
987
974
 
988
975
  // Ensure connection to web socket
989
- // All errors are reported through events ("error" / "disconnected") and telemetry in DeltaManager
990
- this.connectToDeltaStream(args).catch(() => { });
976
+ this.connectToDeltaStream(args);
991
977
  }
992
978
 
993
979
  /**
@@ -1142,7 +1128,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1142
1128
  }
1143
1129
  }
1144
1130
 
1145
- private async connectToDeltaStream(args: IConnectionArgs) {
1131
+ private connectToDeltaStream(args: IConnectionArgs) {
1146
1132
  this.recordConnectStartTime();
1147
1133
 
1148
1134
  // All agents need "write" access, including summarizer.
@@ -1150,7 +1136,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1150
1136
  args.mode = "write";
1151
1137
  }
1152
1138
 
1153
- return this._deltaManager.connect(args);
1139
+ this._deltaManager.connect(args);
1154
1140
  }
1155
1141
 
1156
1142
  /**
@@ -1170,8 +1156,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1170
1156
  }
1171
1157
  this.service = await this.serviceFactory.createDocumentService(this._resolvedUrl, this.subLogger);
1172
1158
 
1173
- let startConnectionP: Promise<IConnectionDetails> | undefined;
1174
-
1175
1159
  // Ideally we always connect as "read" by default.
1176
1160
  // Currently that works with SPO & r11s, because we get "write" connection when connecting to non-existing file.
1177
1161
  // We should not rely on it by (one of them will address the issue, but we need to address both)
@@ -1186,8 +1170,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1186
1170
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1187
1171
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1188
1172
  if (loadMode.deltaConnection === undefined) {
1189
- startConnectionP = this.connectToDeltaStream(connectionArgs);
1190
- startConnectionP.catch((error) => { });
1173
+ this.connectToDeltaStream(connectionArgs);
1191
1174
  }
1192
1175
 
1193
1176
  await this.connectStorageService();
@@ -1539,7 +1522,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1539
1522
  if (this.clientDetailsOverride !== undefined) {
1540
1523
  merge(client.details, this.clientDetailsOverride);
1541
1524
  }
1542
-
1525
+ client.details.environment = [client.details.environment, ` loaderVersion:${pkgVersion}`].join(";");
1543
1526
  return client;
1544
1527
  }
1545
1528
 
@@ -1550,26 +1533,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1550
1533
  * If it's not true, runtime is not in position to send ops.
1551
1534
  */
1552
1535
  private activeConnection() {
1553
- const active = this.connectionState === ConnectionState.Connected &&
1554
- this._deltaManager.connectionMode === "write";
1555
-
1556
- // Check for presence of current client in quorum for "write" connections - inactive clients
1557
- // would get leave op after some long timeout (5 min) and that should automatically transition
1558
- // state to "read" mode.
1559
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1560
- assert(!active || this.getQuorum().getMember(this.clientId!) !== undefined,
1561
- 0x276 /* "active connection not present in quorum" */);
1562
-
1563
- return active;
1536
+ return this.connectionState === ConnectionState.Connected &&
1537
+ this.connectionMode === "write";
1564
1538
  }
1565
1539
 
1566
1540
  private createDeltaManager() {
1567
- const deltaManager: DeltaManager = new DeltaManager(
1568
- () => this.service,
1569
- this.client,
1541
+ const serviceProvider = () => this.service;
1542
+ const deltaManager = new DeltaManager<ConnectionManager>(
1543
+ serviceProvider,
1570
1544
  ChildLogger.create(this.subLogger, "DeltaManager"),
1571
- this._canReconnect,
1572
1545
  () => this.activeConnection(),
1546
+ (props: IConnectionManagerFactoryArgs) => new ConnectionManager(
1547
+ serviceProvider,
1548
+ this.client,
1549
+ this._canReconnect,
1550
+ ChildLogger.create(this.subLogger, "ConnectionManager"),
1551
+ props),
1573
1552
  );
1574
1553
 
1575
1554
  // Disable inbound queues as Container is not ready to accept any ops until we are fully loaded!
@@ -1579,17 +1558,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1579
1558
  deltaManager.inboundSignal.pause();
1580
1559
 
1581
1560
  deltaManager.on("connect", (details: IConnectionDetails, opsBehind?: number) => {
1582
- this.connectionStateHandler.receivedConnectEvent(
1583
- this._deltaManager.connectionMode,
1584
- details,
1585
- );
1586
-
1587
1561
  // Back-compat for new client and old server.
1588
1562
  this._audience.clear();
1589
1563
 
1590
1564
  for (const priorClient of details.initialClients ?? []) {
1591
1565
  this._audience.addMember(priorClient.clientId, priorClient.client);
1592
1566
  }
1567
+
1568
+ this.connectionStateHandler.receivedConnectEvent(
1569
+ this.connectionMode,
1570
+ details,
1571
+ );
1593
1572
  });
1594
1573
 
1595
1574
  deltaManager.on("disconnect", (reason: string) => {
@@ -1646,7 +1625,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1646
1625
  let checkpointSequenceNumber: number | undefined;
1647
1626
  let opsBehind: number | undefined;
1648
1627
  if (value === ConnectionState.Disconnected) {
1649
- autoReconnect = this._deltaManager.reconnectMode;
1628
+ autoReconnect = this._deltaManager.connectionManager.reconnectMode;
1650
1629
  } else {
1651
1630
  if (value === ConnectionState.Connected) {
1652
1631
  durationFromDisconnected = time - this.connectionTransitionTimes[ConnectionState.Disconnected];
@@ -1674,7 +1653,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1674
1653
  durationFromDisconnected,
1675
1654
  reason,
1676
1655
  connectionInitiationReason,
1677
- socketDocumentId: this._deltaManager.socketDocumentId,
1678
1656
  pendingClientId: this.connectionStateHandler.pendingClientId,
1679
1657
  clientId: this.clientId,
1680
1658
  autoReconnect,
@@ -1682,7 +1660,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1682
1660
  online: OnlineStatus[isOnline()],
1683
1661
  lastVisible: this.lastVisible !== undefined ? performance.now() - this.lastVisible : undefined,
1684
1662
  checkpointSequenceNumber,
1685
- ...this._deltaManager.connectionProps(),
1663
+ ...this._deltaManager.connectionProps,
1686
1664
  });
1687
1665
 
1688
1666
  if (value === ConnectionState.Connected) {
@@ -1695,7 +1673,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1695
1673
  const logOpsOnReconnect: boolean =
1696
1674
  this.connectionState === ConnectionState.Connected &&
1697
1675
  !this.firstConnection &&
1698
- this._deltaManager.connectionMode === "write";
1676
+ this.connectionMode === "write";
1699
1677
  if (logOpsOnReconnect) {
1700
1678
  this.messageCountAfterDisconnection = 0;
1701
1679
  }
@@ -191,6 +191,10 @@ export class ContainerContext implements IContainerContext {
191
191
  this.attachListener();
192
192
  }
193
193
 
194
+ public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
195
+ return (this.quorum.get("code") ?? this.quorum.get("code2")) as IFluidCodeDetails | undefined;
196
+ }
197
+
194
198
  public dispose(error?: Error): void {
195
199
  if (this._disposed) {
196
200
  return;