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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/catchUpMonitor.d.ts +1 -1
  3. package/dist/catchUpMonitor.d.ts.map +1 -1
  4. package/dist/catchUpMonitor.js.map +1 -1
  5. package/dist/connectionManager.d.ts +1 -1
  6. package/dist/connectionManager.d.ts.map +1 -1
  7. package/dist/connectionManager.js.map +1 -1
  8. package/dist/connectionStateHandler.d.ts +4 -1
  9. package/dist/connectionStateHandler.d.ts.map +1 -1
  10. package/dist/connectionStateHandler.js +10 -8
  11. package/dist/connectionStateHandler.js.map +1 -1
  12. package/dist/container.d.ts +30 -31
  13. package/dist/container.d.ts.map +1 -1
  14. package/dist/container.js +180 -108
  15. package/dist/container.js.map +1 -1
  16. package/dist/containerContext.d.ts +23 -66
  17. package/dist/containerContext.d.ts.map +1 -1
  18. package/dist/containerContext.js +28 -213
  19. package/dist/containerContext.js.map +1 -1
  20. package/dist/containerStorageAdapter.d.ts +1 -1
  21. package/dist/containerStorageAdapter.d.ts.map +1 -1
  22. package/dist/containerStorageAdapter.js +38 -6
  23. package/dist/containerStorageAdapter.js.map +1 -1
  24. package/dist/contracts.d.ts +1 -3
  25. package/dist/contracts.d.ts.map +1 -1
  26. package/dist/contracts.js.map +1 -1
  27. package/dist/deltaManager.d.ts +2 -1
  28. package/dist/deltaManager.d.ts.map +1 -1
  29. package/dist/deltaManager.js.map +1 -1
  30. package/dist/disposal.d.ts +13 -0
  31. package/dist/disposal.d.ts.map +1 -0
  32. package/dist/disposal.js +25 -0
  33. package/dist/disposal.js.map +1 -0
  34. package/dist/loader.d.ts +1 -2
  35. package/dist/loader.d.ts.map +1 -1
  36. package/dist/loader.js.map +1 -1
  37. package/dist/noopHeuristic.d.ts +23 -0
  38. package/dist/noopHeuristic.d.ts.map +1 -0
  39. package/dist/{collabWindowTracker.js → noopHeuristic.js} +30 -42
  40. package/dist/noopHeuristic.js.map +1 -0
  41. package/dist/packageVersion.d.ts +1 -1
  42. package/dist/packageVersion.js +1 -1
  43. package/dist/packageVersion.js.map +1 -1
  44. package/dist/protocol.d.ts +7 -12
  45. package/dist/protocol.d.ts.map +1 -1
  46. package/dist/protocol.js +17 -19
  47. package/dist/protocol.js.map +1 -1
  48. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  49. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  50. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  51. package/dist/quorum.d.ts +1 -17
  52. package/dist/quorum.d.ts.map +1 -1
  53. package/dist/quorum.js +1 -17
  54. package/dist/quorum.js.map +1 -1
  55. package/dist/retriableDocumentStorageService.d.ts +1 -1
  56. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  57. package/dist/retriableDocumentStorageService.js.map +1 -1
  58. package/lib/catchUpMonitor.d.ts +1 -1
  59. package/lib/catchUpMonitor.d.ts.map +1 -1
  60. package/lib/catchUpMonitor.js.map +1 -1
  61. package/lib/connectionManager.d.ts +1 -1
  62. package/lib/connectionManager.d.ts.map +1 -1
  63. package/lib/connectionManager.js.map +1 -1
  64. package/lib/connectionStateHandler.d.ts +4 -1
  65. package/lib/connectionStateHandler.d.ts.map +1 -1
  66. package/lib/connectionStateHandler.js +10 -8
  67. package/lib/connectionStateHandler.js.map +1 -1
  68. package/lib/container.d.ts +30 -31
  69. package/lib/container.d.ts.map +1 -1
  70. package/lib/container.js +184 -112
  71. package/lib/container.js.map +1 -1
  72. package/lib/containerContext.d.ts +23 -66
  73. package/lib/containerContext.d.ts.map +1 -1
  74. package/lib/containerContext.js +28 -213
  75. package/lib/containerContext.js.map +1 -1
  76. package/lib/containerStorageAdapter.d.ts +1 -1
  77. package/lib/containerStorageAdapter.d.ts.map +1 -1
  78. package/lib/containerStorageAdapter.js +38 -6
  79. package/lib/containerStorageAdapter.js.map +1 -1
  80. package/lib/contracts.d.ts +1 -3
  81. package/lib/contracts.d.ts.map +1 -1
  82. package/lib/contracts.js.map +1 -1
  83. package/lib/deltaManager.d.ts +2 -1
  84. package/lib/deltaManager.d.ts.map +1 -1
  85. package/lib/deltaManager.js.map +1 -1
  86. package/lib/disposal.d.ts +13 -0
  87. package/lib/disposal.d.ts.map +1 -0
  88. package/lib/disposal.js +21 -0
  89. package/lib/disposal.js.map +1 -0
  90. package/lib/loader.d.ts +1 -2
  91. package/lib/loader.d.ts.map +1 -1
  92. package/lib/loader.js.map +1 -1
  93. package/lib/noopHeuristic.d.ts +23 -0
  94. package/lib/noopHeuristic.d.ts.map +1 -0
  95. package/lib/{collabWindowTracker.js → noopHeuristic.js} +30 -42
  96. package/lib/noopHeuristic.js.map +1 -0
  97. package/lib/packageVersion.d.ts +1 -1
  98. package/lib/packageVersion.js +1 -1
  99. package/lib/packageVersion.js.map +1 -1
  100. package/lib/protocol.d.ts +7 -12
  101. package/lib/protocol.d.ts.map +1 -1
  102. package/lib/protocol.js +15 -18
  103. package/lib/protocol.js.map +1 -1
  104. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  105. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  106. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  107. package/lib/quorum.d.ts +1 -17
  108. package/lib/quorum.d.ts.map +1 -1
  109. package/lib/quorum.js +1 -16
  110. package/lib/quorum.js.map +1 -1
  111. package/lib/retriableDocumentStorageService.d.ts +1 -1
  112. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  113. package/lib/retriableDocumentStorageService.js.map +1 -1
  114. package/package.json +18 -14
  115. package/src/catchUpMonitor.ts +1 -1
  116. package/src/connectionManager.ts +1 -1
  117. package/src/connectionStateHandler.ts +15 -10
  118. package/src/container.ts +279 -139
  119. package/src/containerContext.ts +33 -335
  120. package/src/containerStorageAdapter.ts +47 -5
  121. package/src/contracts.ts +1 -3
  122. package/src/deltaManager.ts +15 -8
  123. package/src/disposal.ts +25 -0
  124. package/src/loader.ts +1 -1
  125. package/src/{collabWindowTracker.ts → noopHeuristic.ts} +37 -47
  126. package/src/packageVersion.ts +1 -1
  127. package/src/protocol.ts +18 -39
  128. package/src/protocolTreeDocumentStorageService.ts +1 -1
  129. package/src/quorum.ts +2 -31
  130. package/src/retriableDocumentStorageService.ts +2 -1
  131. package/dist/collabWindowTracker.d.ts +0 -19
  132. package/dist/collabWindowTracker.d.ts.map +0 -1
  133. package/dist/collabWindowTracker.js.map +0 -1
  134. package/dist/deltaManagerProxy.d.ts +0 -42
  135. package/dist/deltaManagerProxy.d.ts.map +0 -1
  136. package/dist/deltaManagerProxy.js +0 -79
  137. package/dist/deltaManagerProxy.js.map +0 -1
  138. package/lib/collabWindowTracker.d.ts +0 -19
  139. package/lib/collabWindowTracker.d.ts.map +0 -1
  140. package/lib/collabWindowTracker.js.map +0 -1
  141. package/lib/deltaManagerProxy.d.ts +0 -42
  142. package/lib/deltaManagerProxy.d.ts.map +0 -1
  143. package/lib/deltaManagerProxy.js +0 -74
  144. package/lib/deltaManagerProxy.js.map +0 -1
  145. package/src/deltaManagerProxy.ts +0 -109
package/src/container.ts CHANGED
@@ -7,9 +7,21 @@
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 { IEvent } from "@fluidframework/common-definitions";
11
+ import {
12
+ TypedEventEmitter,
13
+ assert,
14
+ performance,
15
+ unreachableCase,
16
+ } from "@fluidframework/common-utils";
17
+ import {
18
+ ITelemetryProperties,
19
+ TelemetryEventCategory,
20
+ IRequest,
21
+ IResponse,
22
+ IFluidRouter,
23
+ FluidObject,
24
+ } from "@fluidframework/core-interfaces";
13
25
  import {
14
26
  IAudience,
15
27
  IConnectionDetailsInternal,
@@ -27,6 +39,11 @@ import {
27
39
  IBatchMessage,
28
40
  ICodeDetailsLoader,
29
41
  IHostLoader,
42
+ IFluidModuleWithDetails,
43
+ IProvideRuntimeFactory,
44
+ IProvideFluidCodeDetailsComparer,
45
+ IFluidCodeDetailsComparer,
46
+ IRuntime,
30
47
  } from "@fluidframework/container-definitions";
31
48
  import { GenericError, UsageError } from "@fluidframework/container-utils";
32
49
  import {
@@ -44,11 +61,12 @@ import {
44
61
  combineAppAndProtocolSummary,
45
62
  runWithRetry,
46
63
  isCombinedAppAndProtocolSummary,
64
+ MessageType2,
65
+ canBeCoalescedByService,
47
66
  } from "@fluidframework/driver-utils";
48
67
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
49
68
  import {
50
69
  IClient,
51
- IClientConfiguration,
52
70
  IClientDetails,
53
71
  ICommittedProposal,
54
72
  IDocumentAttributes,
@@ -83,7 +101,6 @@ import { Audience } from "./audience";
83
101
  import { ContainerContext } from "./containerContext";
84
102
  import { ReconnectMode, IConnectionManagerFactoryArgs, getPackageName } 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 {
@@ -94,19 +111,16 @@ import {
94
111
  } from "./containerStorageAdapter";
95
112
  import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
96
113
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
97
- import {
98
- initQuorumValuesFromCodeDetails,
99
- getCodeDetailsFromQuorumValues,
100
- QuorumProxy,
101
- } from "./quorum";
102
- import { CollabWindowTracker } from "./collabWindowTracker";
114
+ import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues } from "./quorum";
115
+ import { NoopHeuristic } from "./noopHeuristic";
103
116
  import { ConnectionManager } from "./connectionManager";
104
117
  import { ConnectionState } from "./connectionState";
105
118
  import {
106
- OnlyValidTermValue,
107
119
  IProtocolHandler,
120
+ OnlyValidTermValue,
108
121
  ProtocolHandler,
109
122
  ProtocolHandlerBuilder,
123
+ protocolHandlerShouldProcessSignal,
110
124
  } from "./protocol";
111
125
 
112
126
  const detachedContainerRefSeqNumber = 0;
@@ -114,6 +128,8 @@ const detachedContainerRefSeqNumber = 0;
114
128
  const dirtyContainerEvent = "dirty";
115
129
  const savedContainerEvent = "saved";
116
130
 
131
+ const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
132
+
117
133
  /**
118
134
  * @internal
119
135
  */
@@ -336,12 +352,15 @@ export interface IPendingContainerState {
336
352
 
337
353
  const summarizerClientType = "summarizer";
338
354
 
355
+ interface IContainerLifecycleEvents extends IEvent {
356
+ (event: "runtimeInstantiated", listener: () => void): void;
357
+ (event: "disposed", listener: () => void): void;
358
+ }
359
+
339
360
  export class Container
340
361
  extends EventEmitterWithErrorHandling<IContainerEvents>
341
362
  implements IContainer, IContainerExperimental
342
363
  {
343
- public static version = "^0.1.0";
344
-
345
364
  /**
346
365
  * Load an existing container.
347
366
  * @internal
@@ -452,9 +471,9 @@ export class Container
452
471
  private readonly urlResolver: IUrlResolver;
453
472
  private readonly serviceFactory: IDocumentServiceFactory;
454
473
  private readonly codeLoader: ICodeDetailsLoader;
455
- public readonly options: ILoaderOptions;
474
+ private readonly options: ILoaderOptions;
456
475
  private readonly scope: FluidObject;
457
- public subLogger: TelemetryLogger;
476
+ private readonly subLogger: TelemetryLogger;
458
477
  private readonly detachedBlobStorage: IDetachedBlobStorage | undefined;
459
478
  private readonly protocolHandlerBuilder: ProtocolHandlerBuilder;
460
479
 
@@ -514,19 +533,16 @@ export class Container
514
533
  private _attachState = AttachState.Detached;
515
534
 
516
535
  private readonly storageAdapter: ContainerStorageAdapter;
517
- public get storage(): IDocumentStorageService {
518
- return this.storageAdapter;
519
- }
520
536
 
521
537
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
522
538
  private service: IDocumentService | undefined;
523
539
 
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");
540
+ private _runtime: IRuntime | undefined;
541
+ private get runtime() {
542
+ if (this._runtime === undefined) {
543
+ throw new Error("Attempted to access runtime before it was defined");
528
544
  }
529
- return this._context;
545
+ return this._runtime;
530
546
  }
531
547
  private _protocolHandler: IProtocolHandler | undefined;
532
548
  private get protocolHandler() {
@@ -551,10 +567,11 @@ export class Container
551
567
  private lastVisible: number | undefined;
552
568
  private readonly visibilityEventHandler: (() => void) | undefined;
553
569
  private readonly connectionStateHandler: IConnectionStateHandler;
570
+ private readonly clientsWhoShouldHaveLeft = new Set<string>();
554
571
 
555
572
  private setAutoReconnectTime = performance.now();
556
573
 
557
- private collabWindowTracker: CollabWindowTracker | undefined;
574
+ private noopHeuristic: NoopHeuristic | undefined;
558
575
 
559
576
  private get connectionMode() {
560
577
  return this._deltaManager.connectionManager.connectionMode;
@@ -579,18 +596,10 @@ export class Container
579
596
  return this.service?.resolvedUrl;
580
597
  }
581
598
 
582
- public get loadedFromVersion(): IVersion | undefined {
583
- return this._loadedFromVersion;
584
- }
585
-
586
599
  public get readOnlyInfo(): ReadOnlyInfo {
587
600
  return this._deltaManager.readOnlyInfo;
588
601
  }
589
602
 
590
- public get closeSignal(): AbortSignal {
591
- return this._deltaManager.closeAbortController.signal;
592
- }
593
-
594
603
  /**
595
604
  * Tracks host requiring read-only mode.
596
605
  */
@@ -606,18 +615,10 @@ export class Container
606
615
  return this.connectionStateHandler.connectionState;
607
616
  }
608
617
 
609
- public get connected(): boolean {
618
+ private get connected(): boolean {
610
619
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
611
620
  }
612
621
 
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
622
  private _clientId: string | undefined;
622
623
 
623
624
  /**
@@ -628,24 +629,12 @@ export class Container
628
629
  return this._clientId;
629
630
  }
630
631
 
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
632
  private get offlineLoadEnabled(): boolean {
644
633
  const enabled =
645
634
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
646
635
  this.options?.enableOfflineLoad === true;
647
636
  // summarizer will not have any pending state we want to save
648
- return enabled && this.clientDetails.capabilities.interactive;
637
+ return enabled && this.deltaManager.clientDetails.capabilities.interactive;
649
638
  }
650
639
 
651
640
  /**
@@ -656,15 +645,18 @@ export class Container
656
645
  return this.getCodeDetailsFromQuorum();
657
646
  }
658
647
 
648
+ private _loadedCodeDetails: IFluidCodeDetails | undefined;
659
649
  /**
660
650
  * Get the code details that were used to load the container.
661
651
  * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
662
652
  * loaded.
663
653
  */
664
654
  public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
665
- return this._context?.codeDetails;
655
+ return this._loadedCodeDetails;
666
656
  }
667
657
 
658
+ private _loadedModule: IFluidModuleWithDetails | undefined;
659
+
668
660
  /**
669
661
  * Retrieves the audience associated with the document
670
662
  */
@@ -684,38 +676,33 @@ export class Container
684
676
  /**
685
677
  * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
686
678
  */
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");
679
+ public async getEntryPoint(): Promise<FluidObject | undefined> {
680
+ if (this._disposed) {
681
+ throw new UsageError("The context is already disposed");
693
682
  }
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
- );
683
+ if (this._runtime !== undefined) {
684
+ return this._runtime.getEntryPoint?.();
713
685
  }
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?.();
686
+ return new Promise<FluidObject | undefined>((resolve, reject) => {
687
+ const runtimeInstantiatedHandler = () => {
688
+ assert(
689
+ this._runtime !== undefined,
690
+ 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */,
691
+ );
692
+ resolve(this._runtime.getEntryPoint?.());
693
+ this._lifecycleEvents.off("disposed", disposedHandler);
694
+ };
695
+ const disposedHandler = () => {
696
+ reject(new Error("ContainerContext was disposed"));
697
+ this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
698
+ };
699
+ this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
700
+ this._lifecycleEvents.once("disposed", disposedHandler);
701
+ });
717
702
  }
718
703
 
704
+ private readonly _lifecycleEvents = new TypedEventEmitter<IContainerLifecycleEvents>();
705
+
719
706
  /**
720
707
  * @internal
721
708
  */
@@ -800,13 +787,16 @@ export class Container
800
787
  dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
801
788
  dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
802
789
  dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
803
- containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
804
- containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
790
+ containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
791
+ containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
805
792
  // message information to associate errors with the specific execution state
806
793
  // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
807
794
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
808
795
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
809
- dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
796
+ dmLastMsqSeqClientId: () =>
797
+ this.deltaManager?.lastMessage?.clientId === null
798
+ ? "null"
799
+ : this.deltaManager?.lastMessage?.clientId,
810
800
  dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
811
801
  connectionStateDuration: () =>
812
802
  performance.now() - this.connectionTransitionTimes[this.connectionState],
@@ -871,6 +861,9 @@ export class Container
871
861
  this.connect();
872
862
  }
873
863
  },
864
+ clientShouldHaveLeft: (clientId: string) => {
865
+ this.clientsWhoShouldHaveLeft.add(clientId);
866
+ },
874
867
  },
875
868
  this.deltaManager,
876
869
  pendingLocalState?.clientId,
@@ -994,6 +987,11 @@ export class Container
994
987
  }
995
988
  } finally {
996
989
  this._lifecycleState = "closed";
990
+
991
+ // There is no user for summarizer, so we need to ensure dispose is called
992
+ if (this.client.details.type === summarizerClientType) {
993
+ this.dispose(error);
994
+ }
997
995
  }
998
996
  }
999
997
 
@@ -1010,7 +1008,8 @@ export class Container
1010
1008
  this.mc.logger.sendTelemetryEvent(
1011
1009
  {
1012
1010
  eventName: "ContainerDispose",
1013
- category: "generic",
1011
+ // Only log error if container isn't closed
1012
+ category: !this.closed && error !== undefined ? "error" : "generic",
1014
1013
  },
1015
1014
  error,
1016
1015
  );
@@ -1024,7 +1023,8 @@ export class Container
1024
1023
 
1025
1024
  this.connectionStateHandler.dispose();
1026
1025
 
1027
- this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
1026
+ const maybeError = error !== undefined ? new Error(error.message) : undefined;
1027
+ this._runtime?.dispose(maybeError);
1028
1028
 
1029
1029
  this.storageAdapter.dispose();
1030
1030
 
@@ -1047,6 +1047,7 @@ export class Container
1047
1047
  }
1048
1048
  } finally {
1049
1049
  this._lifecycleState = "disposed";
1050
+ this._lifecycleEvents.emit("disposed");
1050
1051
  }
1051
1052
  }
1052
1053
 
@@ -1063,6 +1064,11 @@ export class Container
1063
1064
  if (!this.offlineLoadEnabled) {
1064
1065
  throw new UsageError("Can't get pending local state unless offline load is enabled");
1065
1066
  }
1067
+ if (this.closed || this._disposed) {
1068
+ throw new UsageError(
1069
+ "Pending state cannot be retried if the container is closed or disposed",
1070
+ );
1071
+ }
1066
1072
  assert(
1067
1073
  this.attachState === AttachState.Attached,
1068
1074
  0x0d1 /* "Container should be attached before close" */,
@@ -1074,7 +1080,7 @@ export class Container
1074
1080
  assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1075
1081
  assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1076
1082
  const pendingState: IPendingContainerState = {
1077
- pendingRuntimeState: this.context.getPendingLocalState(),
1083
+ pendingRuntimeState: this.runtime.getPendingLocalState(),
1078
1084
  baseSnapshot: this.baseSnapshot,
1079
1085
  snapshotBlobs: this.baseSnapshotBlobs,
1080
1086
  savedOps: this.savedOps,
@@ -1098,7 +1104,7 @@ export class Container
1098
1104
  0x0d3 /* "Should only be called in detached container" */,
1099
1105
  );
1100
1106
 
1101
- const appSummary: ISummaryTree = this.context.createSummary();
1107
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1102
1108
  const protocolSummary = this.captureProtocolSummary();
1103
1109
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1104
1110
 
@@ -1144,7 +1150,7 @@ export class Container
1144
1150
  if (!hasAttachmentBlobs) {
1145
1151
  // Get the document state post attach - possibly can just call attach but we need to change the
1146
1152
  // semantics around what the attach means as far as async code goes.
1147
- const appSummary: ISummaryTree = this.context.createSummary();
1153
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1148
1154
  const protocolSummary = this.captureProtocolSummary();
1149
1155
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1150
1156
 
@@ -1153,6 +1159,7 @@ export class Container
1153
1159
  // starting to attach the container to storage.
1154
1160
  // Also, this should only be fired in detached container.
1155
1161
  this._attachState = AttachState.Attaching;
1162
+ this.runtime.setAttachState(AttachState.Attaching);
1156
1163
  this.emit("attaching");
1157
1164
  if (this.offlineLoadEnabled) {
1158
1165
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1181,7 +1188,7 @@ export class Container
1181
1188
  "containerAttach",
1182
1189
  this.mc.logger,
1183
1190
  {
1184
- cancel: this.closeSignal,
1191
+ cancel: this._deltaManager.closeAbortController.signal,
1185
1192
  }, // progress
1186
1193
  );
1187
1194
  }
@@ -1210,11 +1217,12 @@ export class Container
1210
1217
  }
1211
1218
 
1212
1219
  // take summary and upload
1213
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
1220
+ const appSummary: ISummaryTree = this.runtime.createSummary(redirectTable);
1214
1221
  const protocolSummary = this.captureProtocolSummary();
1215
1222
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1216
1223
 
1217
1224
  this._attachState = AttachState.Attaching;
1225
+ this.runtime.setAttachState(AttachState.Attaching);
1218
1226
  this.emit("attaching");
1219
1227
  if (this.offlineLoadEnabled) {
1220
1228
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1231,6 +1239,7 @@ export class Container
1231
1239
  }
1232
1240
 
1233
1241
  this._attachState = AttachState.Attached;
1242
+ this.runtime.setAttachState(AttachState.Attached);
1234
1243
  this.emit("attached");
1235
1244
 
1236
1245
  if (!this.closed) {
@@ -1255,7 +1264,7 @@ export class Container
1255
1264
  return PerformanceEvent.timedExecAsync(
1256
1265
  this.mc.logger,
1257
1266
  { eventName: "Request" },
1258
- async () => this.context.request(path),
1267
+ async () => this.runtime.request(path),
1259
1268
  { end: true, cancel: "error" },
1260
1269
  );
1261
1270
  }
@@ -1340,7 +1349,7 @@ export class Container
1340
1349
  this.connectToDeltaStream(args);
1341
1350
  }
1342
1351
 
1343
- public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1352
+ public readonly getAbsoluteUrl = async (relativeUrl: string): Promise<string | undefined> => {
1344
1353
  if (this.resolvedUrl === undefined) {
1345
1354
  return undefined;
1346
1355
  }
@@ -1348,9 +1357,9 @@ export class Container
1348
1357
  return this.urlResolver.getAbsoluteUrl(
1349
1358
  this.resolvedUrl,
1350
1359
  relativeUrl,
1351
- getPackageName(this._context?.codeDetails),
1360
+ getPackageName(this._loadedCodeDetails),
1352
1361
  );
1353
- }
1362
+ };
1354
1363
 
1355
1364
  public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1356
1365
  if (!isFluidCodeDetails(codeDetails)) {
@@ -1381,7 +1390,7 @@ export class Container
1381
1390
  this.deltaManager.inboundSignal.pause(),
1382
1391
  ]);
1383
1392
 
1384
- if ((await this.context.satisfies(codeDetails)) === true) {
1393
+ if ((await this.satisfies(codeDetails)) === true) {
1385
1394
  this.deltaManager.inbound.resume();
1386
1395
  this.deltaManager.inboundSignal.resume();
1387
1396
  return;
@@ -1392,6 +1401,47 @@ export class Container
1392
1401
  this.close(error);
1393
1402
  }
1394
1403
 
1404
+ /**
1405
+ * Determines if the currently loaded module satisfies the incoming constraint code details
1406
+ */
1407
+ private async satisfies(constraintCodeDetails: IFluidCodeDetails) {
1408
+ // If we have no module, it can't satisfy anything.
1409
+ if (this._loadedModule === undefined) {
1410
+ return false;
1411
+ }
1412
+
1413
+ const comparers: IFluidCodeDetailsComparer[] = [];
1414
+
1415
+ const maybeCompareCodeLoader = this.codeLoader;
1416
+ if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
1417
+ comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
1418
+ }
1419
+
1420
+ const maybeCompareExport: Partial<IProvideFluidCodeDetailsComparer> | undefined =
1421
+ this._loadedModule?.module.fluidExport;
1422
+ if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
1423
+ comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
1424
+ }
1425
+
1426
+ // If there are no comparers, then it's impossible to know if the currently loaded package satisfies
1427
+ // the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
1428
+ // rather than potentially running with incompatible code.
1429
+ if (comparers.length === 0) {
1430
+ return false;
1431
+ }
1432
+
1433
+ for (const comparer of comparers) {
1434
+ const satisfies = await comparer.satisfies(
1435
+ this._loadedModule?.details,
1436
+ constraintCodeDetails,
1437
+ );
1438
+ if (satisfies === false) {
1439
+ return false;
1440
+ }
1441
+ }
1442
+ return true;
1443
+ }
1444
+
1395
1445
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
1396
1446
  const versions = await this.storageAdapter.getVersions(version, 1);
1397
1447
  return versions[0];
@@ -1469,7 +1519,10 @@ export class Container
1469
1519
  if (this.offlineLoadEnabled) {
1470
1520
  this.baseSnapshot = snapshot;
1471
1521
  // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1472
- this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1522
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(
1523
+ snapshot,
1524
+ this.storageAdapter,
1525
+ );
1473
1526
  }
1474
1527
  }
1475
1528
 
@@ -1525,7 +1578,7 @@ export class Container
1525
1578
  this.processRemoteMessage(message);
1526
1579
 
1527
1580
  // allow runtime to apply stashed ops at this op's sequence number
1528
- await this.context.notifyOpReplay(message);
1581
+ await this.runtime.notifyOpReplay?.(message);
1529
1582
  }
1530
1583
  pendingLocalState.savedOps = [];
1531
1584
 
@@ -1877,7 +1930,7 @@ export class Container
1877
1930
  });
1878
1931
 
1879
1932
  deltaManager.on("disconnect", (reason: string, error?: IAnyDriverError) => {
1880
- this.collabWindowTracker?.stopSequenceNumberUpdate();
1933
+ this.noopHeuristic?.notifyDisconnect();
1881
1934
  if (!this.closed) {
1882
1935
  this.connectionStateHandler.receivedDisconnectEvent(reason, error);
1883
1936
  }
@@ -1955,7 +2008,11 @@ export class Container
1955
2008
  } else if (value === ConnectionState.CatchingUp) {
1956
2009
  // This info is of most interesting while Catching Up.
1957
2010
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1958
- if (this.deltaManager.hasCheckpointSequenceNumber) {
2011
+ // Need to check that we have already loaded and fetched the snapshot.
2012
+ if (
2013
+ this.deltaManager.hasCheckpointSequenceNumber &&
2014
+ this._lifecycleState === "loaded"
2015
+ ) {
1959
2016
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1960
2017
  }
1961
2018
  }
@@ -2100,7 +2157,7 @@ export class Container
2100
2157
  }
2101
2158
 
2102
2159
  this.messageCountAfterDisconnection += 1;
2103
- this.collabWindowTracker?.stopSequenceNumberUpdate();
2160
+ this.noopHeuristic?.notifyMessageSent();
2104
2161
  return this._deltaManager.submit(
2105
2162
  type,
2106
2163
  contents,
@@ -2117,39 +2174,67 @@ export class Container
2117
2174
  }
2118
2175
  const local = this.clientId === message.clientId;
2119
2176
 
2177
+ // Check and report if we're getting messages from a clientId that we previously
2178
+ // flagged should have left, or from a client that's not in the quorum but should be
2179
+ if (message.clientId != null) {
2180
+ const client = this.protocolHandler.quorum.getMember(message.clientId);
2181
+
2182
+ if (client === undefined && message.type !== MessageType.ClientJoin) {
2183
+ // pre-0.58 error message: messageClientIdMissingFromQuorum
2184
+ throw new Error("Remote message's clientId is missing from the quorum");
2185
+ }
2186
+
2187
+ // Here checking canBeCoalescedByService is used as an approximation of "is benign to process despite being unexpected".
2188
+ // It's still not good to see these messages from unexpected clientIds, but since they don't harm the integrity of the
2189
+ // document we don't need to blow up aggressively.
2190
+ if (
2191
+ this.clientsWhoShouldHaveLeft.has(message.clientId) &&
2192
+ !canBeCoalescedByService(message)
2193
+ ) {
2194
+ // pre-0.58 error message: messageClientIdShouldHaveLeft
2195
+ throw new Error("Remote message's clientId already should have left");
2196
+ }
2197
+ }
2198
+
2120
2199
  // Allow the protocol handler to process the message
2121
2200
  const result = this.protocolHandler.processMessage(message, local);
2122
2201
 
2123
2202
  // Forward messages to the loaded runtime for processing
2124
- this.context.process(message, local);
2203
+ this.runtime.process(message, local);
2125
2204
 
2126
2205
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
2127
2206
  if (this.activeConnection()) {
2128
- if (this.collabWindowTracker === undefined) {
2207
+ if (this.noopHeuristic === undefined) {
2208
+ const serviceConfiguration = this.deltaManager.serviceConfiguration;
2129
2209
  // Note that config from first connection will be used for this container's lifetime.
2130
2210
  // That means that if relay service changes settings, such changes will impact only newly booted
2131
2211
  // clients.
2132
2212
  // All existing will continue to use settings they got earlier.
2133
2213
  assert(
2134
- this.serviceConfiguration !== undefined,
2214
+ serviceConfiguration !== undefined,
2135
2215
  0x2e4 /* "there should be service config for active connection" */,
2136
2216
  );
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,
2217
+ this.noopHeuristic = new NoopHeuristic(
2218
+ serviceConfiguration.noopTimeFrequency,
2219
+ serviceConfiguration.noopCountFrequency,
2147
2220
  );
2221
+ this.noopHeuristic.on("wantsNoop", () => {
2222
+ // On disconnect we notify the heuristic which should prevent it from wanting a noop.
2223
+ // Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
2224
+ // running the microtask that the heuristic queued in response.
2225
+ assert(
2226
+ this.activeConnection(),
2227
+ 0x241 /* "Trying to send noop without active connection" */,
2228
+ );
2229
+ this.submitMessage(MessageType.NoOp);
2230
+ });
2231
+ }
2232
+ this.noopHeuristic.notifyMessageProcessed(message);
2233
+ // The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
2234
+ if (result.immediateNoOp === true) {
2235
+ // ADO:1385: Remove cast and use MessageType once definition changes propagate
2236
+ this.submitMessage(MessageType2.Accept as unknown as MessageType);
2148
2237
  }
2149
- this.collabWindowTracker.scheduleSequenceNumberUpdate(
2150
- message,
2151
- result.immediateNoOp === true,
2152
- );
2153
2238
  }
2154
2239
 
2155
2240
  this.emit("op", message);
@@ -2161,11 +2246,11 @@ export class Container
2161
2246
 
2162
2247
  private processSignal(message: ISignalMessage) {
2163
2248
  // No clientId indicates a system signal message.
2164
- if (message.clientId === null) {
2249
+ if (protocolHandlerShouldProcessSignal(message)) {
2165
2250
  this.protocolHandler.processSignal(message);
2166
2251
  } else {
2167
2252
  const local = this.clientId === message.clientId;
2168
- this.context.processSignal(message, local);
2253
+ this.runtime.processSignal(message, local);
2169
2254
  }
2170
2255
  }
2171
2256
 
@@ -2207,23 +2292,50 @@ export class Container
2207
2292
  private async instantiateContext(
2208
2293
  existing: boolean,
2209
2294
  codeDetails: IFluidCodeDetails,
2210
- snapshot?: ISnapshotTree,
2295
+ snapshot: ISnapshotTree | undefined,
2211
2296
  pendingLocalState?: unknown,
2212
2297
  ) {
2213
- assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
2298
+ assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
2214
2299
 
2215
2300
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
2216
2301
  // are set. Global requests will still go directly to the loader
2217
2302
  const maybeLoader: FluidObject<IHostLoader> = this.scope;
2218
2303
  const loader = new RelativeLoader(this, maybeLoader.ILoader);
2219
- this._context = await ContainerContext.createOrLoad(
2220
- this,
2304
+
2305
+ const loadCodeResult = await PerformanceEvent.timedExecAsync(
2306
+ this.subLogger,
2307
+ { eventName: "CodeLoad" },
2308
+ async () => this.codeLoader.load(codeDetails),
2309
+ );
2310
+
2311
+ this._loadedModule = {
2312
+ module: loadCodeResult.module,
2313
+ // An older interface ICodeLoader could return an IFluidModule which didn't have details.
2314
+ // If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
2315
+ // TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
2316
+ details: loadCodeResult.details ?? codeDetails,
2317
+ };
2318
+
2319
+ const fluidExport: FluidObject<IProvideRuntimeFactory> | undefined =
2320
+ this._loadedModule.module.fluidExport;
2321
+ const runtimeFactory = fluidExport?.IRuntimeFactory;
2322
+ if (runtimeFactory === undefined) {
2323
+ throw new Error(packageNotFactoryError);
2324
+ }
2325
+
2326
+ const getSpecifiedCodeDetails = () =>
2327
+ (this.protocolHandler.quorum.get("code") ??
2328
+ this.protocolHandler.quorum.get("code2")) as IFluidCodeDetails | undefined;
2329
+
2330
+ const context = new ContainerContext(
2331
+ this.options,
2221
2332
  this.scope,
2222
- this.codeLoader,
2223
- codeDetails,
2224
2333
  snapshot,
2225
- new DeltaManagerProxy(this._deltaManager),
2226
- new QuorumProxy(this.protocolHandler.quorum),
2334
+ this._loadedFromVersion,
2335
+ this._deltaManager,
2336
+ this.storageAdapter,
2337
+ this.protocolHandler.quorum,
2338
+ this.protocolHandler.audience,
2227
2339
  loader,
2228
2340
  (type, contents, batch, metadata) =>
2229
2341
  this.submitContainerMessage(type, contents, batch, metadata),
@@ -2234,22 +2346,42 @@ export class Container
2234
2346
  (message) => this.submitSignal(message),
2235
2347
  (error?: ICriticalContainerError) => this.dispose(error),
2236
2348
  (error?: ICriticalContainerError) => this.close(error),
2237
- Container.version,
2238
- (dirty: boolean) => this.updateDirtyContainerState(dirty),
2349
+ this.updateDirtyContainerState,
2350
+ this.getAbsoluteUrl,
2351
+ () => this.resolvedUrl?.id,
2352
+ () => this.clientId,
2353
+ () => this._deltaManager.serviceConfiguration,
2354
+ () => this.attachState,
2355
+ () => this.connected,
2356
+ getSpecifiedCodeDetails,
2357
+ this._deltaManager.clientDetails,
2239
2358
  existing,
2359
+ this.subLogger,
2240
2360
  pendingLocalState,
2241
2361
  );
2362
+ this._lifecycleEvents.once("disposed", () => {
2363
+ context.dispose();
2364
+ });
2365
+
2366
+ this._runtime = await PerformanceEvent.timedExecAsync(
2367
+ this.subLogger,
2368
+ { eventName: "InstantiateRuntime" },
2369
+ async () => runtimeFactory.instantiateRuntime(context, existing),
2370
+ );
2371
+ this._lifecycleEvents.emit("runtimeInstantiated");
2372
+
2373
+ this._loadedCodeDetails = codeDetails;
2242
2374
 
2243
2375
  this.emit("contextChanged", codeDetails);
2244
2376
  }
2245
2377
 
2246
- private updateDirtyContainerState(dirty: boolean) {
2378
+ private readonly updateDirtyContainerState = (dirty: boolean) => {
2247
2379
  if (this._dirtyContainer === dirty) {
2248
2380
  return;
2249
2381
  }
2250
2382
  this._dirtyContainer = dirty;
2251
2383
  this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
2252
- }
2384
+ };
2253
2385
 
2254
2386
  /**
2255
2387
  * Set the connected state of the ContainerContext
@@ -2258,21 +2390,21 @@ export class Container
2258
2390
  * @param readonly - Is the container in readonly mode?
2259
2391
  */
2260
2392
  private setContextConnectedState(state: boolean, readonly: boolean): void {
2261
- if (this._context?.disposed === false) {
2393
+ if (this._runtime?.disposed === false) {
2262
2394
  /**
2263
2395
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2264
2396
  * ops getting through to the DeltaManager.
2265
2397
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2266
2398
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2267
2399
  */
2268
- this.context.setConnectionState(state && !readonly, this.clientId);
2400
+ this.runtime.setConnectionState(state && !readonly, this.clientId);
2269
2401
  }
2270
2402
  }
2271
2403
  }
2272
2404
 
2273
2405
  /**
2274
2406
  * IContainer interface that includes experimental features still under development.
2275
- * @internal
2407
+ * @experimental
2276
2408
  */
2277
2409
  export interface IContainerExperimental extends IContainer {
2278
2410
  /**
@@ -2283,5 +2415,13 @@ export interface IContainerExperimental extends IContainer {
2283
2415
  * @experimental misuse of this API can result in duplicate op submission and potential document corruption
2284
2416
  * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2285
2417
  */
2286
- getPendingLocalState(): string;
2418
+ getPendingLocalState?(): string;
2419
+
2420
+ /**
2421
+ * Closes the container and returns serialized local state intended to be
2422
+ * given to a newly loaded container.
2423
+ * @experimental
2424
+ * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2425
+ */
2426
+ closeAndGetPendingLocalState(): string;
2287
2427
  }