@fluidframework/container-runtime 2.70.0-361788 → 2.71.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 (78) hide show
  1. package/.eslintrc.cjs +5 -1
  2. package/CHANGELOG.md +14 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/channelCollection.d.ts +66 -17
  5. package/dist/channelCollection.d.ts.map +1 -1
  6. package/dist/channelCollection.js +118 -84
  7. package/dist/channelCollection.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +19 -11
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +146 -52
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/dataStore.d.ts +3 -1
  13. package/dist/dataStore.d.ts.map +1 -1
  14. package/dist/dataStore.js +8 -9
  15. package/dist/dataStore.js.map +1 -1
  16. package/dist/dataStoreContext.d.ts +6 -5
  17. package/dist/dataStoreContext.d.ts.map +1 -1
  18. package/dist/dataStoreContext.js +7 -4
  19. package/dist/dataStoreContext.js.map +1 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/messageTypes.d.ts +17 -4
  24. package/dist/messageTypes.d.ts.map +1 -1
  25. package/dist/messageTypes.js.map +1 -1
  26. package/dist/packageVersion.d.ts +1 -1
  27. package/dist/packageVersion.d.ts.map +1 -1
  28. package/dist/packageVersion.js +1 -1
  29. package/dist/packageVersion.js.map +1 -1
  30. package/dist/runtimeLayerCompatState.d.ts +2 -2
  31. package/dist/runtimeLayerCompatState.d.ts.map +1 -1
  32. package/dist/runtimeLayerCompatState.js.map +1 -1
  33. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  34. package/dist/summary/summarizerNode/summarizerNode.js +3 -1
  35. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  36. package/lib/channelCollection.d.ts +66 -17
  37. package/lib/channelCollection.d.ts.map +1 -1
  38. package/lib/channelCollection.js +115 -82
  39. package/lib/channelCollection.js.map +1 -1
  40. package/lib/containerRuntime.d.ts +19 -11
  41. package/lib/containerRuntime.d.ts.map +1 -1
  42. package/lib/containerRuntime.js +148 -53
  43. package/lib/containerRuntime.js.map +1 -1
  44. package/lib/dataStore.d.ts +3 -1
  45. package/lib/dataStore.d.ts.map +1 -1
  46. package/lib/dataStore.js +3 -4
  47. package/lib/dataStore.js.map +1 -1
  48. package/lib/dataStoreContext.d.ts +6 -5
  49. package/lib/dataStoreContext.d.ts.map +1 -1
  50. package/lib/dataStoreContext.js +8 -5
  51. package/lib/dataStoreContext.js.map +1 -1
  52. package/lib/index.d.ts +3 -1
  53. package/lib/index.d.ts.map +1 -1
  54. package/lib/index.js +1 -1
  55. package/lib/index.js.map +1 -1
  56. package/lib/messageTypes.d.ts +17 -4
  57. package/lib/messageTypes.d.ts.map +1 -1
  58. package/lib/messageTypes.js.map +1 -1
  59. package/lib/packageVersion.d.ts +1 -1
  60. package/lib/packageVersion.d.ts.map +1 -1
  61. package/lib/packageVersion.js +1 -1
  62. package/lib/packageVersion.js.map +1 -1
  63. package/lib/runtimeLayerCompatState.d.ts +2 -2
  64. package/lib/runtimeLayerCompatState.d.ts.map +1 -1
  65. package/lib/runtimeLayerCompatState.js.map +1 -1
  66. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  67. package/lib/summary/summarizerNode/summarizerNode.js +3 -1
  68. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  69. package/package.json +22 -30
  70. package/src/channelCollection.ts +255 -109
  71. package/src/containerRuntime.ts +262 -92
  72. package/src/dataStore.ts +12 -10
  73. package/src/dataStoreContext.ts +45 -39
  74. package/src/index.ts +8 -3
  75. package/src/messageTypes.ts +17 -2
  76. package/src/packageVersion.ts +1 -1
  77. package/src/runtimeLayerCompatState.ts +1 -1
  78. package/src/summary/summarizerNode/summarizerNode.ts +1 -0
@@ -7,7 +7,12 @@ import type {
7
7
  ILayerCompatDetails,
8
8
  IProvideLayerCompatDetails,
9
9
  } from "@fluid-internal/client-utils";
10
- import { createEmitter, Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
10
+ import {
11
+ checkLayerCompatibility,
12
+ createEmitter,
13
+ Trace,
14
+ TypedEventEmitter,
15
+ } from "@fluid-internal/client-utils";
11
16
  import type {
12
17
  IAudience,
13
18
  ISelf,
@@ -23,6 +28,7 @@ import type {
23
28
  IDeltaManagerFull,
24
29
  ILoader,
25
30
  IContainerStorageService,
31
+ ConnectionStatus,
26
32
  } from "@fluidframework/container-definitions/internal";
27
33
  import {
28
34
  ConnectionState,
@@ -33,6 +39,7 @@ import type {
33
39
  ContainerExtensionId,
34
40
  ExtensionHost,
35
41
  ExtensionHostEvents,
42
+ ExtensionInstantiationResult,
36
43
  ExtensionRuntimeProperties,
37
44
  IContainerRuntime,
38
45
  IContainerRuntimeEvents,
@@ -110,9 +117,9 @@ import type {
110
117
  IGarbageCollectionData,
111
118
  CreateChildSummarizerNodeParam,
112
119
  IDataStore,
113
- IEnvelope,
114
120
  IFluidDataStoreContextDetached,
115
121
  IFluidDataStoreRegistry,
122
+ IFluidParentContext,
116
123
  ISummarizeInternalResult,
117
124
  InboundAttachMessage,
118
125
  NamedFluidDataStoreRegistryEntries,
@@ -120,12 +127,10 @@ import type {
120
127
  IInboundSignalMessage,
121
128
  IRuntimeMessagesContent,
122
129
  ISummarizerNodeWithGC,
123
- // eslint-disable-next-line import/no-deprecated
124
- StageControlsExperimental,
125
- // eslint-disable-next-line import/no-deprecated
126
- IContainerRuntimeBaseExperimental,
127
- IFluidParentContext,
130
+ StageControlsInternal,
131
+ IContainerRuntimeBaseInternal,
128
132
  MinimumVersionForCollab,
133
+ ContainerExtensionExpectations,
129
134
  } from "@fluidframework/runtime-definitions/internal";
130
135
  import {
131
136
  addBlobToSummary,
@@ -181,10 +186,14 @@ import {
181
186
  loadBlobManagerLoadInfo,
182
187
  type IBlobManagerLoadInfo,
183
188
  } from "./blobManager/index.js";
189
+ import type {
190
+ AddressedUnsequencedSignalEnvelope,
191
+ IFluidRootParentContextPrivate,
192
+ } from "./channelCollection.js";
184
193
  import {
185
194
  ChannelCollection,
195
+ formParentContext,
186
196
  getSummaryForDatastores,
187
- wrapContext,
188
197
  } from "./channelCollection.js";
189
198
  import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
190
199
  import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
@@ -218,11 +227,14 @@ import {
218
227
  import { InboundBatchAggregator } from "./inboundBatchAggregator.js";
219
228
  import {
220
229
  ContainerMessageType,
230
+ type ContainerRuntimeAliasMessage,
231
+ type ContainerRuntimeDataStoreOpMessage,
221
232
  type OutboundContainerRuntimeDocumentSchemaMessage,
222
233
  type ContainerRuntimeGCMessage,
223
234
  type ContainerRuntimeIdAllocationMessage,
224
235
  type InboundSequencedContainerRuntimeMessage,
225
236
  type LocalContainerRuntimeMessage,
237
+ type OutboundContainerRuntimeAttachMessage,
226
238
  type UnknownContainerRuntimeMessage,
227
239
  } from "./messageTypes.js";
228
240
  import type { ISavedOpMetadata } from "./metadata.js";
@@ -251,6 +263,7 @@ import {
251
263
  import { BatchRunCounter, RunCounter } from "./runCounter.js";
252
264
  import {
253
265
  runtimeCompatDetailsForLoader,
266
+ runtimeCoreCompatDetails,
254
267
  validateLoaderCompatibility,
255
268
  } from "./runtimeLayerCompatState.js";
256
269
  import { SignalTelemetryManager } from "./signalTelemetryProcessing.js";
@@ -311,11 +324,19 @@ import { Throttler, formExponentialFn } from "./throttler.js";
311
324
  * A {@link ContainerExtension}'s factory function as stored in extension map.
312
325
  */
313
326
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `any` required to allow typed factory to be assignable per ContainerExtension.processSignal
314
- type ExtensionEntry = ContainerExtensionFactory<unknown, any, unknown[]> extends new (
315
- ...args: any[]
316
- ) => infer T
317
- ? T
318
- : never;
327
+ type ExtensionEntry = ExtensionInstantiationResult<unknown, any, unknown[]>;
328
+
329
+ /**
330
+ * ContainerRuntime's compatibility details that is exposed to Container Extensions.
331
+ */
332
+ const containerRuntimeCompatDetailsForContainerExtensions = {
333
+ ...runtimeCoreCompatDetails,
334
+ /**
335
+ * The features supported by the ContainerRuntime's ContainerExtensionStore
336
+ * implementation.
337
+ */
338
+ supportedFeatures: new Set<string>(),
339
+ } as const satisfies ILayerCompatDetails;
319
340
 
320
341
  /**
321
342
  * Creates an error object to be thrown / passed to Container's close fn in case of an unknown message type.
@@ -817,18 +838,23 @@ export class ContainerRuntime
817
838
  extends TypedEventEmitter<IContainerRuntimeEvents>
818
839
  implements
819
840
  IContainerRuntimeInternal,
820
- // eslint-disable-next-line import/no-deprecated
821
- IContainerRuntimeBaseExperimental,
841
+ IContainerRuntimeBaseInternal,
822
842
  // eslint-disable-next-line import/no-deprecated
823
843
  IContainerRuntimeWithResolveHandle_Deprecated,
824
844
  IRuntime,
825
845
  IGarbageCollectionRuntime,
826
846
  ISummarizerRuntime,
827
847
  ISummarizerInternalsProvider,
828
- IFluidParentContext,
848
+ // If ContainerRuntime stops being exported from this package, this can
849
+ // be updated to implement IFluidRootParentContextPrivate and leave
850
+ // submitMessage included.
851
+ // IFluidParentContextPrivate is also better than IFluidParentContext
852
+ // and is also internal only; so, not usable here.
853
+ Omit<IFluidParentContext, "submitMessage" | "submitSignal">,
829
854
  IProvideFluidHandleContext,
830
855
  IProvideLayerCompatDetails
831
856
  {
857
+ /* eslint-disable @fluid-internal/fluid/no-hyphen-after-jsdoc-tag -- false positive AB#50920 */
832
858
  /**
833
859
  * Load the stores from a snapshot and returns the runtime.
834
860
  * @param params - An object housing the runtime properties.
@@ -857,6 +883,7 @@ export class ContainerRuntime
857
883
  registry: new FluidDataStoreRegistry(params.registryEntries),
858
884
  });
859
885
  }
886
+ /* eslint-enable @fluid-internal/fluid/no-hyphen-after-jsdoc-tag */
860
887
 
861
888
  /**
862
889
  * Load the stores from a snapshot and returns the runtime.
@@ -1570,6 +1597,7 @@ export class ContainerRuntime
1570
1597
  deltaManager,
1571
1598
  quorum,
1572
1599
  audience,
1600
+ signalAudience,
1573
1601
  pendingLocalState,
1574
1602
  supportedFeatures,
1575
1603
  snapshotWithContents,
@@ -1643,11 +1671,10 @@ export class ContainerRuntime
1643
1671
  message: OutboundExtensionMessage<TMessage>,
1644
1672
  ): void => {
1645
1673
  this.verifyNotClosed();
1646
- const envelope = createNewSignalEnvelope(
1647
- `/ext/${id}/${addressChain.join("/")}`,
1648
- message.type,
1649
- message.content,
1650
- );
1674
+ const envelope = {
1675
+ address: `/ext/${id}/${addressChain.join("/")}`,
1676
+ contents: message,
1677
+ } satisfies UnsequencedSignalEnvelope;
1651
1678
  sequenceAndSubmitSignal(envelope, message.targetClientId);
1652
1679
  };
1653
1680
 
@@ -1721,10 +1748,11 @@ export class ContainerRuntime
1721
1748
  this.messageAtLastSummary = lastMessageFromMetadata(metadata);
1722
1749
 
1723
1750
  // Note that we only need to pull the *initial* connected state from the context.
1724
- // Later updates come through calls to setConnectionState.
1751
+ // Later updates come through calls to setConnectionState/Status.
1725
1752
  this.canSendOps = connected;
1726
1753
  this.canSendSignals = this.getConnectionState
1727
- ? this.getConnectionState() === ConnectionState.Connected
1754
+ ? this.getConnectionState() === ConnectionState.Connected ||
1755
+ this.getConnectionState() === ConnectionState.CatchingUp
1728
1756
  : undefined;
1729
1757
 
1730
1758
  this.mc.logger.sendTelemetryEvent({
@@ -1899,18 +1927,20 @@ export class ContainerRuntime
1899
1927
  async () => this.garbageCollector.getBaseGCDetails(),
1900
1928
  );
1901
1929
 
1902
- const parentContext = wrapContext(this);
1903
-
1904
- // Due to a mismatch between different layers in terms of
1905
- // what is the interface of passing signals, we need the
1906
- // downstream stores to wrap the signal.
1907
- parentContext.submitSignal = (type: string, content: unknown, targetClientId?: string) => {
1908
- // Future: Can the `content` argument type be IEnvelope?
1909
- // verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
1910
- const envelope1 = content as IEnvelope;
1911
- const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
1912
- this.submitSignalFn(envelope2, targetClientId);
1913
- };
1930
+ const parentContext = formParentContext<IFluidRootParentContextPrivate>(this, {
1931
+ submitMessage: this.submitMessage.bind(this),
1932
+
1933
+ // Due to a mismatch between different layers in terms of
1934
+ // what is the interface of passing signals, we need the
1935
+ // downstream stores to wrap the signal.
1936
+ submitSignal: (
1937
+ envelope: AddressedUnsequencedSignalEnvelope,
1938
+ targetClientId?: string,
1939
+ ): void => {
1940
+ // verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
1941
+ this.submitSignalFn(envelope, targetClientId);
1942
+ },
1943
+ });
1914
1944
 
1915
1945
  let snapshot: ISnapshot | ISnapshotTree | undefined = getSummaryForDatastores(
1916
1946
  baseSnapshot,
@@ -1934,7 +1964,6 @@ export class ContainerRuntime
1934
1964
  }),
1935
1965
  (path: string) => this.garbageCollector.isNodeDeleted(path),
1936
1966
  new Map<string, string>(dataStoreAliasMap),
1937
- async (runtime: ChannelCollection) => provideEntryPoint,
1938
1967
  );
1939
1968
  this._deltaManager.on("readonly", this.notifyReadOnlyState);
1940
1969
 
@@ -2040,6 +2069,8 @@ export class ContainerRuntime
2040
2069
  });
2041
2070
  }
2042
2071
 
2072
+ this.signalAudience = signalAudience;
2073
+
2043
2074
  const closeSummarizerDelayOverride = this.mc.config.getNumber(
2044
2075
  "Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs",
2045
2076
  );
@@ -2812,6 +2843,59 @@ export class ContainerRuntime
2812
2843
  this.channelCollection.notifyReadOnlyState(readonly);
2813
2844
 
2814
2845
  public setConnectionState(canSendOps: boolean, clientId?: string): void {
2846
+ this.setConnectionStateToConnectedOrDisconnected(canSendOps, clientId);
2847
+ }
2848
+
2849
+ public setConnectionStatus(status: ConnectionStatus): void {
2850
+ switch (status.connectionState) {
2851
+ case ConnectionState.Connected: {
2852
+ this.setConnectionStateToConnectedOrDisconnected(
2853
+ status.canSendOps,
2854
+ status.clientConnectionId,
2855
+ );
2856
+
2857
+ break;
2858
+ }
2859
+ case ConnectionState.Disconnected: {
2860
+ this.setConnectionStateToConnectedOrDisconnected(
2861
+ status.canSendOps,
2862
+ status.priorConnectedClientConnectionId,
2863
+ );
2864
+
2865
+ break;
2866
+ }
2867
+ case ConnectionState.CatchingUp: {
2868
+ assert(
2869
+ this.getConnectionState !== undefined &&
2870
+ this.getConnectionState() === ConnectionState.CatchingUp,
2871
+ 0xc8d /* connection state mismatch between getConnectionState and setConnectionStatus notification */,
2872
+ );
2873
+
2874
+ // Note: Historically when only `setConnectionState` of `IRuntime`
2875
+ // was supported, it was possible to be in `CatchingUp` state and
2876
+ // call through to `setConnectionStateCore` when there is a readonly
2877
+ // change - see `Container`'s `"deltaManager.on("readonly"`. There
2878
+ // would not be a transition of `canSendOps` in that case, but
2879
+ // `channelCollection` and `garbageCollector` would receive early
2880
+ // `setConnectionState` call AND `this` would `emit` "disconnected"
2881
+ // event.
2882
+
2883
+ this.emitServiceConnectionEvents(
2884
+ /* canSendOpsChanged */ this.canSendOps,
2885
+ /* canSendOps */ false,
2886
+ status.pendingClientConnectionId,
2887
+ );
2888
+
2889
+ break;
2890
+ }
2891
+ // No default
2892
+ }
2893
+ }
2894
+
2895
+ private setConnectionStateToConnectedOrDisconnected(
2896
+ canSendOps: boolean,
2897
+ clientId: string | undefined,
2898
+ ): void {
2815
2899
  // Validate we have consistent state
2816
2900
  const currentClientId = this._audience.getSelf()?.clientId;
2817
2901
  assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
@@ -2896,7 +2980,7 @@ export class ContainerRuntime
2896
2980
  * Emits service connection events based on connection state changes.
2897
2981
  *
2898
2982
  * @remarks
2899
- * "connectedToService" is emitted when container connection state transitions to 'Connected' regardless of connection mode.
2983
+ * "connectedToService" is emitted when container connection state transitions to 'CatchingUp' or 'Connected' regardless of connection mode.
2900
2984
  * "disconnectedFromService" excludes false "disconnected" events that happen when readonly client transitions to 'Connected'.
2901
2985
  */
2902
2986
  private emitServiceConnectionEvents(
@@ -2908,21 +2992,25 @@ export class ContainerRuntime
2908
2992
  return;
2909
2993
  }
2910
2994
 
2911
- const canSendSignals = this.getConnectionState() === ConnectionState.Connected;
2995
+ const connectionState = this.getConnectionState();
2996
+ const canSendSignals =
2997
+ connectionState === ConnectionState.Connected ||
2998
+ connectionState === ConnectionState.CatchingUp;
2912
2999
  const canSendSignalsChanged = this.canSendSignals !== canSendSignals;
2913
3000
  this.canSendSignals = canSendSignals;
2914
3001
  if (canSendSignalsChanged) {
2915
- // If canSendSignals changed, we either transitioned from Connected to Disconnected or CatchingUp to Connected
3002
+ // If canSendSignals changed, we either transitioned from CatchingUp or
3003
+ // Connected to Disconnected or EstablishingConnection to CatchingUp.
2916
3004
  if (canSendSignals) {
2917
- // Emit for CatchingUp to Connected transition
3005
+ // Emit for EstablishingConnection to CatchingUp or Connected transition
2918
3006
  this.emit("connectedToService", clientId, canSendOps);
2919
3007
  } else {
2920
- // Emit for Connected to Disconnected transition
3008
+ // Emit for CatchingUp or Connected to Disconnected transition
2921
3009
  this.emit("disconnectedFromService");
2922
3010
  }
2923
3011
  } else if (canSendOpsChanged) {
2924
- // If canSendSignals did not change but canSendOps did, then connection type has changed.
2925
- this.emit("connectionTypeChanged", canSendOps);
3012
+ // If canSendSignals did not change but canSendOps did, then operations possible has changed.
3013
+ this.emit("operabilityChanged", canSendOps);
2926
3014
  }
2927
3015
  }
2928
3016
 
@@ -3455,8 +3543,7 @@ export class ContainerRuntime
3455
3543
  */
3456
3544
  public orderSequentially<T>(callback: () => T): T {
3457
3545
  let checkpoint: IBatchCheckpoint | undefined;
3458
- // eslint-disable-next-line import/no-deprecated
3459
- let stageControls: StageControlsExperimental | undefined;
3546
+ let stageControls: StageControlsInternal | undefined;
3460
3547
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback") === true) {
3461
3548
  if (!this.batchRunner.running && !this.inStagingMode) {
3462
3549
  stageControls = this.enterStagingMode();
@@ -3520,8 +3607,7 @@ export class ContainerRuntime
3520
3607
  return result;
3521
3608
  }
3522
3609
 
3523
- // eslint-disable-next-line import/no-deprecated
3524
- private stageControls: StageControlsExperimental | undefined;
3610
+ private stageControls: StageControlsInternal | undefined;
3525
3611
 
3526
3612
  /**
3527
3613
  * If true, the ContainerRuntime is not submitting any new ops to the ordering service.
@@ -3536,10 +3622,9 @@ export class ContainerRuntime
3536
3622
  * Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
3537
3623
  * To exit Staging Mode, call either discardChanges or commitChanges on the Stage Controls returned from this method.
3538
3624
  *
3539
- * @returns StageControlsExperimental - Controls for exiting Staging Mode.
3625
+ * @returns Controls for exiting Staging Mode.
3540
3626
  */
3541
- // eslint-disable-next-line import/no-deprecated
3542
- public enterStagingMode = (): StageControlsExperimental => {
3627
+ public enterStagingMode = (): StageControlsInternal => {
3543
3628
  if (this.stageControls !== undefined) {
3544
3629
  throw new UsageError("Already in staging mode");
3545
3630
  }
@@ -3572,8 +3657,7 @@ export class ContainerRuntime
3572
3657
  }
3573
3658
  };
3574
3659
 
3575
- // eslint-disable-next-line import/no-deprecated
3576
- const stageControls: StageControlsExperimental = {
3660
+ const stageControls: StageControlsInternal = {
3577
3661
  discardChanges: () =>
3578
3662
  exitStagingMode(() => {
3579
3663
  // Pop all staged batches from the PSM and roll them back in LIFO order
@@ -3680,6 +3764,14 @@ export class ContainerRuntime
3680
3764
  return this._audience;
3681
3765
  }
3682
3766
 
3767
+ /**
3768
+ * When defined, this {@link @fluidframework/container-definitions#IAudience}
3769
+ * maintains member list using signals only.
3770
+ * Thus "write" members may be known earlier than quorum and avoid noise from
3771
+ * un-summarized quorum history.
3772
+ */
3773
+ private readonly signalAudience?: IAudience;
3774
+
3683
3775
  /**
3684
3776
  * Returns true of container is dirty, i.e. there are some pending local changes that
3685
3777
  * either were not sent out to delta stream or were not yet acknowledged.
@@ -3715,7 +3807,9 @@ export class ContainerRuntime
3715
3807
  */
3716
3808
  public submitSignal(type: string, content: unknown, targetClientId?: string): void {
3717
3809
  this.verifyNotClosed();
3718
- const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
3810
+ const envelope = {
3811
+ contents: { type, content },
3812
+ } satisfies UnsequencedSignalEnvelope;
3719
3813
  this.submitSignalFn(envelope, targetClientId);
3720
3814
  }
3721
3815
 
@@ -4541,17 +4635,15 @@ export class ContainerRuntime
4541
4635
  }
4542
4636
  }
4543
4637
 
4638
+ // Keep in sync with IFluidRootParentContextPrivate.submitMessage.
4544
4639
  public submitMessage(
4545
- type:
4546
- | ContainerMessageType.FluidDataStoreOp
4547
- | ContainerMessageType.Alias
4548
- | ContainerMessageType.Attach,
4549
- // TODO: better typing
4550
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
4551
- contents: any,
4640
+ containerRuntimeMessage:
4641
+ | ContainerRuntimeDataStoreOpMessage
4642
+ | OutboundContainerRuntimeAttachMessage
4643
+ | ContainerRuntimeAliasMessage,
4552
4644
  localOpMetadata: unknown = undefined,
4553
4645
  ): void {
4554
- this.submit({ type, contents }, localOpMetadata);
4646
+ this.submit(containerRuntimeMessage, localOpMetadata);
4555
4647
  }
4556
4648
 
4557
4649
  public async uploadBlob(
@@ -4822,9 +4914,8 @@ export class ContainerRuntime
4822
4914
  );
4823
4915
  switch (message.type) {
4824
4916
  case ContainerMessageType.FluidDataStoreOp: {
4825
- this.channelCollection.reSubmit(
4826
- message.type,
4827
- message.contents,
4917
+ this.channelCollection.reSubmitContainerMessage(
4918
+ message,
4828
4919
  resubmitData.localOpMetadata,
4829
4920
  /* squash: */ true,
4830
4921
  );
@@ -4856,11 +4947,10 @@ export class ContainerRuntime
4856
4947
  case ContainerMessageType.FluidDataStoreOp:
4857
4948
  case ContainerMessageType.Attach:
4858
4949
  case ContainerMessageType.Alias: {
4859
- // For Operations, call resubmitDataStoreOp which will find the right store
4950
+ // Call reSubmitContainerMessage which will find the right store
4860
4951
  // and trigger resubmission on it.
4861
- this.channelCollection.reSubmit(
4862
- message.type,
4863
- message.contents,
4952
+ this.channelCollection.reSubmitContainerMessage(
4953
+ message,
4864
4954
  localOpMetadata,
4865
4955
  /* squash: */ false,
4866
4956
  );
@@ -4915,7 +5005,7 @@ export class ContainerRuntime
4915
5005
  case ContainerMessageType.FluidDataStoreOp: {
4916
5006
  // For operations, call rollbackDataStoreOp which will find the right store
4917
5007
  // and trigger rollback on it.
4918
- this.channelCollection.rollback(type, contents, localOpMetadata);
5008
+ this.channelCollection.rollbackDataStoreOp(contents, localOpMetadata);
4919
5009
  break;
4920
5010
  }
4921
5011
  case ContainerMessageType.GC: {
@@ -5208,8 +5298,8 @@ export class ContainerRuntime
5208
5298
  eventEmitter.emit("joined", { clientId, canWrite });
5209
5299
  });
5210
5300
  this.on("disconnectedFromService", () => eventEmitter.emit("disconnected"));
5211
- this.on("connectionTypeChanged", (canWrite: boolean) =>
5212
- eventEmitter.emit("connectionTypeChanged", canWrite),
5301
+ this.on("operabilityChanged", (canWrite: boolean) =>
5302
+ eventEmitter.emit("operabilityChanged", canWrite),
5213
5303
  );
5214
5304
  } else {
5215
5305
  this.on("connected", (clientId: string) => {
@@ -5224,7 +5314,11 @@ export class ContainerRuntime
5224
5314
  const getConnectionState = this.getConnectionState;
5225
5315
  if (getConnectionState) {
5226
5316
  const connectionState = getConnectionState();
5227
- if (connectionState === ConnectionState.Connected) {
5317
+ if (
5318
+ connectionState === ConnectionState.Connected ||
5319
+ connectionState === ConnectionState.CatchingUp
5320
+ ) {
5321
+ // Note: when CatchingUp, canSendOps will always be false.
5228
5322
  return this.canSendOps ? "joinedForWriting" : "joinedForReading";
5229
5323
  }
5230
5324
  } else if (this.canSendOps) {
@@ -5248,11 +5342,71 @@ export class ContainerRuntime
5248
5342
  factory: ContainerExtensionFactory<T, TRuntimeProperties, TUseContext>,
5249
5343
  ...useContext: TUseContext
5250
5344
  ): T {
5345
+ return this.acquireExtensionInternal(
5346
+ /* injectionPermitted */ true,
5347
+ id,
5348
+ factory,
5349
+ ...useContext,
5350
+ );
5351
+ }
5352
+
5353
+ public getExtension<
5354
+ T,
5355
+ TRuntimeProperties extends ExtensionRuntimeProperties,
5356
+ TUseContext extends unknown[],
5357
+ >(
5358
+ id: ContainerExtensionId,
5359
+ requirements: ContainerExtensionExpectations,
5360
+ ...useContext: TUseContext
5361
+ ): T {
5362
+ // Temporarily allow injection for extensions.
5363
+ // `requirements` are expected to be a factory as well.
5364
+ return this.acquireExtensionInternal(
5365
+ /* injectionPermitted */ true,
5366
+ id,
5367
+ requirements as ContainerExtensionFactory<T, TRuntimeProperties, TUseContext>,
5368
+ ...useContext,
5369
+ );
5370
+ }
5371
+
5372
+ private acquireExtensionInternal<
5373
+ T,
5374
+ TRuntimeProperties extends ExtensionRuntimeProperties,
5375
+ TUseContext extends unknown[],
5376
+ >(
5377
+ injectionPermitted: boolean,
5378
+ id: ContainerExtensionId,
5379
+ factory: ContainerExtensionFactory<T, TRuntimeProperties, TUseContext>,
5380
+ ...useContext: TUseContext
5381
+ ): T {
5382
+ const compatCheckResult = checkLayerCompatibility(
5383
+ factory.hostRequirements,
5384
+ containerRuntimeCompatDetailsForContainerExtensions,
5385
+ );
5386
+ if (!compatCheckResult.isCompatible) {
5387
+ throw new UsageError("Extension is not compatible with ContainerRuntime", {
5388
+ errorDetails: JSON.stringify({
5389
+ containerRuntimeVersion:
5390
+ containerRuntimeCompatDetailsForContainerExtensions.pkgVersion,
5391
+ containerRuntimeGeneration:
5392
+ containerRuntimeCompatDetailsForContainerExtensions.generation,
5393
+ minSupportedGeneration: factory.hostRequirements.minSupportedGeneration,
5394
+ isGenerationCompatible: compatCheckResult.isGenerationCompatible,
5395
+ unsupportedFeatures: compatCheckResult.unsupportedFeatures,
5396
+ }),
5397
+ });
5398
+ }
5399
+
5251
5400
  let entry = this.extensions.get(id);
5252
5401
  if (entry === undefined) {
5402
+ if (!injectionPermitted) {
5403
+ throw new Error(`Extension ${id} not found`);
5404
+ }
5405
+
5406
+ const audience = this.signalAudience;
5253
5407
  const runtime = {
5254
5408
  getJoinedStatus: this.getJoinedStatus.bind(this),
5255
- getClientId: () => this.clientId,
5409
+ getClientId: audience ? () => audience.getSelf()?.clientId : () => this.clientId,
5256
5410
  events: this.lazyEventsForExtensions.value,
5257
5411
  logger: this.baseLogger,
5258
5412
  submitAddressedSignal: (
@@ -5262,16 +5416,45 @@ export class ContainerRuntime
5262
5416
  this.submitExtensionSignal(id, addressChain, message);
5263
5417
  },
5264
5418
  getQuorum: this.getQuorum.bind(this),
5265
- getAudience: this.getAudience.bind(this),
5419
+ getAudience: audience ? () => audience : this.getAudience.bind(this),
5266
5420
  supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
5267
5421
  } satisfies ExtensionHost<TRuntimeProperties>;
5268
- entry = new factory(runtime, ...useContext);
5422
+ entry = factory.instantiateExtension(runtime, ...useContext);
5269
5423
  this.extensions.set(id, entry);
5270
5424
  } else {
5271
- assert(
5272
- entry instanceof factory,
5273
- 0xba1 /* Extension entry is not of the expected type */,
5274
- );
5425
+ const { extension, compatibility } = entry;
5426
+ if (
5427
+ // Check short-circuit (re-use) for same instance which must be
5428
+ // same version and capabilities.
5429
+ !(entry instanceof factory) &&
5430
+ // Check version and capabilities if different instance. If
5431
+ // version matches and existing has all capabilities of
5432
+ // requested, then allow direct reuse.
5433
+ (compatibility.version !== factory.instanceExpectations.version ||
5434
+ [...factory.instanceExpectations.capabilities].some(
5435
+ (cap) => !compatibility.capabilities.has(cap),
5436
+ ))
5437
+ ) {
5438
+ // eslint-disable-next-line unicorn/prefer-ternary -- operations are significant and deserve own blocks
5439
+ if (
5440
+ !injectionPermitted ||
5441
+ gt(compatibility.version, factory.instanceExpectations.version)
5442
+ ) {
5443
+ // This is an attempt to acquire an older version of an
5444
+ // extension that is already acquired OR updating (form of
5445
+ // injection) is not permitted.
5446
+ entry = extension.handleVersionOrCapabilitiesMismatch(
5447
+ entry,
5448
+ factory.instanceExpectations,
5449
+ );
5450
+ } else {
5451
+ // This is an attempt to acquire a newer or more capable
5452
+ // version of an extension that is already acquired. Replace
5453
+ // existing with new.
5454
+ entry = factory.resolvePriorInstantiation(entry);
5455
+ }
5456
+ }
5457
+ // eslint-disable-next-line unicorn/consistent-destructuring -- 'entry' may have been update and thus use of 'extension' would be incorrect
5275
5458
  entry.extension.onNewUse(...useContext);
5276
5459
  }
5277
5460
  return entry.interface as T;
@@ -5282,19 +5465,6 @@ export class ContainerRuntime
5282
5465
  }
5283
5466
  }
5284
5467
 
5285
- export function createNewSignalEnvelope(
5286
- address: string | undefined,
5287
- type: string,
5288
- content: unknown,
5289
- ): UnsequencedSignalEnvelope {
5290
- const newEnvelope: UnsequencedSignalEnvelope = {
5291
- address,
5292
- contents: { type, content },
5293
- };
5294
-
5295
- return newEnvelope;
5296
- }
5297
-
5298
5468
  export function isContainerMessageDirtyable({
5299
5469
  type,
5300
5470
  contents,
package/src/dataStore.ts CHANGED
@@ -7,12 +7,11 @@ import { AttachState } from "@fluidframework/container-definitions";
7
7
  import type { FluidObject } from "@fluidframework/core-interfaces";
8
8
  import type { IFluidHandleInternal } from "@fluidframework/core-interfaces/internal";
9
9
  import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
10
- import type {
11
- AliasResult,
12
- IDataStore,
13
- IFluidDataStoreChannel,
14
- // eslint-disable-next-line import/no-deprecated
15
- IContainerRuntimeBaseExperimental,
10
+ import {
11
+ type AliasResult,
12
+ type IDataStore,
13
+ type IFluidDataStoreChannel,
14
+ asLegacyAlpha,
16
15
  } from "@fluidframework/runtime-definitions/internal";
17
16
  import {
18
17
  type ITelemetryLoggerExt,
@@ -26,6 +25,8 @@ import { ContainerMessageType } from "./messageTypes.js";
26
25
  /**
27
26
  * Interface for an op to be used for assigning an
28
27
  * alias to a datastore
28
+ * @internal
29
+ * @privateRemarks exported per ContainerRuntime export for testing purposes
29
30
  */
30
31
  export interface IDataStoreAliasMessage {
31
32
  /**
@@ -80,9 +81,7 @@ class DataStore implements IDataStore {
80
81
  if (alias.includes("/")) {
81
82
  throw new UsageError(`The alias cannot contain slashes: '${alias}'`);
82
83
  }
83
- // eslint-disable-next-line import/no-deprecated
84
- const runtime = this.parentContext.containerRuntime as IContainerRuntimeBaseExperimental;
85
- if (runtime.inStagingMode === true) {
84
+ if (asLegacyAlpha(this.parentContext.containerRuntime).inStagingMode === true) {
86
85
  throw new UsageError("Cannot set aliases while in staging mode");
87
86
  }
88
87
 
@@ -147,7 +146,10 @@ class DataStore implements IDataStore {
147
146
  }
148
147
 
149
148
  const aliased = await this.ackBasedPromise<boolean>((resolve) => {
150
- this.parentContext.submitMessage(ContainerMessageType.Alias, message, resolve);
149
+ this.parentContext.submitMessage(
150
+ { type: ContainerMessageType.Alias, contents: message },
151
+ resolve,
152
+ );
151
153
  })
152
154
  .catch((error) => {
153
155
  this.logger.sendErrorEvent(