@fluidframework/container-runtime 2.51.0 → 2.52.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 (94) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +1 -2
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/blobManager/blobManager.d.ts +15 -7
  5. package/dist/blobManager/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager/blobManager.js +72 -186
  7. package/dist/blobManager/blobManager.js.map +1 -1
  8. package/dist/containerCompatibility.d.ts +34 -0
  9. package/dist/containerCompatibility.d.ts.map +1 -0
  10. package/dist/containerCompatibility.js +125 -0
  11. package/dist/containerCompatibility.js.map +1 -0
  12. package/dist/containerRuntime.d.ts +27 -15
  13. package/dist/containerRuntime.d.ts.map +1 -1
  14. package/dist/containerRuntime.js +175 -136
  15. package/dist/containerRuntime.js.map +1 -1
  16. package/dist/dataStoreContext.d.ts +6 -6
  17. package/dist/dataStoreContext.d.ts.map +1 -1
  18. package/dist/dataStoreContext.js.map +1 -1
  19. package/dist/index.d.ts +5 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/metadata.d.ts +3 -2
  23. package/dist/metadata.d.ts.map +1 -1
  24. package/dist/metadata.js +7 -1
  25. package/dist/metadata.js.map +1 -1
  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/storageServiceWithAttachBlobs.d.ts +40 -5
  30. package/dist/storageServiceWithAttachBlobs.d.ts.map +1 -1
  31. package/dist/storageServiceWithAttachBlobs.js +56 -5
  32. package/dist/storageServiceWithAttachBlobs.js.map +1 -1
  33. package/dist/summary/documentSchema.d.ts +1 -1
  34. package/dist/summary/documentSchema.d.ts.map +1 -1
  35. package/dist/summary/documentSchema.js.map +1 -1
  36. package/dist/summary/summaryFormat.d.ts +3 -3
  37. package/dist/summary/summaryFormat.d.ts.map +1 -1
  38. package/dist/summary/summaryFormat.js.map +1 -1
  39. package/lib/blobManager/blobManager.d.ts +15 -7
  40. package/lib/blobManager/blobManager.d.ts.map +1 -1
  41. package/lib/blobManager/blobManager.js +39 -153
  42. package/lib/blobManager/blobManager.js.map +1 -1
  43. package/lib/containerCompatibility.d.ts +34 -0
  44. package/lib/containerCompatibility.d.ts.map +1 -0
  45. package/lib/containerCompatibility.js +120 -0
  46. package/lib/containerCompatibility.js.map +1 -0
  47. package/lib/containerRuntime.d.ts +27 -15
  48. package/lib/containerRuntime.d.ts.map +1 -1
  49. package/lib/containerRuntime.js +103 -64
  50. package/lib/containerRuntime.js.map +1 -1
  51. package/lib/dataStoreContext.d.ts +6 -6
  52. package/lib/dataStoreContext.d.ts.map +1 -1
  53. package/lib/dataStoreContext.js +1 -1
  54. package/lib/dataStoreContext.js.map +1 -1
  55. package/lib/index.d.ts +5 -1
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js.map +1 -1
  58. package/lib/metadata.d.ts +3 -2
  59. package/lib/metadata.d.ts.map +1 -1
  60. package/lib/metadata.js +5 -0
  61. package/lib/metadata.js.map +1 -1
  62. package/lib/packageVersion.d.ts +1 -1
  63. package/lib/packageVersion.js +1 -1
  64. package/lib/packageVersion.js.map +1 -1
  65. package/lib/storageServiceWithAttachBlobs.d.ts +40 -5
  66. package/lib/storageServiceWithAttachBlobs.d.ts.map +1 -1
  67. package/lib/storageServiceWithAttachBlobs.js +56 -5
  68. package/lib/storageServiceWithAttachBlobs.js.map +1 -1
  69. package/lib/summary/documentSchema.d.ts +1 -1
  70. package/lib/summary/documentSchema.d.ts.map +1 -1
  71. package/lib/summary/documentSchema.js.map +1 -1
  72. package/lib/summary/summaryFormat.d.ts +3 -3
  73. package/lib/summary/summaryFormat.d.ts.map +1 -1
  74. package/lib/summary/summaryFormat.js.map +1 -1
  75. package/package.json +20 -20
  76. package/src/blobManager/blobManager.ts +53 -195
  77. package/src/containerCompatibility.ts +176 -0
  78. package/src/containerRuntime.ts +157 -122
  79. package/src/dataStoreContext.ts +13 -5
  80. package/src/index.ts +6 -1
  81. package/src/metadata.ts +10 -2
  82. package/src/packageVersion.ts +1 -1
  83. package/src/storageServiceWithAttachBlobs.ts +92 -10
  84. package/src/summary/documentSchema.ts +1 -1
  85. package/src/summary/summaryFormat.ts +2 -2
  86. package/dist/compatUtils.d.ts +0 -106
  87. package/dist/compatUtils.d.ts.map +0 -1
  88. package/dist/compatUtils.js +0 -251
  89. package/dist/compatUtils.js.map +0 -1
  90. package/lib/compatUtils.d.ts +0 -106
  91. package/lib/compatUtils.d.ts.map +0 -1
  92. package/lib/compatUtils.js +0 -242
  93. package/lib/compatUtils.js.map +0 -1
  94. package/src/compatUtils.ts +0 -365
@@ -0,0 +1,176 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import {
7
+ FlushMode,
8
+ type MinimumVersionForCollab,
9
+ } from "@fluidframework/runtime-definitions/internal";
10
+ import {
11
+ configValueToMinVersionForCollab,
12
+ getConfigsForMinVersionForCollab,
13
+ getValidationForRuntimeOptions,
14
+ type ConfigMap,
15
+ type ConfigValidationMap,
16
+ } from "@fluidframework/runtime-utils/internal";
17
+
18
+ import {
19
+ disabledCompressionConfig,
20
+ enabledCompressionConfig,
21
+ } from "./compressionDefinitions.js";
22
+ import type { ContainerRuntimeOptionsInternal } from "./containerRuntime.js";
23
+
24
+ /**
25
+ * Subset of the {@link ContainerRuntimeOptionsInternal} properties which
26
+ * affect {@link IDocumentSchemaFeatures}.
27
+ *
28
+ * @remarks
29
+ * When a new option is added to {@link ContainerRuntimeOptionsInternal}, we
30
+ * must consider if it changes the DocumentSchema. If so, then a corresponding
31
+ * entry must be added to {@link runtimeOptionsAffectingDocSchemaConfigMap}
32
+ * below. If not, then it must be omitted from this type.
33
+ *
34
+ * Note: `Omit` is used instead of `Pick` to ensure that all new options are
35
+ * included in this type by default. If any new properties are added to
36
+ * {@link ContainerRuntimeOptionsInternal}, they will be included in this
37
+ * type unless explicitly omitted. This will prevent us from forgetting to
38
+ * account for any new properties in the future.
39
+ */
40
+ export type RuntimeOptionsAffectingDocSchema = Omit<
41
+ ContainerRuntimeOptionsInternal,
42
+ | "chunkSizeInBytes"
43
+ | "maxBatchSizeInBytes"
44
+ | "loadSequenceNumberVerification"
45
+ | "summaryOptions"
46
+ >;
47
+
48
+ /**
49
+ * Mapping of RuntimeOptionsAffectingDocSchema to their compatibility related configs.
50
+ *
51
+ * Each key in this map corresponds to a property in RuntimeOptionsAffectingDocSchema. The value is an object that maps MinimumVersionForCollab
52
+ * to the appropriate default value for that property to supporting that MinimumVersionForCollab. If clients running MinimumVersionForCollab X are able to understand
53
+ * the format changes introduced by the property, then the default value for that MinimumVersionForCollab will enable the feature associated with the property.
54
+ * Otherwise, the feature will be disabled.
55
+ *
56
+ * For example if the minVersionForCollab is a 1.x version (i.e. "1.5.0"), then the default value for `enableGroupedBatching` will be false since 1.x
57
+ * clients do not understand the document format when batching is enabled. If the minVersionForCollab is a 2.x client (i.e. "2.0.0" or later), then the
58
+ * default value for `enableGroupedBatching` will be true because clients running 2.0 or later will be able to understand the format changes associated
59
+ * with the batching feature.
60
+ */
61
+ const runtimeOptionsAffectingDocSchemaConfigMap = {
62
+ enableGroupedBatching: {
63
+ "1.0.0": false,
64
+ "2.0.0-defaults": true,
65
+ },
66
+ compressionOptions: {
67
+ "1.0.0": disabledCompressionConfig,
68
+ "2.0.0-defaults": enabledCompressionConfig,
69
+ },
70
+ enableRuntimeIdCompressor: {
71
+ // For IdCompressorMode, `undefined` represents a logical state (off).
72
+ // However, to satisfy the Required<> constraint while
73
+ // `exactOptionalPropertyTypes` is `false` (TODO: AB#8215), we need
74
+ // to have it defined, so we trick the type checker here.
75
+ "1.0.0": undefined,
76
+ // We do not yet want to enable idCompressor by default since it will
77
+ // increase bundle sizes, and not all customers will benefit from it.
78
+ // Therefore, we will require customers to explicitly enable it. We
79
+ // are keeping it as a DocSchema affecting option for now as this may
80
+ // change in the future.
81
+ },
82
+ explicitSchemaControl: {
83
+ "1.0.0": false,
84
+ // This option's intention is to prevent 1.x clients from joining sessions
85
+ // when enabled. This is set to true when the minVersionForCollab is set
86
+ // to >=2.0.0 (explicitly). This is different than other 2.0 defaults
87
+ // because it was not enabled by default prior to the implementation of
88
+ // `minVersionForCollab`.
89
+ // `defaultMinVersionForCollab` is set to "2.0.0-defaults" which "2.0.0"
90
+ // does not satisfy to avoiding enabling this option by default as of
91
+ // `minVersionForCollab` introduction, which could be unexpected.
92
+ // Only enable as a default when `minVersionForCollab` is specified at
93
+ // 2.0.0+.
94
+ "2.0.0": true,
95
+ },
96
+ flushMode: {
97
+ // Note: 1.x clients are compatible with TurnBased flushing, but here we elect to remain on Immediate flush mode
98
+ // as a work-around for inability to send batches larger than 1Mb. Immediate flushing keeps batches smaller as
99
+ // fewer messages will be included per flush.
100
+ "1.0.0": FlushMode.Immediate,
101
+ "2.0.0-defaults": FlushMode.TurnBased,
102
+ },
103
+ gcOptions: {
104
+ "1.0.0": {},
105
+ // Although sweep is supported in 2.x, it is disabled by default until minVersionForCollab>=3.0.0 to be extra safe.
106
+ "3.0.0": { enableGCSweep: true },
107
+ },
108
+ createBlobPayloadPending: {
109
+ // This feature is new and disabled by default. In the future we will enable it by default, but we have not
110
+ // closed on the version where that will happen yet. Probably a .10 release since blob functionality is not
111
+ // exposed on the `@public` API surface.
112
+ "1.0.0": undefined,
113
+ },
114
+ } as const satisfies ConfigMap<RuntimeOptionsAffectingDocSchema>;
115
+
116
+ const runtimeOptionsAffectingDocSchemaConfigValidationMap = {
117
+ enableGroupedBatching: configValueToMinVersionForCollab([
118
+ [false, "1.0.0"],
119
+ [true, "2.0.0-defaults"],
120
+ ]),
121
+ compressionOptions: configValueToMinVersionForCollab([
122
+ [{ ...disabledCompressionConfig }, "1.0.0"],
123
+ [{ ...enabledCompressionConfig }, "2.0.0-defaults"],
124
+ ]),
125
+ enableRuntimeIdCompressor: configValueToMinVersionForCollab([
126
+ [undefined, "1.0.0"],
127
+ ["on", "2.0.0-defaults"],
128
+ ["delayed", "2.0.0-defaults"],
129
+ ]),
130
+ explicitSchemaControl: configValueToMinVersionForCollab([
131
+ [false, "1.0.0"],
132
+ [true, "2.0.0-defaults"],
133
+ ]),
134
+ flushMode: configValueToMinVersionForCollab([
135
+ [FlushMode.Immediate, "1.0.0"],
136
+ [FlushMode.TurnBased, "2.0.0-defaults"],
137
+ ]),
138
+ gcOptions: configValueToMinVersionForCollab([
139
+ [{ enableGCSweep: undefined }, "1.0.0"],
140
+ [{ enableGCSweep: true }, "2.0.0-defaults"],
141
+ ]),
142
+ createBlobPayloadPending: configValueToMinVersionForCollab([
143
+ [undefined, "1.0.0"],
144
+ [true, "2.40.0"],
145
+ ]),
146
+ } as const satisfies ConfigValidationMap<RuntimeOptionsAffectingDocSchema>;
147
+
148
+ /**
149
+ * Returns the default RuntimeOptionsAffectingDocSchema configuration for a given minVersionForCollab.
150
+ */
151
+ export function getMinVersionForCollabDefaults(
152
+ minVersionForCollab: MinimumVersionForCollab,
153
+ ): RuntimeOptionsAffectingDocSchema {
154
+ return getConfigsForMinVersionForCollab(
155
+ minVersionForCollab,
156
+ runtimeOptionsAffectingDocSchemaConfigMap,
157
+ // This is a bad cast away from Partial that getConfigsForCompatMode provides.
158
+ // ConfigMap should be restructured to provide RuntimeOptionsAffectingDocSchema guarantee.
159
+ ) as RuntimeOptionsAffectingDocSchema;
160
+ }
161
+
162
+ /**
163
+ * Validates if the runtime options passed in from the user are compatible with the minVersionForCollab.
164
+ * For example, if a user sets the `enableGroupedBatching` option to true, but the minVersionForCollab
165
+ * is set to "1.0.0", then we should throw a UsageError since 1.x clients do not support batching.
166
+ * */
167
+ export function validateRuntimeOptions(
168
+ minVersionForCollab: MinimumVersionForCollab,
169
+ runtimeOptions: Partial<ContainerRuntimeOptionsInternal>,
170
+ ): void {
171
+ getValidationForRuntimeOptions<RuntimeOptionsAffectingDocSchema>(
172
+ minVersionForCollab,
173
+ runtimeOptions as Partial<RuntimeOptionsAffectingDocSchema>,
174
+ runtimeOptionsAffectingDocSchemaConfigValidationMap,
175
+ );
176
+ }
@@ -22,8 +22,12 @@ import type {
22
22
  IDeltaManager,
23
23
  IDeltaManagerFull,
24
24
  ILoader,
25
+ IContainerStorageService,
26
+ } from "@fluidframework/container-definitions/internal";
27
+ import {
28
+ ConnectionState,
29
+ isIDeltaManagerFull,
25
30
  } from "@fluidframework/container-definitions/internal";
26
- import { isIDeltaManagerFull } from "@fluidframework/container-definitions/internal";
27
31
  import type {
28
32
  ContainerExtensionFactory,
29
33
  ContainerExtensionId,
@@ -35,7 +39,9 @@ import type {
35
39
  IContainerRuntimeInternal,
36
40
  // eslint-disable-next-line import/no-deprecated
37
41
  IContainerRuntimeWithResolveHandle_Deprecated,
42
+ JoinedStatus,
38
43
  OutboundExtensionMessage,
44
+ UnverifiedBrand,
39
45
  } from "@fluidframework/container-runtime-definitions/internal";
40
46
  import type {
41
47
  FluidObject,
@@ -46,12 +52,11 @@ import type {
46
52
  Listenable,
47
53
  } from "@fluidframework/core-interfaces";
48
54
  import type {
49
- IErrorBase,
50
55
  IFluidHandleContext,
51
56
  IFluidHandleInternal,
52
57
  IProvideFluidHandleContext,
53
58
  ISignalEnvelope,
54
- JsonDeserialized,
59
+ OpaqueJsonDeserialized,
55
60
  TypedMessage,
56
61
  } from "@fluidframework/core-interfaces/internal";
57
62
  import {
@@ -71,7 +76,6 @@ import type {
71
76
  } from "@fluidframework/driver-definitions";
72
77
  import { SummaryType } from "@fluidframework/driver-definitions";
73
78
  import type {
74
- IDocumentStorageService,
75
79
  IDocumentMessage,
76
80
  ISequencedDocumentMessage,
77
81
  ISignalMessage,
@@ -121,7 +125,13 @@ import {
121
125
  FlushModeExperimental,
122
126
  channelsTreeName,
123
127
  gcTreeKey,
128
+ type MinimumVersionForCollab,
124
129
  } from "@fluidframework/runtime-definitions/internal";
130
+ import {
131
+ defaultMinVersionForCollab,
132
+ isValidMinVersionForCollab,
133
+ type SemanticVersion,
134
+ } from "@fluidframework/runtime-utils/internal";
125
135
  import {
126
136
  GCDataBuilder,
127
137
  RequestParser,
@@ -178,18 +188,14 @@ import {
178
188
  getSummaryForDatastores,
179
189
  wrapContext,
180
190
  } from "./channelCollection.js";
191
+ import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
192
+ import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
193
+ import { ReportOpPerfTelemetry } from "./connectionTelemetry.js";
181
194
  import {
182
- defaultMinVersionForCollab,
183
195
  getMinVersionForCollabDefaults,
184
- isValidMinVersionForCollab,
185
196
  type RuntimeOptionsAffectingDocSchema,
186
- type MinimumVersionForCollab,
187
- type SemanticVersion,
188
197
  validateRuntimeOptions,
189
- } from "./compatUtils.js";
190
- import type { ICompressionRuntimeOptions } from "./compressionDefinitions.js";
191
- import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
192
- import { ReportOpPerfTelemetry } from "./connectionTelemetry.js";
198
+ } from "./containerCompatibility.js";
193
199
  import { ContainerFluidHandleContext } from "./containerHandleContext.js";
194
200
  import { channelToDataStore } from "./dataStore.js";
195
201
  import { FluidDataStoreRegistry } from "./dataStoreRegistry.js";
@@ -368,7 +374,7 @@ export interface ISummaryRuntimeOptions {
368
374
  *
369
375
  * @privateRemarks If any new properties are added to this interface (or
370
376
  * {@link IContainerRuntimeOptionsInternal}), then we will also need to make
371
- * changes in {@link file://./compatUtils.ts}.
377
+ * changes in {@link file://./containerCompatibility.ts}.
372
378
  * If the new property does not change the DocumentSchema, then it must be
373
379
  * explicity omitted from {@link RuntimeOptionsAffectingDocSchema}.
374
380
  * If it does change the DocumentSchema, then a corresponding entry must be
@@ -702,6 +708,21 @@ export let getSingleUseLegacyLogCallback = (logger: ITelemetryLoggerExt, type: s
702
708
  };
703
709
  };
704
710
 
711
+ /**
712
+ * A {@link TypedMessage} that has unknown content explicitly
713
+ * noted as deserialized JSON.
714
+ */
715
+ export interface UnknownIncomingTypedMessage extends TypedMessage {
716
+ content: OpaqueJsonDeserialized<unknown>;
717
+ }
718
+
719
+ /**
720
+ * Does nothing helper to apply unverified branding to a value.
721
+ */
722
+ function markUnverified<const T>(value: T): T & UnverifiedBrand<T> {
723
+ return value as T & UnverifiedBrand<T>;
724
+ }
725
+
705
726
  type UnsequencedSignalEnvelope = Omit<ISignalEnvelope, "clientBroadcastSignalSequenceNumber">;
706
727
 
707
728
  /**
@@ -1173,16 +1194,6 @@ export class ContainerRuntime
1173
1194
  recentBatchInfo,
1174
1195
  );
1175
1196
 
1176
- runtime.blobManager.stashedBlobsUploadP.then(
1177
- () => {
1178
- // make sure we didn't reconnect before the promise resolved
1179
- if (runtime.delayConnectClientId !== undefined && !runtime.disposed) {
1180
- runtime.delayConnectClientId = undefined;
1181
- runtime.setConnectionStateCore(true, runtime.delayConnectClientId);
1182
- }
1183
- },
1184
- (error: IErrorBase) => runtime.closeFn(error),
1185
- );
1186
1197
  // Initialize the base state of the runtime before it's returned.
1187
1198
  await runtime.initializeBaseState(context.loader);
1188
1199
 
@@ -1194,7 +1205,6 @@ export class ContainerRuntime
1194
1205
  }
1195
1206
 
1196
1207
  public readonly options: Record<string | number, unknown>;
1197
- private imminentClosure: boolean = false;
1198
1208
 
1199
1209
  private readonly _getClientId: () => string | undefined;
1200
1210
  public get clientId(): string | undefined {
@@ -1205,7 +1215,7 @@ export class ContainerRuntime
1205
1215
 
1206
1216
  private readonly isSummarizerClient: boolean;
1207
1217
 
1208
- public get storage(): IDocumentStorageService {
1218
+ public get storage(): IContainerStorageService {
1209
1219
  return this._storage;
1210
1220
  }
1211
1221
 
@@ -1338,14 +1348,11 @@ export class ContainerRuntime
1338
1348
  private flushScheduled = false;
1339
1349
 
1340
1350
  private canSendOps: boolean;
1351
+ private canSendSignals: boolean | undefined;
1341
1352
 
1342
- private consecutiveReconnects = 0;
1353
+ private readonly getConnectionState?: () => ConnectionState;
1343
1354
 
1344
- /**
1345
- * Used to delay transition to "connected" state while we upload
1346
- * attachment blobs that were added while disconnected
1347
- */
1348
- private delayConnectClientId?: string;
1355
+ private consecutiveReconnects = 0;
1349
1356
 
1350
1357
  private readonly dataModelChangeRunner = new RunCounter();
1351
1358
 
@@ -1497,7 +1504,7 @@ export class ContainerRuntime
1497
1504
  existing: boolean,
1498
1505
 
1499
1506
  blobManagerLoadInfo: IBlobManagerLoadInfo,
1500
- private readonly _storage: IDocumentStorageService,
1507
+ private readonly _storage: IContainerStorageService,
1501
1508
  private readonly createIdCompressorFn: () => IIdCompressor & IIdCompressorCore,
1502
1509
 
1503
1510
  private readonly documentsSchemaController: DocumentsSchemaController,
@@ -1536,15 +1543,18 @@ export class ContainerRuntime
1536
1543
  pendingLocalState,
1537
1544
  supportedFeatures,
1538
1545
  snapshotWithContents,
1546
+ getConnectionState,
1539
1547
  } = context;
1540
1548
 
1549
+ this.getConnectionState = getConnectionState;
1550
+
1541
1551
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
1542
1552
  this.disposeFn = disposeFn ?? closeFn;
1543
1553
 
1544
1554
  // Validate that the Loader is compatible with this Runtime.
1545
- const maybeloaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
1555
+ const maybeLoaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
1546
1556
  validateLoaderCompatibility(
1547
- maybeloaderCompatDetailsForRuntime.ILayerCompatDetails,
1557
+ maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails,
1548
1558
  this.disposeFn,
1549
1559
  );
1550
1560
 
@@ -1680,6 +1690,9 @@ export class ContainerRuntime
1680
1690
  // Note that we only need to pull the *initial* connected state from the context.
1681
1691
  // Later updates come through calls to setConnectionState.
1682
1692
  this.canSendOps = connected;
1693
+ this.canSendSignals = this.getConnectionState
1694
+ ? this.getConnectionState() === ConnectionState.Connected
1695
+ : undefined;
1683
1696
 
1684
1697
  this.mc.logger.sendTelemetryEvent({
1685
1698
  eventName: "GCFeatureMatrix",
@@ -1763,7 +1776,7 @@ export class ContainerRuntime
1763
1776
  // If the context has ILayerCompatDetails, it supports referenceSequenceNumbers since that features
1764
1777
  // predates ILayerCompatDetails.
1765
1778
  const referenceSequenceNumbersSupported =
1766
- maybeloaderCompatDetailsForRuntime.ILayerCompatDetails === undefined
1779
+ maybeLoaderCompatDetailsForRuntime.ILayerCompatDetails === undefined
1767
1780
  ? supportedFeatures?.get("referenceSequenceNumbers") === true
1768
1781
  : true;
1769
1782
  if (
@@ -1898,7 +1911,7 @@ export class ContainerRuntime
1898
1911
  routeContext: this.handleContext,
1899
1912
  blobManagerLoadInfo,
1900
1913
  storage: this.storage,
1901
- sendBlobAttachOp: (localId: string, blobId?: string) => {
1914
+ sendBlobAttachOp: (localId: string, blobId: string) => {
1902
1915
  if (!this.disposed) {
1903
1916
  this.submit(
1904
1917
  { type: ContainerMessageType.BlobAttach, contents: undefined },
@@ -2774,28 +2787,6 @@ export class ContainerRuntime
2774
2787
  if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
2775
2788
  this.loadIdCompressor();
2776
2789
  }
2777
- if (canSendOps === false && this.delayConnectClientId !== undefined) {
2778
- this.delayConnectClientId = undefined;
2779
- this.mc.logger.sendTelemetryEvent({
2780
- eventName: "UnsuccessfulConnectedTransition",
2781
- });
2782
- // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
2783
- return;
2784
- }
2785
-
2786
- // If there are stashed blobs in the pending state, we need to delay
2787
- // propagation of the "connected" event until we have uploaded them to
2788
- // ensure we don't submit ops referencing a blob that has not been uploaded
2789
- const connecting = canSendOps && !this.canSendOps;
2790
- if (connecting && this.blobManager.hasPendingStashedUploads()) {
2791
- assert(
2792
- !this.delayConnectClientId,
2793
- 0x791 /* Connect event delay must be canceled before subsequent connect event */,
2794
- );
2795
- assert(!!clientId, 0x792 /* Must have clientId when connecting */);
2796
- this.delayConnectClientId = clientId;
2797
- return;
2798
- }
2799
2790
 
2800
2791
  this.setConnectionStateCore(canSendOps, clientId);
2801
2792
  }
@@ -2806,10 +2797,6 @@ export class ContainerRuntime
2806
2797
  * @remarks The connection state from container context used here when raising connected events.
2807
2798
  */
2808
2799
  private setConnectionStateCore(canSendOps: boolean, clientId?: string): void {
2809
- assert(
2810
- !this.delayConnectClientId,
2811
- 0x394 /* connect event delay must be cleared before propagating connect event */,
2812
- );
2813
2800
  this.verifyNotClosed();
2814
2801
 
2815
2802
  // There might be no change of state due to Container calling this API after loading runtime.
@@ -2863,7 +2850,44 @@ export class ContainerRuntime
2863
2850
  this.channelCollection.setConnectionState(canSendOps, clientId);
2864
2851
  this.garbageCollector.setConnectionState(canSendOps, clientId);
2865
2852
 
2853
+ // Emit "connected" and "disconnected" events based on ability to send ops
2866
2854
  raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
2855
+ // Emit "connectedToService" and "disconnectedFromService" events based on service connection status
2856
+ this.emitServiceConnectionEvents(canSendOpsChanged, canSendOps, clientId);
2857
+ }
2858
+
2859
+ /**
2860
+ * Emits service connection events based on connection state changes.
2861
+ *
2862
+ * @remarks
2863
+ * "connectedToService" is emitted when container connection state transitions to 'Connected' regardless of connection mode.
2864
+ * "disconnectedFromService" excludes false "disconnected" events that happen when readonly client transitions to 'Connected'.
2865
+ */
2866
+ private emitServiceConnectionEvents(
2867
+ canSendOpsChanged: boolean,
2868
+ canSendOps: boolean,
2869
+ clientId?: string,
2870
+ ): void {
2871
+ if (!this.getConnectionState) {
2872
+ return;
2873
+ }
2874
+
2875
+ const canSendSignals = this.getConnectionState() === ConnectionState.Connected;
2876
+ const canSendSignalsChanged = this.canSendSignals !== canSendSignals;
2877
+ this.canSendSignals = canSendSignals;
2878
+ if (canSendSignalsChanged) {
2879
+ // If canSendSignals changed, we either transitioned from Connected to Disconnected or CatchingUp to Connected
2880
+ if (canSendSignals) {
2881
+ // Emit for CatchingUp to Connected transition
2882
+ this.emit("connectedToService", clientId, canSendOps);
2883
+ } else {
2884
+ // Emit for Connected to Disconnected transition
2885
+ this.emit("disconnectedFromService");
2886
+ }
2887
+ } else if (canSendOpsChanged) {
2888
+ // If canSendSignals did not change but canSendOps did, then connection type has changed.
2889
+ this.emit("connectionTypeChanged", canSendOps);
2890
+ }
2867
2891
  }
2868
2892
 
2869
2893
  public async notifyOpReplay(message: ISequencedDocumentMessage): Promise<void> {
@@ -3289,17 +3313,17 @@ export class ContainerRuntime
3289
3313
  public processSignal(
3290
3314
  message: ISignalMessage<{
3291
3315
  type: string;
3292
- content: ISignalEnvelope<{ type: string; content: JsonDeserialized<unknown> }>;
3316
+ content: ISignalEnvelope<{ type: string; content: OpaqueJsonDeserialized<unknown> }>;
3293
3317
  }>,
3294
3318
  local: boolean,
3295
3319
  ): void {
3296
3320
  const envelope = message.content;
3297
- const transformed = {
3321
+ const transformed = markUnverified({
3298
3322
  clientId: message.clientId,
3299
3323
  content: envelope.contents.content,
3300
3324
  type: envelope.contents.type,
3301
3325
  targetClientId: message.targetClientId,
3302
- };
3326
+ });
3303
3327
 
3304
3328
  // Only collect signal telemetry for broadcast messages sent by the current client.
3305
3329
  if (message.clientId === this.clientId) {
@@ -3322,7 +3346,8 @@ export class ContainerRuntime
3322
3346
 
3323
3347
  private routeNonContainerSignal(
3324
3348
  address: string,
3325
- signalMessage: IInboundSignalMessage<{ type: string; content: JsonDeserialized<unknown> }>,
3349
+ signalMessage: IInboundSignalMessage<UnknownIncomingTypedMessage> &
3350
+ UnverifiedBrand<UnknownIncomingTypedMessage>,
3326
3351
  local: boolean,
3327
3352
  ): void {
3328
3353
  // channelCollection signals are identified by no starting `/` in address.
@@ -3330,13 +3355,15 @@ export class ContainerRuntime
3330
3355
  // Due to a mismatch between different layers in terms of
3331
3356
  // what is the interface of passing signals, we need to adjust
3332
3357
  // the signal envelope before sending it to the datastores to be processed
3333
- const envelope = {
3334
- address,
3335
- contents: signalMessage.content,
3358
+ const channelSignalMessage = {
3359
+ ...signalMessage,
3360
+ content: {
3361
+ address,
3362
+ contents: signalMessage.content,
3363
+ },
3336
3364
  };
3337
- signalMessage.content = envelope;
3338
3365
 
3339
- this.channelCollection.processSignal(signalMessage, local);
3366
+ this.channelCollection.processSignal(channelSignalMessage, local);
3340
3367
  return;
3341
3368
  }
3342
3369
 
@@ -3605,9 +3632,7 @@ export class ContainerRuntime
3605
3632
  private shouldSendOps(): boolean {
3606
3633
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
3607
3634
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
3608
- return (
3609
- this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure
3610
- );
3635
+ return this.connected && !this.innerDeltaManager.readOnlyInfo.readonly;
3611
3636
  }
3612
3637
 
3613
3638
  private readonly _quorum: IQuorumClients;
@@ -5016,60 +5041,45 @@ export class ContainerRuntime
5016
5041
 
5017
5042
  public getPendingLocalState(props?: IGetPendingLocalStateProps): unknown {
5018
5043
  this.verifyNotClosed();
5044
+ if (props?.notifyImminentClosure) {
5045
+ throw new UsageError("notifyImminentClosure is no longer supported in ContainerRuntime");
5046
+ }
5019
5047
 
5020
5048
  if (this.batchRunner.running) {
5021
5049
  throw new UsageError("can't get state while manually accumulating a batch");
5022
5050
  }
5023
- this.imminentClosure ||= props?.notifyImminentClosure ?? false;
5024
-
5025
- const getSyncState = (
5026
- pendingAttachmentBlobs?: IPendingBlobs,
5027
- ): IPendingRuntimeState | undefined => {
5028
- const pending = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
5029
- const sessionExpiryTimerStarted =
5030
- props?.sessionExpiryTimerStarted ?? this.garbageCollector.sessionExpiryTimerStarted;
5031
-
5032
- const pendingIdCompressorState = this._idCompressor?.serialize(true);
5033
-
5034
- return {
5035
- pending,
5036
- pendingIdCompressorState,
5037
- pendingAttachmentBlobs,
5038
- sessionExpiryTimerStarted,
5039
- };
5040
- };
5041
- const perfEvent = {
5042
- eventName: "getPendingLocalState",
5043
- notifyImminentClosure: props?.notifyImminentClosure,
5044
- };
5045
- const logAndReturnPendingState = (
5046
- event: PerformanceEvent,
5047
- pendingState?: IPendingRuntimeState,
5048
- ): IPendingRuntimeState | undefined => {
5049
- event.end({
5050
- attachmentBlobsSize: Object.keys(pendingState?.pendingAttachmentBlobs ?? {}).length,
5051
- pendingOpsSize: pendingState?.pending?.pendingStates.length,
5052
- });
5053
- return pendingState;
5054
- };
5055
5051
 
5056
5052
  // Flush pending batch.
5057
- // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
5053
+ // getPendingLocalState() is only exposed through Container.getPendingLocalState(), so it's safe
5058
5054
  // to close current batch.
5059
5055
  this.flush();
5060
5056
 
5061
- return props?.notifyImminentClosure === true
5062
- ? PerformanceEvent.timedExecAsync(this.mc.logger, perfEvent, async (event) =>
5063
- logAndReturnPendingState(
5064
- event,
5065
- getSyncState(
5066
- await this.blobManager.attachAndGetPendingBlobs(props?.stopBlobAttachingSignal),
5067
- ),
5068
- ),
5069
- )
5070
- : PerformanceEvent.timedExec(this.mc.logger, perfEvent, (event) =>
5071
- logAndReturnPendingState(event, getSyncState()),
5072
- );
5057
+ return PerformanceEvent.timedExec<IPendingRuntimeState | undefined>(
5058
+ this.mc.logger,
5059
+ {
5060
+ eventName: "getPendingLocalState",
5061
+ },
5062
+ (event) => {
5063
+ const pending = this.pendingStateManager.getLocalState(props?.snapshotSequenceNumber);
5064
+ const sessionExpiryTimerStarted =
5065
+ props?.sessionExpiryTimerStarted ?? this.garbageCollector.sessionExpiryTimerStarted;
5066
+
5067
+ const pendingIdCompressorState = this._idCompressor?.serialize(true);
5068
+ const pendingAttachmentBlobs = this.blobManager.getPendingBlobs();
5069
+
5070
+ const pendingRuntimeState: IPendingRuntimeState = {
5071
+ pending,
5072
+ pendingIdCompressorState,
5073
+ pendingAttachmentBlobs,
5074
+ sessionExpiryTimerStarted,
5075
+ };
5076
+ event.end({
5077
+ attachmentBlobsSize: Object.keys(pendingAttachmentBlobs ?? {}).length,
5078
+ pendingOpsSize: pendingRuntimeState?.pending?.pendingStates.length,
5079
+ });
5080
+ return pendingRuntimeState;
5081
+ },
5082
+ );
5073
5083
  }
5074
5084
 
5075
5085
  public summarizeOnDemand(options: IOnDemandSummarizeOptions): ISummarizeResults {
@@ -5103,11 +5113,36 @@ export class ContainerRuntime
5103
5113
  // It is lazily create to avoid listeners (old events) that ultimately go nowhere.
5104
5114
  private readonly lazyEventsForExtensions = new Lazy<Listenable<ExtensionHostEvents>>(() => {
5105
5115
  const eventEmitter = createEmitter<ExtensionHostEvents>();
5106
- this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
5107
- this.on("disconnected", () => eventEmitter.emit("disconnected"));
5116
+ if (this.getConnectionState) {
5117
+ this.on("connectedToService", (clientId: string, canWrite: boolean) => {
5118
+ eventEmitter.emit("joined", { clientId, canWrite });
5119
+ });
5120
+ this.on("disconnectedFromService", () => eventEmitter.emit("disconnected"));
5121
+ this.on("connectionTypeChanged", (canWrite: boolean) =>
5122
+ eventEmitter.emit("connectionTypeChanged", canWrite),
5123
+ );
5124
+ } else {
5125
+ this.on("connected", (clientId: string) => {
5126
+ eventEmitter.emit("joined", { clientId, canWrite: true });
5127
+ });
5128
+ this.on("disconnected", () => eventEmitter.emit("disconnected"));
5129
+ }
5108
5130
  return eventEmitter;
5109
5131
  });
5110
5132
 
5133
+ private getJoinedStatus(): JoinedStatus {
5134
+ const getConnectionState = this.getConnectionState;
5135
+ if (getConnectionState) {
5136
+ const connectionState = getConnectionState();
5137
+ if (connectionState === ConnectionState.Connected) {
5138
+ return this.canSendOps ? "joinedForWriting" : "joinedForReading";
5139
+ }
5140
+ } else if (this.canSendOps) {
5141
+ return "joinedForWriting";
5142
+ }
5143
+ return "disconnected";
5144
+ }
5145
+
5111
5146
  private readonly submitExtensionSignal: <TMessage extends TypedMessage>(
5112
5147
  id: string,
5113
5148
  addressChain: string[],
@@ -5126,7 +5161,7 @@ export class ContainerRuntime
5126
5161
  let entry = this.extensions.get(id);
5127
5162
  if (entry === undefined) {
5128
5163
  const runtime = {
5129
- isConnected: () => this.connected,
5164
+ getJoinedStatus: this.getJoinedStatus.bind(this),
5130
5165
  getClientId: () => this.clientId,
5131
5166
  events: this.lazyEventsForExtensions.value,
5132
5167
  logger: this.baseLogger,