@fluidframework/container-loader 2.0.0-internal.5.1.1 → 2.0.0-internal.5.3.0

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 +27 -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 +182 -109
  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 +186 -113
  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 +284 -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
@@ -354,6 +373,10 @@ export class Container
354
373
 
355
374
  const container = new Container(createProps, loadProps);
356
375
 
376
+ const disableRecordHeapSize = container.mc.config.getBoolean(
377
+ "Fluid.Loader.DisableRecordHeapSize",
378
+ );
379
+
357
380
  return PerformanceEvent.timedExecAsync(
358
381
  container.mc.logger,
359
382
  { eventName: "Load" },
@@ -395,6 +418,7 @@ export class Container
395
418
  );
396
419
  }),
397
420
  { start: true, end: true, cancel: "generic" },
421
+ disableRecordHeapSize !== true /* recordHeapSize */,
398
422
  );
399
423
  }
400
424
 
@@ -447,9 +471,9 @@ export class Container
447
471
  private readonly urlResolver: IUrlResolver;
448
472
  private readonly serviceFactory: IDocumentServiceFactory;
449
473
  private readonly codeLoader: ICodeDetailsLoader;
450
- public readonly options: ILoaderOptions;
474
+ private readonly options: ILoaderOptions;
451
475
  private readonly scope: FluidObject;
452
- public subLogger: TelemetryLogger;
476
+ private readonly subLogger: TelemetryLogger;
453
477
  private readonly detachedBlobStorage: IDetachedBlobStorage | undefined;
454
478
  private readonly protocolHandlerBuilder: ProtocolHandlerBuilder;
455
479
 
@@ -509,19 +533,16 @@ export class Container
509
533
  private _attachState = AttachState.Detached;
510
534
 
511
535
  private readonly storageAdapter: ContainerStorageAdapter;
512
- public get storage(): IDocumentStorageService {
513
- return this.storageAdapter;
514
- }
515
536
 
516
537
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
517
538
  private service: IDocumentService | undefined;
518
539
 
519
- private _context: ContainerContext | undefined;
520
- private get context() {
521
- if (this._context === undefined) {
522
- 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");
523
544
  }
524
- return this._context;
545
+ return this._runtime;
525
546
  }
526
547
  private _protocolHandler: IProtocolHandler | undefined;
527
548
  private get protocolHandler() {
@@ -546,10 +567,11 @@ export class Container
546
567
  private lastVisible: number | undefined;
547
568
  private readonly visibilityEventHandler: (() => void) | undefined;
548
569
  private readonly connectionStateHandler: IConnectionStateHandler;
570
+ private readonly clientsWhoShouldHaveLeft = new Set<string>();
549
571
 
550
572
  private setAutoReconnectTime = performance.now();
551
573
 
552
- private collabWindowTracker: CollabWindowTracker | undefined;
574
+ private noopHeuristic: NoopHeuristic | undefined;
553
575
 
554
576
  private get connectionMode() {
555
577
  return this._deltaManager.connectionManager.connectionMode;
@@ -574,18 +596,10 @@ export class Container
574
596
  return this.service?.resolvedUrl;
575
597
  }
576
598
 
577
- public get loadedFromVersion(): IVersion | undefined {
578
- return this._loadedFromVersion;
579
- }
580
-
581
599
  public get readOnlyInfo(): ReadOnlyInfo {
582
600
  return this._deltaManager.readOnlyInfo;
583
601
  }
584
602
 
585
- public get closeSignal(): AbortSignal {
586
- return this._deltaManager.closeAbortController.signal;
587
- }
588
-
589
603
  /**
590
604
  * Tracks host requiring read-only mode.
591
605
  */
@@ -601,18 +615,10 @@ export class Container
601
615
  return this.connectionStateHandler.connectionState;
602
616
  }
603
617
 
604
- public get connected(): boolean {
618
+ private get connected(): boolean {
605
619
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
606
620
  }
607
621
 
608
- /**
609
- * Service configuration details. If running in offline mode will be undefined otherwise will contain service
610
- * configuration details returned as part of the initial connection.
611
- */
612
- public get serviceConfiguration(): IClientConfiguration | undefined {
613
- return this._deltaManager.serviceConfiguration;
614
- }
615
-
616
622
  private _clientId: string | undefined;
617
623
 
618
624
  /**
@@ -623,24 +629,12 @@ export class Container
623
629
  return this._clientId;
624
630
  }
625
631
 
626
- /**
627
- * The server provided claims of the client.
628
- * Set once this.connected is true, otherwise undefined
629
- */
630
- public get scopes(): string[] | undefined {
631
- return this._deltaManager.connectionManager.scopes;
632
- }
633
-
634
- public get clientDetails(): IClientDetails {
635
- return this._deltaManager.clientDetails;
636
- }
637
-
638
632
  private get offlineLoadEnabled(): boolean {
639
633
  const enabled =
640
634
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
641
635
  this.options?.enableOfflineLoad === true;
642
636
  // summarizer will not have any pending state we want to save
643
- return enabled && this.clientDetails.capabilities.interactive;
637
+ return enabled && this.deltaManager.clientDetails.capabilities.interactive;
644
638
  }
645
639
 
646
640
  /**
@@ -651,15 +645,18 @@ export class Container
651
645
  return this.getCodeDetailsFromQuorum();
652
646
  }
653
647
 
648
+ private _loadedCodeDetails: IFluidCodeDetails | undefined;
654
649
  /**
655
650
  * Get the code details that were used to load the container.
656
651
  * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
657
652
  * loaded.
658
653
  */
659
654
  public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
660
- return this._context?.codeDetails;
655
+ return this._loadedCodeDetails;
661
656
  }
662
657
 
658
+ private _loadedModule: IFluidModuleWithDetails | undefined;
659
+
663
660
  /**
664
661
  * Retrieves the audience associated with the document
665
662
  */
@@ -679,38 +676,33 @@ export class Container
679
676
  /**
680
677
  * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
681
678
  */
682
- public async getEntryPoint?(): Promise<FluidObject | undefined> {
683
- // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
684
- // allow it since they mean a kind of read-only state for the Container.
685
- // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
686
- if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
687
- 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");
688
682
  }
689
- while (this._context === undefined) {
690
- await new Promise<void>((resolve, reject) => {
691
- const contextChangedHandler = () => {
692
- resolve();
693
- this.off("disposed", disposedHandler);
694
- };
695
- const disposedHandler = (error) => {
696
- reject(error ?? "The Container is disposed");
697
- this.off("contextChanged", contextChangedHandler);
698
- };
699
- this.once("contextChanged", contextChangedHandler);
700
- this.once("disposed", disposedHandler);
701
- });
702
- // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
703
- // should have set this._context; making sure.
704
- assert(
705
- this._context !== undefined,
706
- 0x5a2 /* Context still not defined after contextChanged event */,
707
- );
683
+ if (this._runtime !== undefined) {
684
+ return this._runtime.getEntryPoint?.();
708
685
  }
709
- // Disable lint rule for the sake of more complete stack traces
710
- // eslint-disable-next-line no-return-await
711
- 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
+ });
712
702
  }
713
703
 
704
+ private readonly _lifecycleEvents = new TypedEventEmitter<IContainerLifecycleEvents>();
705
+
714
706
  /**
715
707
  * @internal
716
708
  */
@@ -795,13 +787,16 @@ export class Container
795
787
  dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
796
788
  dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
797
789
  dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
798
- containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
799
- containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
790
+ containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
791
+ containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
800
792
  // message information to associate errors with the specific execution state
801
793
  // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
802
794
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
803
795
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
804
- dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
796
+ dmLastMsqSeqClientId: () =>
797
+ this.deltaManager?.lastMessage?.clientId === null
798
+ ? "null"
799
+ : this.deltaManager?.lastMessage?.clientId,
805
800
  dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
806
801
  connectionStateDuration: () =>
807
802
  performance.now() - this.connectionTransitionTimes[this.connectionState],
@@ -866,6 +861,9 @@ export class Container
866
861
  this.connect();
867
862
  }
868
863
  },
864
+ clientShouldHaveLeft: (clientId: string) => {
865
+ this.clientsWhoShouldHaveLeft.add(clientId);
866
+ },
869
867
  },
870
868
  this.deltaManager,
871
869
  pendingLocalState?.clientId,
@@ -989,6 +987,11 @@ export class Container
989
987
  }
990
988
  } finally {
991
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
+ }
992
995
  }
993
996
  }
994
997
 
@@ -1005,7 +1008,8 @@ export class Container
1005
1008
  this.mc.logger.sendTelemetryEvent(
1006
1009
  {
1007
1010
  eventName: "ContainerDispose",
1008
- category: "generic",
1011
+ // Only log error if container isn't closed
1012
+ category: !this.closed && error !== undefined ? "error" : "generic",
1009
1013
  },
1010
1014
  error,
1011
1015
  );
@@ -1019,7 +1023,8 @@ export class Container
1019
1023
 
1020
1024
  this.connectionStateHandler.dispose();
1021
1025
 
1022
- 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);
1023
1028
 
1024
1029
  this.storageAdapter.dispose();
1025
1030
 
@@ -1042,6 +1047,7 @@ export class Container
1042
1047
  }
1043
1048
  } finally {
1044
1049
  this._lifecycleState = "disposed";
1050
+ this._lifecycleEvents.emit("disposed");
1045
1051
  }
1046
1052
  }
1047
1053
 
@@ -1058,6 +1064,11 @@ export class Container
1058
1064
  if (!this.offlineLoadEnabled) {
1059
1065
  throw new UsageError("Can't get pending local state unless offline load is enabled");
1060
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
+ }
1061
1072
  assert(
1062
1073
  this.attachState === AttachState.Attached,
1063
1074
  0x0d1 /* "Container should be attached before close" */,
@@ -1069,7 +1080,7 @@ export class Container
1069
1080
  assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1070
1081
  assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1071
1082
  const pendingState: IPendingContainerState = {
1072
- pendingRuntimeState: this.context.getPendingLocalState(),
1083
+ pendingRuntimeState: this.runtime.getPendingLocalState(),
1073
1084
  baseSnapshot: this.baseSnapshot,
1074
1085
  snapshotBlobs: this.baseSnapshotBlobs,
1075
1086
  savedOps: this.savedOps,
@@ -1093,7 +1104,7 @@ export class Container
1093
1104
  0x0d3 /* "Should only be called in detached container" */,
1094
1105
  );
1095
1106
 
1096
- const appSummary: ISummaryTree = this.context.createSummary();
1107
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1097
1108
  const protocolSummary = this.captureProtocolSummary();
1098
1109
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1099
1110
 
@@ -1139,7 +1150,7 @@ export class Container
1139
1150
  if (!hasAttachmentBlobs) {
1140
1151
  // Get the document state post attach - possibly can just call attach but we need to change the
1141
1152
  // semantics around what the attach means as far as async code goes.
1142
- const appSummary: ISummaryTree = this.context.createSummary();
1153
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1143
1154
  const protocolSummary = this.captureProtocolSummary();
1144
1155
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1145
1156
 
@@ -1148,6 +1159,7 @@ export class Container
1148
1159
  // starting to attach the container to storage.
1149
1160
  // Also, this should only be fired in detached container.
1150
1161
  this._attachState = AttachState.Attaching;
1162
+ this.runtime.setAttachState(AttachState.Attaching);
1151
1163
  this.emit("attaching");
1152
1164
  if (this.offlineLoadEnabled) {
1153
1165
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1176,7 +1188,7 @@ export class Container
1176
1188
  "containerAttach",
1177
1189
  this.mc.logger,
1178
1190
  {
1179
- cancel: this.closeSignal,
1191
+ cancel: this._deltaManager.closeAbortController.signal,
1180
1192
  }, // progress
1181
1193
  );
1182
1194
  }
@@ -1205,11 +1217,12 @@ export class Container
1205
1217
  }
1206
1218
 
1207
1219
  // take summary and upload
1208
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
1220
+ const appSummary: ISummaryTree = this.runtime.createSummary(redirectTable);
1209
1221
  const protocolSummary = this.captureProtocolSummary();
1210
1222
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1211
1223
 
1212
1224
  this._attachState = AttachState.Attaching;
1225
+ this.runtime.setAttachState(AttachState.Attaching);
1213
1226
  this.emit("attaching");
1214
1227
  if (this.offlineLoadEnabled) {
1215
1228
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1226,6 +1239,7 @@ export class Container
1226
1239
  }
1227
1240
 
1228
1241
  this._attachState = AttachState.Attached;
1242
+ this.runtime.setAttachState(AttachState.Attached);
1229
1243
  this.emit("attached");
1230
1244
 
1231
1245
  if (!this.closed) {
@@ -1250,7 +1264,7 @@ export class Container
1250
1264
  return PerformanceEvent.timedExecAsync(
1251
1265
  this.mc.logger,
1252
1266
  { eventName: "Request" },
1253
- async () => this.context.request(path),
1267
+ async () => this.runtime.request(path),
1254
1268
  { end: true, cancel: "error" },
1255
1269
  );
1256
1270
  }
@@ -1335,7 +1349,7 @@ export class Container
1335
1349
  this.connectToDeltaStream(args);
1336
1350
  }
1337
1351
 
1338
- public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1352
+ public readonly getAbsoluteUrl = async (relativeUrl: string): Promise<string | undefined> => {
1339
1353
  if (this.resolvedUrl === undefined) {
1340
1354
  return undefined;
1341
1355
  }
@@ -1343,9 +1357,9 @@ export class Container
1343
1357
  return this.urlResolver.getAbsoluteUrl(
1344
1358
  this.resolvedUrl,
1345
1359
  relativeUrl,
1346
- getPackageName(this._context?.codeDetails),
1360
+ getPackageName(this._loadedCodeDetails),
1347
1361
  );
1348
- }
1362
+ };
1349
1363
 
1350
1364
  public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1351
1365
  if (!isFluidCodeDetails(codeDetails)) {
@@ -1376,7 +1390,7 @@ export class Container
1376
1390
  this.deltaManager.inboundSignal.pause(),
1377
1391
  ]);
1378
1392
 
1379
- if ((await this.context.satisfies(codeDetails)) === true) {
1393
+ if ((await this.satisfies(codeDetails)) === true) {
1380
1394
  this.deltaManager.inbound.resume();
1381
1395
  this.deltaManager.inboundSignal.resume();
1382
1396
  return;
@@ -1387,6 +1401,47 @@ export class Container
1387
1401
  this.close(error);
1388
1402
  }
1389
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
+
1390
1445
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
1391
1446
  const versions = await this.storageAdapter.getVersions(version, 1);
1392
1447
  return versions[0];
@@ -1464,7 +1519,10 @@ export class Container
1464
1519
  if (this.offlineLoadEnabled) {
1465
1520
  this.baseSnapshot = snapshot;
1466
1521
  // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1467
- this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1522
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(
1523
+ snapshot,
1524
+ this.storageAdapter,
1525
+ );
1468
1526
  }
1469
1527
  }
1470
1528
 
@@ -1520,7 +1578,7 @@ export class Container
1520
1578
  this.processRemoteMessage(message);
1521
1579
 
1522
1580
  // allow runtime to apply stashed ops at this op's sequence number
1523
- await this.context.notifyOpReplay(message);
1581
+ await this.runtime.notifyOpReplay?.(message);
1524
1582
  }
1525
1583
  pendingLocalState.savedOps = [];
1526
1584
 
@@ -1872,7 +1930,7 @@ export class Container
1872
1930
  });
1873
1931
 
1874
1932
  deltaManager.on("disconnect", (reason: string, error?: IAnyDriverError) => {
1875
- this.collabWindowTracker?.stopSequenceNumberUpdate();
1933
+ this.noopHeuristic?.notifyDisconnect();
1876
1934
  if (!this.closed) {
1877
1935
  this.connectionStateHandler.receivedDisconnectEvent(reason, error);
1878
1936
  }
@@ -1950,7 +2008,11 @@ export class Container
1950
2008
  } else if (value === ConnectionState.CatchingUp) {
1951
2009
  // This info is of most interesting while Catching Up.
1952
2010
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1953
- 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
+ ) {
1954
2016
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1955
2017
  }
1956
2018
  }
@@ -2095,7 +2157,7 @@ export class Container
2095
2157
  }
2096
2158
 
2097
2159
  this.messageCountAfterDisconnection += 1;
2098
- this.collabWindowTracker?.stopSequenceNumberUpdate();
2160
+ this.noopHeuristic?.notifyMessageSent();
2099
2161
  return this._deltaManager.submit(
2100
2162
  type,
2101
2163
  contents,
@@ -2112,39 +2174,67 @@ export class Container
2112
2174
  }
2113
2175
  const local = this.clientId === message.clientId;
2114
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
+
2115
2199
  // Allow the protocol handler to process the message
2116
2200
  const result = this.protocolHandler.processMessage(message, local);
2117
2201
 
2118
2202
  // Forward messages to the loaded runtime for processing
2119
- this.context.process(message, local);
2203
+ this.runtime.process(message, local);
2120
2204
 
2121
2205
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
2122
2206
  if (this.activeConnection()) {
2123
- if (this.collabWindowTracker === undefined) {
2207
+ if (this.noopHeuristic === undefined) {
2208
+ const serviceConfiguration = this.deltaManager.serviceConfiguration;
2124
2209
  // Note that config from first connection will be used for this container's lifetime.
2125
2210
  // That means that if relay service changes settings, such changes will impact only newly booted
2126
2211
  // clients.
2127
2212
  // All existing will continue to use settings they got earlier.
2128
2213
  assert(
2129
- this.serviceConfiguration !== undefined,
2214
+ serviceConfiguration !== undefined,
2130
2215
  0x2e4 /* "there should be service config for active connection" */,
2131
2216
  );
2132
- this.collabWindowTracker = new CollabWindowTracker(
2133
- (type) => {
2134
- assert(
2135
- this.activeConnection(),
2136
- 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */,
2137
- );
2138
- this.submitMessage(type);
2139
- },
2140
- this.serviceConfiguration.noopTimeFrequency,
2141
- this.serviceConfiguration.noopCountFrequency,
2217
+ this.noopHeuristic = new NoopHeuristic(
2218
+ serviceConfiguration.noopTimeFrequency,
2219
+ serviceConfiguration.noopCountFrequency,
2142
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);
2143
2237
  }
2144
- this.collabWindowTracker.scheduleSequenceNumberUpdate(
2145
- message,
2146
- result.immediateNoOp === true,
2147
- );
2148
2238
  }
2149
2239
 
2150
2240
  this.emit("op", message);
@@ -2156,11 +2246,11 @@ export class Container
2156
2246
 
2157
2247
  private processSignal(message: ISignalMessage) {
2158
2248
  // No clientId indicates a system signal message.
2159
- if (message.clientId === null) {
2249
+ if (protocolHandlerShouldProcessSignal(message)) {
2160
2250
  this.protocolHandler.processSignal(message);
2161
2251
  } else {
2162
2252
  const local = this.clientId === message.clientId;
2163
- this.context.processSignal(message, local);
2253
+ this.runtime.processSignal(message, local);
2164
2254
  }
2165
2255
  }
2166
2256
 
@@ -2202,23 +2292,50 @@ export class Container
2202
2292
  private async instantiateContext(
2203
2293
  existing: boolean,
2204
2294
  codeDetails: IFluidCodeDetails,
2205
- snapshot?: ISnapshotTree,
2295
+ snapshot: ISnapshotTree | undefined,
2206
2296
  pendingLocalState?: unknown,
2207
2297
  ) {
2208
- assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
2298
+ assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
2209
2299
 
2210
2300
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
2211
2301
  // are set. Global requests will still go directly to the loader
2212
2302
  const maybeLoader: FluidObject<IHostLoader> = this.scope;
2213
2303
  const loader = new RelativeLoader(this, maybeLoader.ILoader);
2214
- this._context = await ContainerContext.createOrLoad(
2215
- 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,
2216
2332
  this.scope,
2217
- this.codeLoader,
2218
- codeDetails,
2219
2333
  snapshot,
2220
- new DeltaManagerProxy(this._deltaManager),
2221
- new QuorumProxy(this.protocolHandler.quorum),
2334
+ this._loadedFromVersion,
2335
+ this._deltaManager,
2336
+ this.storageAdapter,
2337
+ this.protocolHandler.quorum,
2338
+ this.protocolHandler.audience,
2222
2339
  loader,
2223
2340
  (type, contents, batch, metadata) =>
2224
2341
  this.submitContainerMessage(type, contents, batch, metadata),
@@ -2229,22 +2346,42 @@ export class Container
2229
2346
  (message) => this.submitSignal(message),
2230
2347
  (error?: ICriticalContainerError) => this.dispose(error),
2231
2348
  (error?: ICriticalContainerError) => this.close(error),
2232
- Container.version,
2233
- (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,
2234
2358
  existing,
2359
+ this.subLogger,
2235
2360
  pendingLocalState,
2236
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;
2237
2374
 
2238
2375
  this.emit("contextChanged", codeDetails);
2239
2376
  }
2240
2377
 
2241
- private updateDirtyContainerState(dirty: boolean) {
2378
+ private readonly updateDirtyContainerState = (dirty: boolean) => {
2242
2379
  if (this._dirtyContainer === dirty) {
2243
2380
  return;
2244
2381
  }
2245
2382
  this._dirtyContainer = dirty;
2246
2383
  this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
2247
- }
2384
+ };
2248
2385
 
2249
2386
  /**
2250
2387
  * Set the connected state of the ContainerContext
@@ -2253,21 +2390,21 @@ export class Container
2253
2390
  * @param readonly - Is the container in readonly mode?
2254
2391
  */
2255
2392
  private setContextConnectedState(state: boolean, readonly: boolean): void {
2256
- if (this._context?.disposed === false) {
2393
+ if (this._runtime?.disposed === false) {
2257
2394
  /**
2258
2395
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2259
2396
  * ops getting through to the DeltaManager.
2260
2397
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2261
2398
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2262
2399
  */
2263
- this.context.setConnectionState(state && !readonly, this.clientId);
2400
+ this.runtime.setConnectionState(state && !readonly, this.clientId);
2264
2401
  }
2265
2402
  }
2266
2403
  }
2267
2404
 
2268
2405
  /**
2269
2406
  * IContainer interface that includes experimental features still under development.
2270
- * @internal
2407
+ * @experimental
2271
2408
  */
2272
2409
  export interface IContainerExperimental extends IContainer {
2273
2410
  /**
@@ -2278,5 +2415,13 @@ export interface IContainerExperimental extends IContainer {
2278
2415
  * @experimental misuse of this API can result in duplicate op submission and potential document corruption
2279
2416
  * {@link https://github.com/microsoft/FluidFramework/blob/main/packages/loader/container-loader/closeAndGetPendingLocalState.md}
2280
2417
  */
2281
- 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;
2282
2427
  }