@fluidframework/container-loader 2.0.0-internal.5.1.0 → 2.0.0-internal.5.2.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 (93) hide show
  1. package/CHANGELOG.md +16 -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.map +1 -1
  6. package/dist/connectionManager.js.map +1 -1
  7. package/dist/connectionStateHandler.d.ts +3 -0
  8. package/dist/connectionStateHandler.d.ts.map +1 -1
  9. package/dist/connectionStateHandler.js +10 -8
  10. package/dist/connectionStateHandler.js.map +1 -1
  11. package/dist/container.d.ts +20 -27
  12. package/dist/container.d.ts.map +1 -1
  13. package/dist/container.js +164 -104
  14. package/dist/container.js.map +1 -1
  15. package/dist/containerContext.d.ts +22 -58
  16. package/dist/containerContext.d.ts.map +1 -1
  17. package/dist/containerContext.js +27 -200
  18. package/dist/containerContext.js.map +1 -1
  19. package/dist/containerStorageAdapter.d.ts +1 -1
  20. package/dist/containerStorageAdapter.d.ts.map +1 -1
  21. package/dist/containerStorageAdapter.js.map +1 -1
  22. package/dist/noopHeuristic.d.ts +23 -0
  23. package/dist/noopHeuristic.d.ts.map +1 -0
  24. package/dist/{collabWindowTracker.js → noopHeuristic.js} +30 -42
  25. package/dist/noopHeuristic.js.map +1 -0
  26. package/dist/packageVersion.d.ts +1 -1
  27. package/dist/packageVersion.js +1 -1
  28. package/dist/packageVersion.js.map +1 -1
  29. package/dist/protocol.d.ts +1 -12
  30. package/dist/protocol.d.ts.map +1 -1
  31. package/dist/protocol.js +0 -18
  32. package/dist/protocol.js.map +1 -1
  33. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  34. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  35. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  36. package/dist/retriableDocumentStorageService.d.ts +1 -1
  37. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  38. package/dist/retriableDocumentStorageService.js.map +1 -1
  39. package/lib/catchUpMonitor.d.ts +1 -1
  40. package/lib/catchUpMonitor.d.ts.map +1 -1
  41. package/lib/catchUpMonitor.js.map +1 -1
  42. package/lib/connectionManager.d.ts.map +1 -1
  43. package/lib/connectionManager.js.map +1 -1
  44. package/lib/connectionStateHandler.d.ts +3 -0
  45. package/lib/connectionStateHandler.d.ts.map +1 -1
  46. package/lib/connectionStateHandler.js +10 -8
  47. package/lib/connectionStateHandler.js.map +1 -1
  48. package/lib/container.d.ts +20 -27
  49. package/lib/container.d.ts.map +1 -1
  50. package/lib/container.js +166 -106
  51. package/lib/container.js.map +1 -1
  52. package/lib/containerContext.d.ts +22 -58
  53. package/lib/containerContext.d.ts.map +1 -1
  54. package/lib/containerContext.js +27 -200
  55. package/lib/containerContext.js.map +1 -1
  56. package/lib/containerStorageAdapter.d.ts +1 -1
  57. package/lib/containerStorageAdapter.d.ts.map +1 -1
  58. package/lib/containerStorageAdapter.js.map +1 -1
  59. package/lib/noopHeuristic.d.ts +23 -0
  60. package/lib/noopHeuristic.d.ts.map +1 -0
  61. package/lib/{collabWindowTracker.js → noopHeuristic.js} +30 -42
  62. package/lib/noopHeuristic.js.map +1 -0
  63. package/lib/packageVersion.d.ts +1 -1
  64. package/lib/packageVersion.js +1 -1
  65. package/lib/packageVersion.js.map +1 -1
  66. package/lib/protocol.d.ts +1 -12
  67. package/lib/protocol.d.ts.map +1 -1
  68. package/lib/protocol.js +0 -18
  69. package/lib/protocol.js.map +1 -1
  70. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  71. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  72. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  73. package/lib/retriableDocumentStorageService.d.ts +1 -1
  74. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  75. package/lib/retriableDocumentStorageService.js.map +1 -1
  76. package/package.json +11 -11
  77. package/src/catchUpMonitor.ts +1 -1
  78. package/src/connectionManager.ts +2 -1
  79. package/src/connectionStateHandler.ts +14 -9
  80. package/src/container.ts +247 -126
  81. package/src/containerContext.ts +32 -318
  82. package/src/containerStorageAdapter.ts +1 -1
  83. package/src/{collabWindowTracker.ts → noopHeuristic.ts} +37 -47
  84. package/src/packageVersion.ts +1 -1
  85. package/src/protocol.ts +0 -39
  86. package/src/protocolTreeDocumentStorageService.ts +1 -1
  87. package/src/retriableDocumentStorageService.ts +2 -1
  88. package/dist/collabWindowTracker.d.ts +0 -19
  89. package/dist/collabWindowTracker.d.ts.map +0 -1
  90. package/dist/collabWindowTracker.js.map +0 -1
  91. package/lib/collabWindowTracker.d.ts +0 -19
  92. package/lib/collabWindowTracker.d.ts.map +0 -1
  93. package/lib/collabWindowTracker.js.map +0 -1
package/src/container.ts CHANGED
@@ -7,8 +7,17 @@
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";
10
+ import {
11
+ IEvent,
12
+ ITelemetryProperties,
13
+ TelemetryEventCategory,
14
+ } from "@fluidframework/common-definitions";
15
+ import {
16
+ TypedEventEmitter,
17
+ assert,
18
+ performance,
19
+ unreachableCase,
20
+ } from "@fluidframework/common-utils";
12
21
  import { IRequest, IResponse, IFluidRouter, FluidObject } from "@fluidframework/core-interfaces";
13
22
  import {
14
23
  IAudience,
@@ -27,6 +36,11 @@ import {
27
36
  IBatchMessage,
28
37
  ICodeDetailsLoader,
29
38
  IHostLoader,
39
+ IFluidModuleWithDetails,
40
+ IProvideRuntimeFactory,
41
+ IProvideFluidCodeDetailsComparer,
42
+ IFluidCodeDetailsComparer,
43
+ IRuntime,
30
44
  } from "@fluidframework/container-definitions";
31
45
  import { GenericError, UsageError } from "@fluidframework/container-utils";
32
46
  import {
@@ -44,11 +58,12 @@ import {
44
58
  combineAppAndProtocolSummary,
45
59
  runWithRetry,
46
60
  isCombinedAppAndProtocolSummary,
61
+ MessageType2,
62
+ canBeCoalescedByService,
47
63
  } from "@fluidframework/driver-utils";
48
64
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
49
65
  import {
50
66
  IClient,
51
- IClientConfiguration,
52
67
  IClientDetails,
53
68
  ICommittedProposal,
54
69
  IDocumentAttributes,
@@ -99,7 +114,7 @@ import {
99
114
  getCodeDetailsFromQuorumValues,
100
115
  QuorumProxy,
101
116
  } from "./quorum";
102
- import { CollabWindowTracker } from "./collabWindowTracker";
117
+ import { NoopHeuristic } from "./noopHeuristic";
103
118
  import { ConnectionManager } from "./connectionManager";
104
119
  import { ConnectionState } from "./connectionState";
105
120
  import {
@@ -114,6 +129,8 @@ const detachedContainerRefSeqNumber = 0;
114
129
  const dirtyContainerEvent = "dirty";
115
130
  const savedContainerEvent = "saved";
116
131
 
132
+ const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
133
+
117
134
  /**
118
135
  * @internal
119
136
  */
@@ -336,12 +353,15 @@ export interface IPendingContainerState {
336
353
 
337
354
  const summarizerClientType = "summarizer";
338
355
 
356
+ interface IContainerLifecycleEvents extends IEvent {
357
+ (event: "runtimeInstantiated", listener: () => void): void;
358
+ (event: "disposed", listener: () => void): void;
359
+ }
360
+
339
361
  export class Container
340
362
  extends EventEmitterWithErrorHandling<IContainerEvents>
341
363
  implements IContainer, IContainerExperimental
342
364
  {
343
- public static version = "^0.1.0";
344
-
345
365
  /**
346
366
  * Load an existing container.
347
367
  * @internal
@@ -354,6 +374,10 @@ export class Container
354
374
 
355
375
  const container = new Container(createProps, loadProps);
356
376
 
377
+ const disableRecordHeapSize = container.mc.config.getBoolean(
378
+ "Fluid.Loader.DisableRecordHeapSize",
379
+ );
380
+
357
381
  return PerformanceEvent.timedExecAsync(
358
382
  container.mc.logger,
359
383
  { eventName: "Load" },
@@ -395,6 +419,7 @@ export class Container
395
419
  );
396
420
  }),
397
421
  { start: true, end: true, cancel: "generic" },
422
+ disableRecordHeapSize !== true /* recordHeapSize */,
398
423
  );
399
424
  }
400
425
 
@@ -447,9 +472,9 @@ export class Container
447
472
  private readonly urlResolver: IUrlResolver;
448
473
  private readonly serviceFactory: IDocumentServiceFactory;
449
474
  private readonly codeLoader: ICodeDetailsLoader;
450
- public readonly options: ILoaderOptions;
475
+ private readonly options: ILoaderOptions;
451
476
  private readonly scope: FluidObject;
452
- public subLogger: TelemetryLogger;
477
+ private readonly subLogger: TelemetryLogger;
453
478
  private readonly detachedBlobStorage: IDetachedBlobStorage | undefined;
454
479
  private readonly protocolHandlerBuilder: ProtocolHandlerBuilder;
455
480
 
@@ -509,19 +534,16 @@ export class Container
509
534
  private _attachState = AttachState.Detached;
510
535
 
511
536
  private readonly storageAdapter: ContainerStorageAdapter;
512
- public get storage(): IDocumentStorageService {
513
- return this.storageAdapter;
514
- }
515
537
 
516
538
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
517
539
  private service: IDocumentService | undefined;
518
540
 
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");
541
+ private _runtime: IRuntime | undefined;
542
+ private get runtime() {
543
+ if (this._runtime === undefined) {
544
+ throw new Error("Attempted to access runtime before it was defined");
523
545
  }
524
- return this._context;
546
+ return this._runtime;
525
547
  }
526
548
  private _protocolHandler: IProtocolHandler | undefined;
527
549
  private get protocolHandler() {
@@ -546,10 +568,11 @@ export class Container
546
568
  private lastVisible: number | undefined;
547
569
  private readonly visibilityEventHandler: (() => void) | undefined;
548
570
  private readonly connectionStateHandler: IConnectionStateHandler;
571
+ private readonly clientsWhoShouldHaveLeft = new Set<string>();
549
572
 
550
573
  private setAutoReconnectTime = performance.now();
551
574
 
552
- private collabWindowTracker: CollabWindowTracker | undefined;
575
+ private noopHeuristic: NoopHeuristic | undefined;
553
576
 
554
577
  private get connectionMode() {
555
578
  return this._deltaManager.connectionManager.connectionMode;
@@ -574,18 +597,10 @@ export class Container
574
597
  return this.service?.resolvedUrl;
575
598
  }
576
599
 
577
- public get loadedFromVersion(): IVersion | undefined {
578
- return this._loadedFromVersion;
579
- }
580
-
581
600
  public get readOnlyInfo(): ReadOnlyInfo {
582
601
  return this._deltaManager.readOnlyInfo;
583
602
  }
584
603
 
585
- public get closeSignal(): AbortSignal {
586
- return this._deltaManager.closeAbortController.signal;
587
- }
588
-
589
604
  /**
590
605
  * Tracks host requiring read-only mode.
591
606
  */
@@ -601,18 +616,10 @@ export class Container
601
616
  return this.connectionStateHandler.connectionState;
602
617
  }
603
618
 
604
- public get connected(): boolean {
619
+ private get connected(): boolean {
605
620
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
606
621
  }
607
622
 
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
623
  private _clientId: string | undefined;
617
624
 
618
625
  /**
@@ -623,24 +630,12 @@ export class Container
623
630
  return this._clientId;
624
631
  }
625
632
 
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
633
  private get offlineLoadEnabled(): boolean {
639
634
  const enabled =
640
635
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
641
636
  this.options?.enableOfflineLoad === true;
642
637
  // summarizer will not have any pending state we want to save
643
- return enabled && this.clientDetails.capabilities.interactive;
638
+ return enabled && this.deltaManager.clientDetails.capabilities.interactive;
644
639
  }
645
640
 
646
641
  /**
@@ -651,15 +646,18 @@ export class Container
651
646
  return this.getCodeDetailsFromQuorum();
652
647
  }
653
648
 
649
+ private _loadedCodeDetails: IFluidCodeDetails | undefined;
654
650
  /**
655
651
  * Get the code details that were used to load the container.
656
652
  * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
657
653
  * loaded.
658
654
  */
659
655
  public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
660
- return this._context?.codeDetails;
656
+ return this._loadedCodeDetails;
661
657
  }
662
658
 
659
+ private _loadedModule: IFluidModuleWithDetails | undefined;
660
+
663
661
  /**
664
662
  * Retrieves the audience associated with the document
665
663
  */
@@ -679,38 +677,33 @@ export class Container
679
677
  /**
680
678
  * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
681
679
  */
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");
680
+ public async getEntryPoint(): Promise<FluidObject | undefined> {
681
+ if (this._disposed) {
682
+ throw new UsageError("The context is already disposed");
688
683
  }
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
- );
684
+ if (this._runtime !== undefined) {
685
+ return this._runtime.getEntryPoint?.();
708
686
  }
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?.();
687
+ return new Promise<FluidObject | undefined>((resolve, reject) => {
688
+ const runtimeInstantiatedHandler = () => {
689
+ assert(
690
+ this._runtime !== undefined,
691
+ 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */,
692
+ );
693
+ resolve(this._runtime.getEntryPoint?.());
694
+ this._lifecycleEvents.off("disposed", disposedHandler);
695
+ };
696
+ const disposedHandler = () => {
697
+ reject(new Error("ContainerContext was disposed"));
698
+ this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
699
+ };
700
+ this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
701
+ this._lifecycleEvents.once("disposed", disposedHandler);
702
+ });
712
703
  }
713
704
 
705
+ private readonly _lifecycleEvents = new TypedEventEmitter<IContainerLifecycleEvents>();
706
+
714
707
  /**
715
708
  * @internal
716
709
  */
@@ -795,8 +788,8 @@ export class Container
795
788
  dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
796
789
  dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
797
790
  dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
798
- containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
799
- containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
791
+ containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
792
+ containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
800
793
  // message information to associate errors with the specific execution state
801
794
  // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
802
795
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
@@ -866,6 +859,9 @@ export class Container
866
859
  this.connect();
867
860
  }
868
861
  },
862
+ clientShouldHaveLeft: (clientId: string) => {
863
+ this.clientsWhoShouldHaveLeft.add(clientId);
864
+ },
869
865
  },
870
866
  this.deltaManager,
871
867
  pendingLocalState?.clientId,
@@ -1005,7 +1001,8 @@ export class Container
1005
1001
  this.mc.logger.sendTelemetryEvent(
1006
1002
  {
1007
1003
  eventName: "ContainerDispose",
1008
- category: "generic",
1004
+ // Only log error if container isn't closed
1005
+ category: !this.closed && error !== undefined ? "error" : "generic",
1009
1006
  },
1010
1007
  error,
1011
1008
  );
@@ -1019,7 +1016,8 @@ export class Container
1019
1016
 
1020
1017
  this.connectionStateHandler.dispose();
1021
1018
 
1022
- this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
1019
+ const maybeError = error !== undefined ? new Error(error.message) : undefined;
1020
+ this._runtime?.dispose(maybeError);
1023
1021
 
1024
1022
  this.storageAdapter.dispose();
1025
1023
 
@@ -1042,6 +1040,7 @@ export class Container
1042
1040
  }
1043
1041
  } finally {
1044
1042
  this._lifecycleState = "disposed";
1043
+ this._lifecycleEvents.emit("disposed");
1045
1044
  }
1046
1045
  }
1047
1046
 
@@ -1069,7 +1068,7 @@ export class Container
1069
1068
  assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1070
1069
  assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1071
1070
  const pendingState: IPendingContainerState = {
1072
- pendingRuntimeState: this.context.getPendingLocalState(),
1071
+ pendingRuntimeState: this.runtime.getPendingLocalState(),
1073
1072
  baseSnapshot: this.baseSnapshot,
1074
1073
  snapshotBlobs: this.baseSnapshotBlobs,
1075
1074
  savedOps: this.savedOps,
@@ -1093,7 +1092,7 @@ export class Container
1093
1092
  0x0d3 /* "Should only be called in detached container" */,
1094
1093
  );
1095
1094
 
1096
- const appSummary: ISummaryTree = this.context.createSummary();
1095
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1097
1096
  const protocolSummary = this.captureProtocolSummary();
1098
1097
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1099
1098
 
@@ -1139,7 +1138,7 @@ export class Container
1139
1138
  if (!hasAttachmentBlobs) {
1140
1139
  // Get the document state post attach - possibly can just call attach but we need to change the
1141
1140
  // semantics around what the attach means as far as async code goes.
1142
- const appSummary: ISummaryTree = this.context.createSummary();
1141
+ const appSummary: ISummaryTree = this.runtime.createSummary();
1143
1142
  const protocolSummary = this.captureProtocolSummary();
1144
1143
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1145
1144
 
@@ -1148,6 +1147,7 @@ export class Container
1148
1147
  // starting to attach the container to storage.
1149
1148
  // Also, this should only be fired in detached container.
1150
1149
  this._attachState = AttachState.Attaching;
1150
+ this.runtime.setAttachState(AttachState.Attaching);
1151
1151
  this.emit("attaching");
1152
1152
  if (this.offlineLoadEnabled) {
1153
1153
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1176,7 +1176,7 @@ export class Container
1176
1176
  "containerAttach",
1177
1177
  this.mc.logger,
1178
1178
  {
1179
- cancel: this.closeSignal,
1179
+ cancel: this._deltaManager.closeAbortController.signal,
1180
1180
  }, // progress
1181
1181
  );
1182
1182
  }
@@ -1205,11 +1205,12 @@ export class Container
1205
1205
  }
1206
1206
 
1207
1207
  // take summary and upload
1208
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
1208
+ const appSummary: ISummaryTree = this.runtime.createSummary(redirectTable);
1209
1209
  const protocolSummary = this.captureProtocolSummary();
1210
1210
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1211
1211
 
1212
1212
  this._attachState = AttachState.Attaching;
1213
+ this.runtime.setAttachState(AttachState.Attaching);
1213
1214
  this.emit("attaching");
1214
1215
  if (this.offlineLoadEnabled) {
1215
1216
  const snapshot = getSnapshotTreeFromSerializedContainer(summary);
@@ -1226,6 +1227,7 @@ export class Container
1226
1227
  }
1227
1228
 
1228
1229
  this._attachState = AttachState.Attached;
1230
+ this.runtime.setAttachState(AttachState.Attached);
1229
1231
  this.emit("attached");
1230
1232
 
1231
1233
  if (!this.closed) {
@@ -1250,7 +1252,7 @@ export class Container
1250
1252
  return PerformanceEvent.timedExecAsync(
1251
1253
  this.mc.logger,
1252
1254
  { eventName: "Request" },
1253
- async () => this.context.request(path),
1255
+ async () => this.runtime.request(path),
1254
1256
  { end: true, cancel: "error" },
1255
1257
  );
1256
1258
  }
@@ -1335,7 +1337,7 @@ export class Container
1335
1337
  this.connectToDeltaStream(args);
1336
1338
  }
1337
1339
 
1338
- public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1340
+ public readonly getAbsoluteUrl = async (relativeUrl: string): Promise<string | undefined> => {
1339
1341
  if (this.resolvedUrl === undefined) {
1340
1342
  return undefined;
1341
1343
  }
@@ -1343,9 +1345,9 @@ export class Container
1343
1345
  return this.urlResolver.getAbsoluteUrl(
1344
1346
  this.resolvedUrl,
1345
1347
  relativeUrl,
1346
- getPackageName(this._context?.codeDetails),
1348
+ getPackageName(this._loadedCodeDetails),
1347
1349
  );
1348
- }
1350
+ };
1349
1351
 
1350
1352
  public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1351
1353
  if (!isFluidCodeDetails(codeDetails)) {
@@ -1376,7 +1378,7 @@ export class Container
1376
1378
  this.deltaManager.inboundSignal.pause(),
1377
1379
  ]);
1378
1380
 
1379
- if ((await this.context.satisfies(codeDetails)) === true) {
1381
+ if ((await this.satisfies(codeDetails)) === true) {
1380
1382
  this.deltaManager.inbound.resume();
1381
1383
  this.deltaManager.inboundSignal.resume();
1382
1384
  return;
@@ -1387,6 +1389,47 @@ export class Container
1387
1389
  this.close(error);
1388
1390
  }
1389
1391
 
1392
+ /**
1393
+ * Determines if the currently loaded module satisfies the incoming constraint code details
1394
+ */
1395
+ private async satisfies(constraintCodeDetails: IFluidCodeDetails) {
1396
+ // If we have no module, it can't satisfy anything.
1397
+ if (this._loadedModule === undefined) {
1398
+ return false;
1399
+ }
1400
+
1401
+ const comparers: IFluidCodeDetailsComparer[] = [];
1402
+
1403
+ const maybeCompareCodeLoader = this.codeLoader;
1404
+ if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
1405
+ comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
1406
+ }
1407
+
1408
+ const maybeCompareExport: Partial<IProvideFluidCodeDetailsComparer> | undefined =
1409
+ this._loadedModule?.module.fluidExport;
1410
+ if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
1411
+ comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
1412
+ }
1413
+
1414
+ // If there are no comparers, then it's impossible to know if the currently loaded package satisfies
1415
+ // the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
1416
+ // rather than potentially running with incompatible code.
1417
+ if (comparers.length === 0) {
1418
+ return false;
1419
+ }
1420
+
1421
+ for (const comparer of comparers) {
1422
+ const satisfies = await comparer.satisfies(
1423
+ this._loadedModule?.details,
1424
+ constraintCodeDetails,
1425
+ );
1426
+ if (satisfies === false) {
1427
+ return false;
1428
+ }
1429
+ }
1430
+ return true;
1431
+ }
1432
+
1390
1433
  private async getVersion(version: string | null): Promise<IVersion | undefined> {
1391
1434
  const versions = await this.storageAdapter.getVersions(version, 1);
1392
1435
  return versions[0];
@@ -1464,7 +1507,10 @@ export class Container
1464
1507
  if (this.offlineLoadEnabled) {
1465
1508
  this.baseSnapshot = snapshot;
1466
1509
  // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1467
- this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1510
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(
1511
+ snapshot,
1512
+ this.storageAdapter,
1513
+ );
1468
1514
  }
1469
1515
  }
1470
1516
 
@@ -1520,7 +1566,7 @@ export class Container
1520
1566
  this.processRemoteMessage(message);
1521
1567
 
1522
1568
  // allow runtime to apply stashed ops at this op's sequence number
1523
- await this.context.notifyOpReplay(message);
1569
+ await this.runtime.notifyOpReplay?.(message);
1524
1570
  }
1525
1571
  pendingLocalState.savedOps = [];
1526
1572
 
@@ -1872,7 +1918,7 @@ export class Container
1872
1918
  });
1873
1919
 
1874
1920
  deltaManager.on("disconnect", (reason: string, error?: IAnyDriverError) => {
1875
- this.collabWindowTracker?.stopSequenceNumberUpdate();
1921
+ this.noopHeuristic?.notifyDisconnect();
1876
1922
  if (!this.closed) {
1877
1923
  this.connectionStateHandler.receivedDisconnectEvent(reason, error);
1878
1924
  }
@@ -2095,7 +2141,7 @@ export class Container
2095
2141
  }
2096
2142
 
2097
2143
  this.messageCountAfterDisconnection += 1;
2098
- this.collabWindowTracker?.stopSequenceNumberUpdate();
2144
+ this.noopHeuristic?.notifyMessageSent();
2099
2145
  return this._deltaManager.submit(
2100
2146
  type,
2101
2147
  contents,
@@ -2112,39 +2158,67 @@ export class Container
2112
2158
  }
2113
2159
  const local = this.clientId === message.clientId;
2114
2160
 
2161
+ // Check and report if we're getting messages from a clientId that we previously
2162
+ // flagged should have left, or from a client that's not in the quorum but should be
2163
+ if (message.clientId != null) {
2164
+ const client = this.protocolHandler.quorum.getMember(message.clientId);
2165
+
2166
+ if (client === undefined && message.type !== MessageType.ClientJoin) {
2167
+ // pre-0.58 error message: messageClientIdMissingFromQuorum
2168
+ throw new Error("Remote message's clientId is missing from the quorum");
2169
+ }
2170
+
2171
+ // Here checking canBeCoalescedByService is used as an approximation of "is benign to process despite being unexpected".
2172
+ // It's still not good to see these messages from unexpected clientIds, but since they don't harm the integrity of the
2173
+ // document we don't need to blow up aggressively.
2174
+ if (
2175
+ this.clientsWhoShouldHaveLeft.has(message.clientId) &&
2176
+ !canBeCoalescedByService(message)
2177
+ ) {
2178
+ // pre-0.58 error message: messageClientIdShouldHaveLeft
2179
+ throw new Error("Remote message's clientId already should have left");
2180
+ }
2181
+ }
2182
+
2115
2183
  // Allow the protocol handler to process the message
2116
2184
  const result = this.protocolHandler.processMessage(message, local);
2117
2185
 
2118
2186
  // Forward messages to the loaded runtime for processing
2119
- this.context.process(message, local);
2187
+ this.runtime.process(message, local);
2120
2188
 
2121
2189
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
2122
2190
  if (this.activeConnection()) {
2123
- if (this.collabWindowTracker === undefined) {
2191
+ if (this.noopHeuristic === undefined) {
2192
+ const serviceConfiguration = this.deltaManager.serviceConfiguration;
2124
2193
  // Note that config from first connection will be used for this container's lifetime.
2125
2194
  // That means that if relay service changes settings, such changes will impact only newly booted
2126
2195
  // clients.
2127
2196
  // All existing will continue to use settings they got earlier.
2128
2197
  assert(
2129
- this.serviceConfiguration !== undefined,
2198
+ serviceConfiguration !== undefined,
2130
2199
  0x2e4 /* "there should be service config for active connection" */,
2131
2200
  );
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,
2201
+ this.noopHeuristic = new NoopHeuristic(
2202
+ serviceConfiguration.noopTimeFrequency,
2203
+ serviceConfiguration.noopCountFrequency,
2142
2204
  );
2205
+ this.noopHeuristic.on("wantsNoop", () => {
2206
+ // On disconnect we notify the heuristic which should prevent it from wanting a noop.
2207
+ // Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
2208
+ // running the microtask that the heuristic queued in response.
2209
+ assert(
2210
+ this.activeConnection(),
2211
+ 0x241 /* "Trying to send noop without active connection" */,
2212
+ );
2213
+ this.submitMessage(MessageType.NoOp);
2214
+ });
2215
+ }
2216
+ this.noopHeuristic.notifyMessageProcessed(message);
2217
+ // The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
2218
+ if (result.immediateNoOp === true) {
2219
+ // ADO:1385: Remove cast and use MessageType once definition changes propagate
2220
+ this.submitMessage(MessageType2.Accept as unknown as MessageType);
2143
2221
  }
2144
- this.collabWindowTracker.scheduleSequenceNumberUpdate(
2145
- message,
2146
- result.immediateNoOp === true,
2147
- );
2148
2222
  }
2149
2223
 
2150
2224
  this.emit("op", message);
@@ -2160,7 +2234,7 @@ export class Container
2160
2234
  this.protocolHandler.processSignal(message);
2161
2235
  } else {
2162
2236
  const local = this.clientId === message.clientId;
2163
- this.context.processSignal(message, local);
2237
+ this.runtime.processSignal(message, local);
2164
2238
  }
2165
2239
  }
2166
2240
 
@@ -2202,23 +2276,49 @@ export class Container
2202
2276
  private async instantiateContext(
2203
2277
  existing: boolean,
2204
2278
  codeDetails: IFluidCodeDetails,
2205
- snapshot?: ISnapshotTree,
2279
+ snapshot: ISnapshotTree | undefined,
2206
2280
  pendingLocalState?: unknown,
2207
2281
  ) {
2208
- assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
2282
+ assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
2209
2283
 
2210
2284
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
2211
2285
  // are set. Global requests will still go directly to the loader
2212
2286
  const maybeLoader: FluidObject<IHostLoader> = this.scope;
2213
2287
  const loader = new RelativeLoader(this, maybeLoader.ILoader);
2214
- this._context = await ContainerContext.createOrLoad(
2215
- this,
2288
+
2289
+ const loadCodeResult = await PerformanceEvent.timedExecAsync(
2290
+ this.subLogger,
2291
+ { eventName: "CodeLoad" },
2292
+ async () => this.codeLoader.load(codeDetails),
2293
+ );
2294
+
2295
+ this._loadedModule = {
2296
+ module: loadCodeResult.module,
2297
+ // An older interface ICodeLoader could return an IFluidModule which didn't have details.
2298
+ // If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
2299
+ // TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
2300
+ details: loadCodeResult.details ?? codeDetails,
2301
+ };
2302
+
2303
+ const fluidExport: FluidObject<IProvideRuntimeFactory> | undefined =
2304
+ this._loadedModule.module.fluidExport;
2305
+ const runtimeFactory = fluidExport?.IRuntimeFactory;
2306
+ if (runtimeFactory === undefined) {
2307
+ throw new Error(packageNotFactoryError);
2308
+ }
2309
+
2310
+ const deltaManagerProxy = new DeltaManagerProxy(this._deltaManager);
2311
+ const quorumProxy = new QuorumProxy(this.protocolHandler.quorum);
2312
+
2313
+ const context = new ContainerContext(
2314
+ this.options,
2216
2315
  this.scope,
2217
- this.codeLoader,
2218
- codeDetails,
2219
2316
  snapshot,
2220
- new DeltaManagerProxy(this._deltaManager),
2221
- new QuorumProxy(this.protocolHandler.quorum),
2317
+ this._loadedFromVersion,
2318
+ deltaManagerProxy,
2319
+ this.storageAdapter,
2320
+ quorumProxy,
2321
+ this.protocolHandler.audience,
2222
2322
  loader,
2223
2323
  (type, contents, batch, metadata) =>
2224
2324
  this.submitContainerMessage(type, contents, batch, metadata),
@@ -2229,22 +2329,43 @@ export class Container
2229
2329
  (message) => this.submitSignal(message),
2230
2330
  (error?: ICriticalContainerError) => this.dispose(error),
2231
2331
  (error?: ICriticalContainerError) => this.close(error),
2232
- Container.version,
2233
- (dirty: boolean) => this.updateDirtyContainerState(dirty),
2332
+ this.updateDirtyContainerState,
2333
+ this.getAbsoluteUrl,
2334
+ () => this.resolvedUrl?.id,
2335
+ () => this.clientId,
2336
+ () => deltaManagerProxy.serviceConfiguration,
2337
+ () => this.attachState,
2338
+ () => this.connected,
2339
+ this._deltaManager.clientDetails,
2234
2340
  existing,
2341
+ this.subLogger,
2235
2342
  pendingLocalState,
2236
2343
  );
2344
+ this._lifecycleEvents.once("disposed", () => {
2345
+ context.dispose();
2346
+ quorumProxy.dispose();
2347
+ deltaManagerProxy.dispose();
2348
+ });
2349
+
2350
+ this._runtime = await PerformanceEvent.timedExecAsync(
2351
+ this.subLogger,
2352
+ { eventName: "InstantiateRuntime" },
2353
+ async () => runtimeFactory.instantiateRuntime(context, existing),
2354
+ );
2355
+ this._lifecycleEvents.emit("runtimeInstantiated");
2356
+
2357
+ this._loadedCodeDetails = codeDetails;
2237
2358
 
2238
2359
  this.emit("contextChanged", codeDetails);
2239
2360
  }
2240
2361
 
2241
- private updateDirtyContainerState(dirty: boolean) {
2362
+ private readonly updateDirtyContainerState = (dirty: boolean) => {
2242
2363
  if (this._dirtyContainer === dirty) {
2243
2364
  return;
2244
2365
  }
2245
2366
  this._dirtyContainer = dirty;
2246
2367
  this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
2247
- }
2368
+ };
2248
2369
 
2249
2370
  /**
2250
2371
  * Set the connected state of the ContainerContext
@@ -2253,14 +2374,14 @@ export class Container
2253
2374
  * @param readonly - Is the container in readonly mode?
2254
2375
  */
2255
2376
  private setContextConnectedState(state: boolean, readonly: boolean): void {
2256
- if (this._context?.disposed === false) {
2377
+ if (this._runtime?.disposed === false) {
2257
2378
  /**
2258
2379
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2259
2380
  * ops getting through to the DeltaManager.
2260
2381
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2261
2382
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2262
2383
  */
2263
- this.context.setConnectionState(state && !readonly, this.clientId);
2384
+ this.runtime.setConnectionState(state && !readonly, this.clientId);
2264
2385
  }
2265
2386
  }
2266
2387
  }