@fluidframework/container-loader 2.0.0-dev-rc.1.0.0.228517 → 2.0.0-dev-rc.1.0.0.232845

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 (90) hide show
  1. package/README.md +3 -3
  2. package/dist/attachment.d.ts +116 -0
  3. package/dist/attachment.d.ts.map +1 -0
  4. package/dist/attachment.js +82 -0
  5. package/dist/attachment.js.map +1 -0
  6. package/dist/connectionManager.d.ts.map +1 -1
  7. package/dist/connectionManager.js +1 -2
  8. package/dist/connectionManager.js.map +1 -1
  9. package/dist/container-loader-alpha.d.ts +1 -1
  10. package/dist/container-loader-untrimmed.d.ts +1 -1
  11. package/dist/container.d.ts +21 -8
  12. package/dist/container.d.ts.map +1 -1
  13. package/dist/container.js +177 -149
  14. package/dist/container.js.map +1 -1
  15. package/dist/containerContext.d.ts +3 -2
  16. package/dist/containerContext.d.ts.map +1 -1
  17. package/dist/containerContext.js +2 -1
  18. package/dist/containerContext.js.map +1 -1
  19. package/dist/containerStorageAdapter.d.ts +3 -3
  20. package/dist/containerStorageAdapter.d.ts.map +1 -1
  21. package/dist/containerStorageAdapter.js +10 -11
  22. package/dist/containerStorageAdapter.js.map +1 -1
  23. package/dist/loader.d.ts +1 -1
  24. package/dist/loader.js.map +1 -1
  25. package/dist/packageVersion.d.ts +1 -1
  26. package/dist/packageVersion.js +1 -1
  27. package/dist/packageVersion.js.map +1 -1
  28. package/dist/protocolTreeDocumentStorageService.d.ts +1 -0
  29. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  30. package/dist/protocolTreeDocumentStorageService.js +1 -0
  31. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  32. package/dist/retriableDocumentStorageService.d.ts +2 -1
  33. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  34. package/dist/retriableDocumentStorageService.js +8 -0
  35. package/dist/retriableDocumentStorageService.js.map +1 -1
  36. package/dist/tsdoc-metadata.json +1 -1
  37. package/dist/utils.d.ts +13 -7
  38. package/dist/utils.d.ts.map +1 -1
  39. package/dist/utils.js +98 -29
  40. package/dist/utils.js.map +1 -1
  41. package/lib/attachment.d.mts +112 -0
  42. package/lib/attachment.d.mts.map +1 -0
  43. package/lib/attachment.mjs +74 -0
  44. package/lib/attachment.mjs.map +1 -0
  45. package/lib/connectionManager.d.mts.map +1 -1
  46. package/lib/connectionManager.mjs +2 -5
  47. package/lib/connectionManager.mjs.map +1 -1
  48. package/lib/container-loader-alpha.d.mts +1 -1
  49. package/lib/container-loader-untrimmed.d.mts +1 -1
  50. package/lib/container.d.mts +21 -8
  51. package/lib/container.d.mts.map +1 -1
  52. package/lib/container.mjs +176 -151
  53. package/lib/container.mjs.map +1 -1
  54. package/lib/containerContext.d.mts +3 -2
  55. package/lib/containerContext.d.mts.map +1 -1
  56. package/lib/containerContext.mjs +2 -1
  57. package/lib/containerContext.mjs.map +1 -1
  58. package/lib/containerStorageAdapter.d.mts +3 -3
  59. package/lib/containerStorageAdapter.d.mts.map +1 -1
  60. package/lib/containerStorageAdapter.mjs +10 -11
  61. package/lib/containerStorageAdapter.mjs.map +1 -1
  62. package/lib/loader.d.mts +1 -1
  63. package/lib/loader.mjs.map +1 -1
  64. package/lib/packageVersion.d.mts +1 -1
  65. package/lib/packageVersion.mjs +1 -1
  66. package/lib/packageVersion.mjs.map +1 -1
  67. package/lib/protocolTreeDocumentStorageService.d.mts +1 -0
  68. package/lib/protocolTreeDocumentStorageService.d.mts.map +1 -1
  69. package/lib/protocolTreeDocumentStorageService.mjs +1 -0
  70. package/lib/protocolTreeDocumentStorageService.mjs.map +1 -1
  71. package/lib/retriableDocumentStorageService.d.mts +2 -1
  72. package/lib/retriableDocumentStorageService.d.mts.map +1 -1
  73. package/lib/retriableDocumentStorageService.mjs +9 -1
  74. package/lib/retriableDocumentStorageService.mjs.map +1 -1
  75. package/lib/utils.d.mts +13 -7
  76. package/lib/utils.d.mts.map +1 -1
  77. package/lib/utils.mjs +96 -29
  78. package/lib/utils.mjs.map +1 -1
  79. package/package.json +21 -15
  80. package/src/attachment.ts +219 -0
  81. package/src/connectionManager.ts +2 -4
  82. package/src/container.ts +260 -191
  83. package/src/containerContext.ts +2 -1
  84. package/src/containerStorageAdapter.ts +15 -12
  85. package/src/loader.ts +1 -1
  86. package/src/packageVersion.ts +1 -1
  87. package/src/protocolTreeDocumentStorageService.ts +1 -0
  88. package/src/retriableDocumentStorageService.ts +18 -1
  89. package/src/utils.ts +128 -36
  90. /package/{.eslintrc.js → .eslintrc.cjs} +0 -0
package/src/container.ts CHANGED
@@ -10,9 +10,9 @@ import {
10
10
  IEvent,
11
11
  ITelemetryProperties,
12
12
  TelemetryEventCategory,
13
- IRequest,
14
13
  FluidObject,
15
14
  LogLevel,
15
+ IRequest,
16
16
  } from "@fluidframework/core-interfaces";
17
17
  import {
18
18
  AttachState,
@@ -41,6 +41,7 @@ import {
41
41
  IDocumentServiceFactory,
42
42
  IDocumentStorageService,
43
43
  IResolvedUrl,
44
+ ISnapshot,
44
45
  IThrottlingWarning,
45
46
  IUrlResolver,
46
47
  } from "@fluidframework/driver-definitions";
@@ -48,9 +49,10 @@ import {
48
49
  readAndParse,
49
50
  OnlineStatus,
50
51
  isOnline,
51
- runWithRetry,
52
52
  isCombinedAppAndProtocolSummary,
53
53
  MessageType2,
54
+ isInstanceOfISnapshot,
55
+ runWithRetry,
54
56
  } from "@fluidframework/driver-utils";
55
57
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
56
58
  import {
@@ -86,7 +88,9 @@ import {
86
88
  formatTick,
87
89
  GenericError,
88
90
  UsageError,
91
+ IFluidErrorBase,
89
92
  } from "@fluidframework/telemetry-utils";
93
+ import structuredClone from "@ungap/structured-clone";
90
94
  import { Audience } from "./audience";
91
95
  import { ContainerContext } from "./containerContext";
92
96
  import {
@@ -102,14 +106,17 @@ import { pkgVersion } from "./packageVersion";
102
106
  import {
103
107
  ContainerStorageAdapter,
104
108
  getBlobContentsFromTree,
105
- getBlobContentsFromTreeWithBlobContents,
106
109
  ISerializableBlobContents,
107
110
  } from "./containerStorageAdapter";
108
111
  import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
109
112
  import {
113
+ ISnapshotTreeWithBlobContents,
110
114
  combineAppAndProtocolSummary,
111
115
  getProtocolSnapshotTree,
112
- getSnapshotTreeFromSerializedContainer,
116
+ getSnapshotTreeAndBlobsFromSerializedContainer,
117
+ combineSnapshotTreeAndSnapshotBlobs,
118
+ getDetachedContainerStateFromSerializedContainer,
119
+ runSingle,
113
120
  } from "./utils";
114
121
  import { initQuorumValuesFromCodeDetails } from "./quorum";
115
122
  import { NoopHeuristic } from "./noopHeuristic";
@@ -121,6 +128,7 @@ import {
121
128
  ProtocolHandlerBuilder,
122
129
  protocolHandlerShouldProcessSignal,
123
130
  } from "./protocol";
131
+ import { AttachProcessProps, AttachmentData, runRetriableAttachProcess } from "./attachment";
124
132
 
125
133
  const detachedContainerRefSeqNumber = 0;
126
134
 
@@ -129,8 +137,6 @@ const savedContainerEvent = "saved";
129
137
 
130
138
  const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
131
139
 
132
- const hasBlobsSummaryTree = ".hasAttachmentBlobs";
133
-
134
140
  /**
135
141
  * @internal
136
142
  */
@@ -360,6 +366,18 @@ export interface IPendingContainerState {
360
366
  clientId?: string;
361
367
  }
362
368
 
369
+ /**
370
+ * State saved by a container in detached state, to be used to load a new instance
371
+ * of the container to the same state (rehydrate)
372
+ * @internal
373
+ */
374
+ export interface IPendingDetachedContainerState {
375
+ attached: boolean;
376
+ baseSnapshot: ISnapshotTree;
377
+ snapshotBlobs: ISerializableBlobContents;
378
+ hasAttachmentBlobs: boolean;
379
+ }
380
+
363
381
  const summarizerClientType = "summarizer";
364
382
 
365
383
  interface IContainerLifecycleEvents extends IEvent {
@@ -471,12 +489,9 @@ export class Container
471
489
  container.mc.logger,
472
490
  { eventName: "RehydrateDetachedFromSnapshot" },
473
491
  async (_event) => {
474
- const deserializedSummary = JSON.parse(snapshot);
475
- if (!isCombinedAppAndProtocolSummary(deserializedSummary, hasBlobsSummaryTree)) {
476
- throw new UsageError("Cannot rehydrate detached container. Incorrect format");
477
- }
478
-
479
- await container.rehydrateDetachedFromSnapshot(deserializedSummary);
492
+ const detachedContainerState: IPendingDetachedContainerState =
493
+ getDetachedContainerStateFromSerializedContainer(snapshot);
494
+ await container.rehydrateDetachedFromSnapshot(detachedContainerState);
480
495
  return container;
481
496
  },
482
497
  { start: true, end: true, cancel: "generic" },
@@ -551,8 +566,6 @@ export class Container
551
566
  return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
552
567
  }
553
568
 
554
- private _attachState = AttachState.Detached;
555
-
556
569
  private readonly storageAdapter: ContainerStorageAdapter;
557
570
 
558
571
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
@@ -578,17 +591,16 @@ export class Container
578
591
  private firstConnection = true;
579
592
  private readonly connectionTransitionTimes: number[] = [];
580
593
  private _loadedFromVersion: IVersion | undefined;
581
- private attachStarted = false;
582
594
  private _dirtyContainer = false;
583
595
  private readonly savedOps: ISequencedDocumentMessage[] = [];
584
- private baseSnapshot?: ISnapshotTree;
585
- private baseSnapshotBlobs?: ISerializableBlobContents;
596
+ private attachmentData: AttachmentData = { state: AttachState.Detached };
586
597
  private readonly _containerId: string;
587
598
 
588
599
  private lastVisible: number | undefined;
589
600
  private readonly visibilityEventHandler: (() => void) | undefined;
590
601
  private readonly connectionStateHandler: IConnectionStateHandler;
591
602
  private readonly clientsWhoShouldHaveLeft = new Set<string>();
603
+ private _containerMetadata: Readonly<Record<string, string>> = {};
592
604
 
593
605
  private setAutoReconnectTime = performance.now();
594
606
 
@@ -617,6 +629,10 @@ export class Container
617
629
  return this._deltaManager.readOnlyInfo;
618
630
  }
619
631
 
632
+ public get containerMetadata(): Record<string, string> {
633
+ return this._containerMetadata;
634
+ }
635
+
620
636
  /**
621
637
  * Sends signal to runtime (and data stores) to be read-only.
622
638
  * Hosts may have read only views, indicating to data stores that no edits are allowed.
@@ -829,7 +845,7 @@ export class Container
829
845
  clientType, // Differentiating summarizer container from main container
830
846
  containerId: this._containerId,
831
847
  docId: () => this.resolvedUrl?.id,
832
- containerAttachState: () => this._attachState,
848
+ containerAttachState: () => this.attachState,
833
849
  containerLifecycleState: () => this._lifecycleState,
834
850
  containerConnectionState: () => ConnectionState[this.connectionState],
835
851
  serializedContainer: pendingLocalState !== undefined,
@@ -1030,6 +1046,11 @@ export class Container
1030
1046
 
1031
1047
  this._lifecycleState = "closing";
1032
1048
 
1049
+ // Back-compat for Old driver
1050
+ if (this.service?.off !== undefined) {
1051
+ this.service?.off("metadataUpdate", this.metadataUpdateHandler);
1052
+ }
1053
+
1033
1054
  this._protocolHandler?.close();
1034
1055
 
1035
1056
  this.connectionStateHandler.dispose();
@@ -1147,20 +1168,19 @@ export class Container
1147
1168
  );
1148
1169
  }
1149
1170
  assert(
1150
- this.attachState === AttachState.Attached,
1171
+ this.attachmentData.state === AttachState.Attached,
1151
1172
  0x0d1 /* "Container should be attached before close" */,
1152
1173
  );
1153
1174
  assert(
1154
1175
  this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
1155
1176
  0x0d2 /* "resolved url should be valid Fluid url" */,
1156
1177
  );
1157
- assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
1158
- assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
1178
+ assert(this.attachmentData.snapshot !== undefined, 0x5d5 /* no base data */);
1159
1179
  const pendingRuntimeState = await this.runtime.getPendingLocalState(props);
1160
1180
  const pendingState: IPendingContainerState = {
1161
1181
  pendingRuntimeState,
1162
- baseSnapshot: this.baseSnapshot,
1163
- snapshotBlobs: this.baseSnapshotBlobs,
1182
+ baseSnapshot: this.attachmentData.snapshot.tree,
1183
+ snapshotBlobs: this.attachmentData.snapshot.blobs,
1164
1184
  savedOps: this.savedOps,
1165
1185
  url: this.resolvedUrl.url,
1166
1186
  // no need to save this if there is no pending runtime state
@@ -1173,7 +1193,7 @@ export class Container
1173
1193
  }
1174
1194
 
1175
1195
  public get attachState(): AttachState {
1176
- return this._attachState;
1196
+ return this.attachmentData.state;
1177
1197
  }
1178
1198
 
1179
1199
  public serialize(): string {
@@ -1186,142 +1206,129 @@ export class Container
1186
1206
  const protocolSummary = this.captureProtocolSummary();
1187
1207
  const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1188
1208
 
1189
- if (this.detachedBlobStorage && this.detachedBlobStorage.size > 0) {
1190
- combinedSummary.tree[hasBlobsSummaryTree] = {
1191
- type: SummaryType.Blob,
1192
- content: "true",
1193
- };
1194
- }
1195
- return JSON.stringify(combinedSummary);
1196
- }
1209
+ const { tree: snapshot, blobs } =
1210
+ getSnapshotTreeAndBlobsFromSerializedContainer(combinedSummary);
1197
1211
 
1198
- public async attach(
1199
- request: IRequest,
1200
- attachProps?: { deltaConnection?: "none" | "delayed" },
1201
- ): Promise<void> {
1202
- await PerformanceEvent.timedExecAsync(
1203
- this.mc.logger,
1204
- { eventName: "Attach" },
1205
- async () => {
1206
- if (this._lifecycleState !== "loaded") {
1207
- // pre-0.58 error message: containerNotValidForAttach
1208
- throw new UsageError(
1209
- `The Container is not in a valid state for attach [${this._lifecycleState}]`,
1210
- );
1211
- }
1212
-
1213
- // If container is already attached or attach is in progress, throw an error.
1214
- assert(
1215
- this._attachState === AttachState.Detached && !this.attachStarted,
1216
- 0x205 /* "attach() called more than once" */,
1217
- );
1218
- this.attachStarted = true;
1219
-
1220
- // If attachment blobs were uploaded in detached state we will go through a different attach flow
1221
- const hasAttachmentBlobs =
1222
- this.detachedBlobStorage !== undefined && this.detachedBlobStorage.size > 0;
1223
-
1224
- try {
1225
- assert(
1226
- this.deltaManager.inbound.length === 0,
1227
- 0x0d6 /* "Inbound queue should be empty when attaching" */,
1228
- );
1229
-
1230
- let summary: ISummaryTree;
1231
- if (!hasAttachmentBlobs) {
1232
- // Get the document state post attach - possibly can just call attach but we need to change the
1233
- // semantics around what the attach means as far as async code goes.
1234
- const appSummary: ISummaryTree = this.runtime.createSummary();
1235
- const protocolSummary = this.captureProtocolSummary();
1236
- summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1237
-
1238
- // Set the state as attaching as we are starting the process of attaching container.
1239
- // This should be fired after taking the summary because it is the place where we are
1240
- // starting to attach the container to storage.
1241
- // Also, this should only be fired in detached container.
1242
- this._attachState = AttachState.Attaching;
1243
- this.runtime.setAttachState(AttachState.Attaching);
1244
- this.emit("attaching");
1245
- if (this.offlineLoadEnabled) {
1246
- const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1247
- this.baseSnapshot = snapshot;
1248
- this.baseSnapshotBlobs =
1249
- getBlobContentsFromTreeWithBlobContents(snapshot);
1250
- }
1251
- }
1252
-
1253
- // Actually go and create the resolved document
1254
- if (this.service === undefined) {
1255
- const createNewResolvedUrl = await this.urlResolver.resolve(request);
1256
- assert(
1257
- this.client.details.type !== summarizerClientType &&
1258
- createNewResolvedUrl !== undefined,
1259
- 0x2c4 /* "client should not be summarizer before container is created" */,
1260
- );
1261
- this.service = await runWithRetry(
1262
- async () =>
1263
- this.serviceFactory.createContainer(
1264
- summary,
1265
- createNewResolvedUrl,
1266
- this.subLogger,
1267
- false, // clientIsSummarizer
1268
- ),
1269
- "containerAttach",
1270
- this.mc.logger,
1271
- {
1272
- cancel: this._deltaManager.closeAbortController.signal,
1273
- }, // progress
1212
+ const detachedContainerState: IPendingDetachedContainerState = {
1213
+ attached: false,
1214
+ baseSnapshot: snapshot,
1215
+ snapshotBlobs: blobs,
1216
+ hasAttachmentBlobs: !!this.detachedBlobStorage && this.detachedBlobStorage.size > 0,
1217
+ };
1218
+ return JSON.stringify(detachedContainerState);
1219
+ }
1220
+
1221
+ public readonly attach = runSingle(
1222
+ async (
1223
+ request: IRequest,
1224
+ attachProps?: { deltaConnection?: "none" | "delayed" },
1225
+ ): Promise<void> => {
1226
+ await PerformanceEvent.timedExecAsync(
1227
+ this.mc.logger,
1228
+ { eventName: "Attach" },
1229
+ async () => {
1230
+ if (
1231
+ this._lifecycleState !== "loaded" ||
1232
+ this.attachmentData.state === AttachState.Attached
1233
+ ) {
1234
+ // pre-0.58 error message: containerNotValidForAttach
1235
+ throw new UsageError(
1236
+ `The Container is not in a valid state for attach [${this._lifecycleState}] and [${this.attachState}]`,
1274
1237
  );
1275
1238
  }
1276
- this.storageAdapter.connectToService(this.service);
1277
1239
 
1278
- if (hasAttachmentBlobs) {
1279
- // upload blobs to storage
1280
- assert(
1281
- !!this.detachedBlobStorage,
1282
- 0x24e /* "assertion for type narrowing" */,
1283
- );
1240
+ const normalizeErrorAndClose = (error: unknown): IFluidErrorBase => {
1241
+ const newError = normalizeError(error);
1242
+ this.close(newError);
1243
+ // add resolved URL on error object so that host has the ability to find this document and delete it
1244
+ newError.addTelemetryProperties({
1245
+ resolvedUrl: this.service?.resolvedUrl?.url,
1246
+ });
1247
+ return newError;
1248
+ };
1284
1249
 
1285
- // build a table mapping IDs assigned locally to IDs assigned by storage and pass it to runtime to
1286
- // support blob handles that only know about the local IDs
1287
- const redirectTable = new Map<string, string>();
1288
- // if new blobs are added while uploading, upload them too
1289
- while (redirectTable.size < this.detachedBlobStorage.size) {
1290
- const newIds = this.detachedBlobStorage
1291
- .getBlobIds()
1292
- .filter((id) => !redirectTable.has(id));
1293
- for (const id of newIds) {
1294
- const blob = await this.detachedBlobStorage.readBlob(id);
1295
- const response = await this.storageAdapter.createBlob(blob);
1296
- redirectTable.set(id, response.id);
1250
+ const setAttachmentData: AttachProcessProps["setAttachmentData"] = (
1251
+ attachmentData,
1252
+ ) => {
1253
+ const previousState = this.attachmentData.state;
1254
+ this.attachmentData = attachmentData;
1255
+ const state = this.attachmentData.state;
1256
+ if (state !== previousState && state !== AttachState.Detached) {
1257
+ try {
1258
+ this.runtime.setAttachState(state);
1259
+ this.emit(state.toLocaleLowerCase());
1260
+ } catch (error) {
1261
+ throw normalizeErrorAndClose(error);
1297
1262
  }
1298
1263
  }
1264
+ };
1299
1265
 
1300
- // take summary and upload
1301
- const appSummary: ISummaryTree = this.runtime.createSummary(redirectTable);
1302
- const protocolSummary = this.captureProtocolSummary();
1303
- summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1304
-
1305
- this._attachState = AttachState.Attaching;
1306
- this.runtime.setAttachState(AttachState.Attaching);
1307
- this.emit("attaching");
1308
- if (this.offlineLoadEnabled) {
1309
- const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1310
- this.baseSnapshot = snapshot;
1311
- this.baseSnapshotBlobs =
1312
- getBlobContentsFromTreeWithBlobContents(snapshot);
1266
+ const createAttachmentSummary: AttachProcessProps["createAttachmentSummary"] = (
1267
+ redirectTable?: Map<string, string>,
1268
+ ) => {
1269
+ try {
1270
+ assert(
1271
+ this.deltaManager.inbound.length === 0,
1272
+ 0x0d6 /* "Inbound queue should be empty when attaching" */,
1273
+ );
1274
+ return combineAppAndProtocolSummary(
1275
+ this.runtime.createSummary(redirectTable),
1276
+ this.captureProtocolSummary(),
1277
+ );
1278
+ } catch (error) {
1279
+ throw normalizeErrorAndClose(error);
1313
1280
  }
1281
+ };
1282
+
1283
+ const createOrGetStorageService: AttachProcessProps["createOrGetStorageService"] =
1284
+ async (summary) => {
1285
+ // Actually go and create the resolved document
1286
+ if (this.service === undefined) {
1287
+ const createNewResolvedUrl =
1288
+ await this.urlResolver.resolve(request);
1289
+ assert(
1290
+ this.client.details.type !== summarizerClientType &&
1291
+ createNewResolvedUrl !== undefined,
1292
+ 0x2c4 /* "client should not be summarizer before container is created" */,
1293
+ );
1294
+ this.service = await runWithRetry(
1295
+ async () =>
1296
+ this.serviceFactory.createContainer(
1297
+ summary,
1298
+ createNewResolvedUrl,
1299
+ this.subLogger,
1300
+ false, // clientIsSummarizer
1301
+ ),
1302
+ "containerAttach",
1303
+ this.mc.logger,
1304
+ {
1305
+ cancel: this._deltaManager.closeAbortController.signal,
1306
+ }, // progress
1307
+ );
1308
+ }
1309
+ this.storageAdapter.connectToService(this.service);
1310
+ return this.storageAdapter;
1311
+ };
1312
+
1313
+ let attachP = runRetriableAttachProcess({
1314
+ initialAttachmentData: this.attachmentData,
1315
+ offlineLoadEnabled: this.offlineLoadEnabled,
1316
+ detachedBlobStorage: this.detachedBlobStorage,
1317
+ setAttachmentData,
1318
+ createAttachmentSummary,
1319
+ createOrGetStorageService,
1320
+ });
1314
1321
 
1315
- await this.storageAdapter.uploadSummaryWithContext(summary, {
1316
- referenceSequenceNumber: 0,
1317
- ackHandle: undefined,
1318
- proposalHandle: undefined,
1322
+ // only enable the new behavior if the config is set
1323
+ if (
1324
+ this.mc.config.getBoolean("Fluid.Container.RetryOnAttachFailure") !== true
1325
+ ) {
1326
+ attachP = attachP.catch((error) => {
1327
+ throw normalizeErrorAndClose(error);
1319
1328
  });
1320
1329
  }
1321
1330
 
1322
- this._attachState = AttachState.Attached;
1323
- this.runtime.setAttachState(AttachState.Attached);
1324
- this.emit("attached");
1331
+ await attachP;
1325
1332
 
1326
1333
  if (!this.closed) {
1327
1334
  this.handleDeltaConnectionArg(
@@ -1332,17 +1339,11 @@ export class Container
1332
1339
  attachProps?.deltaConnection,
1333
1340
  );
1334
1341
  }
1335
- } catch (error) {
1336
- // add resolved URL on error object so that host has the ability to find this document and delete it
1337
- const newError = normalizeError(error);
1338
- newError.addTelemetryProperties({ resolvedUrl: this.resolvedUrl?.url });
1339
- this.close(newError);
1340
- throw newError;
1341
- }
1342
- },
1343
- { start: true, end: true, cancel: "generic" },
1344
- );
1345
- }
1342
+ },
1343
+ { start: true, end: true, cancel: "generic" },
1344
+ );
1345
+ },
1346
+ );
1346
1347
 
1347
1348
  private setAutoReconnectInternal(mode: ReconnectMode, reason: IConnectionStateChangeReason) {
1348
1349
  const currentMode = this._deltaManager.connectionManager.reconnectMode;
@@ -1369,7 +1370,7 @@ export class Container
1369
1370
  public connect() {
1370
1371
  if (this.closed) {
1371
1372
  throw new UsageError(`The Container is closed and cannot be connected`);
1372
- } else if (this._attachState !== AttachState.Attached) {
1373
+ } else if (this.attachState !== AttachState.Attached) {
1373
1374
  throw new UsageError(`The Container is not attached and cannot be connected`);
1374
1375
  } else if (!this.connected) {
1375
1376
  // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
@@ -1385,7 +1386,7 @@ export class Container
1385
1386
  private connectInternal(args: IConnectionArgs) {
1386
1387
  assert(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
1387
1388
  assert(
1388
- this._attachState === AttachState.Attached,
1389
+ this.attachState === AttachState.Attached,
1389
1390
  0x2c6 /* "Attempting to connect() a container that is not attached" */,
1390
1391
  );
1391
1392
 
@@ -1534,6 +1535,22 @@ export class Container
1534
1535
  this._deltaManager.connect(args);
1535
1536
  }
1536
1537
 
1538
+ private readonly metadataUpdateHandler = (metadata: Record<string, string>) => {
1539
+ this._containerMetadata = { ...this._containerMetadata, ...metadata };
1540
+ this.emit("metadataUpdate", metadata);
1541
+ };
1542
+
1543
+ private async createDocumentService(
1544
+ serviceProvider: () => Promise<IDocumentService>,
1545
+ ): Promise<IDocumentService> {
1546
+ const service = await serviceProvider();
1547
+ // Back-compat for Old driver
1548
+ if (service.on !== undefined) {
1549
+ service.on("metadataUpdate", this.metadataUpdateHandler);
1550
+ }
1551
+ return service;
1552
+ }
1553
+
1537
1554
  /**
1538
1555
  * Load container.
1539
1556
  *
@@ -1547,10 +1564,12 @@ export class Container
1547
1564
  loadToSequenceNumber: number | undefined,
1548
1565
  ) {
1549
1566
  const timings: Record<string, number> = { phase1: performance.now() };
1550
- this.service = await this.serviceFactory.createDocumentService(
1551
- resolvedUrl,
1552
- this.subLogger,
1553
- this.client.details.type === summarizerClientType,
1567
+ this.service = await this.createDocumentService(async () =>
1568
+ this.serviceFactory.createDocumentService(
1569
+ resolvedUrl,
1570
+ this.subLogger,
1571
+ this.client.details.type === summarizerClientType,
1572
+ ),
1554
1573
  );
1555
1574
 
1556
1575
  // Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
@@ -1573,33 +1592,42 @@ export class Container
1573
1592
 
1574
1593
  this.storageAdapter.connectToService(this.service);
1575
1594
 
1576
- this._attachState = AttachState.Attached;
1595
+ this.attachmentData = {
1596
+ state: AttachState.Attached,
1597
+ };
1577
1598
 
1578
1599
  timings.phase2 = performance.now();
1579
1600
  // Fetch specified snapshot.
1580
1601
  const { snapshot, versionId } =
1581
1602
  pendingLocalState === undefined
1582
- ? await this.fetchSnapshotTree(specifiedVersion)
1603
+ ? await this.fetchSnapshot(specifiedVersion)
1583
1604
  : { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
1584
1605
 
1606
+ const snapshotTree: ISnapshotTree | undefined = isInstanceOfISnapshot(snapshot)
1607
+ ? snapshot.snapshotTree
1608
+ : snapshot;
1585
1609
  if (pendingLocalState) {
1586
- this.baseSnapshot = pendingLocalState.baseSnapshot;
1587
- this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
1610
+ this.attachmentData = {
1611
+ state: AttachState.Attached,
1612
+ snapshot: {
1613
+ tree: pendingLocalState.baseSnapshot,
1614
+ blobs: pendingLocalState.snapshotBlobs,
1615
+ },
1616
+ };
1588
1617
  } else {
1589
- assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
1618
+ assert(snapshotTree !== undefined, 0x237 /* "Snapshot should exist" */);
1590
1619
  if (this.offlineLoadEnabled) {
1591
- this.baseSnapshot = snapshot;
1592
- // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1593
- this.baseSnapshotBlobs = await getBlobContentsFromTree(
1594
- snapshot,
1595
- this.storageAdapter,
1596
- );
1620
+ const blobs = await getBlobContentsFromTree(snapshotTree, this.storageAdapter);
1621
+ this.attachmentData = {
1622
+ state: AttachState.Attached,
1623
+ snapshot: { tree: snapshotTree, blobs },
1624
+ };
1597
1625
  }
1598
1626
  }
1599
1627
 
1600
1628
  const attributes: IDocumentAttributes = await this.getDocumentAttributes(
1601
1629
  this.storageAdapter,
1602
- snapshot,
1630
+ snapshotTree,
1603
1631
  );
1604
1632
 
1605
1633
  // If we saved ops, we will replay them and don't need DeltaManager to fetch them
@@ -1686,15 +1714,20 @@ export class Container
1686
1714
 
1687
1715
  // ...load in the existing quorum
1688
1716
  // Initialize the protocol handler
1689
- await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
1717
+ await this.initializeProtocolStateFromSnapshot(
1718
+ attributes,
1719
+ this.storageAdapter,
1720
+ snapshotTree,
1721
+ );
1690
1722
 
1691
1723
  timings.phase3 = performance.now();
1692
1724
  const codeDetails = this.getCodeDetailsFromQuorum();
1693
1725
  await this.instantiateRuntime(
1694
1726
  codeDetails,
1695
- snapshot,
1727
+ snapshotTree,
1696
1728
  // give runtime a dummy value so it knows we're loading from a stash blob
1697
1729
  pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined,
1730
+ isInstanceOfISnapshot(snapshot) ? snapshot : undefined,
1698
1731
  );
1699
1732
 
1700
1733
  // replay saved ops
@@ -1807,24 +1840,30 @@ export class Container
1807
1840
  this.setLoaded();
1808
1841
  }
1809
1842
 
1810
- private async rehydrateDetachedFromSnapshot(detachedContainerSnapshot: ISummaryTree) {
1811
- if (detachedContainerSnapshot.tree[hasBlobsSummaryTree] !== undefined) {
1843
+ private async rehydrateDetachedFromSnapshot({
1844
+ attached,
1845
+ baseSnapshot,
1846
+ snapshotBlobs,
1847
+ hasAttachmentBlobs,
1848
+ }: IPendingDetachedContainerState) {
1849
+ if (hasAttachmentBlobs) {
1812
1850
  assert(
1813
1851
  !!this.detachedBlobStorage && this.detachedBlobStorage.size > 0,
1814
1852
  0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */,
1815
1853
  );
1816
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1817
- delete detachedContainerSnapshot.tree[hasBlobsSummaryTree];
1818
1854
  }
1819
-
1820
- const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1821
- this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1822
- const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
1855
+ const snapshotTreeWithBlobContents: ISnapshotTreeWithBlobContents =
1856
+ combineSnapshotTreeAndSnapshotBlobs(baseSnapshot, snapshotBlobs);
1857
+ this.storageAdapter.loadSnapshotFromSnapshotBlobs(snapshotBlobs);
1858
+ const attributes = await this.getDocumentAttributes(
1859
+ this.storageAdapter,
1860
+ snapshotTreeWithBlobContents,
1861
+ );
1823
1862
 
1824
1863
  await this.attachDeltaManagerOpHandler(attributes);
1825
1864
 
1826
1865
  // Initialize the protocol handler
1827
- const baseTree = getProtocolSnapshotTree(snapshotTree);
1866
+ const baseTree = getProtocolSnapshotTree(snapshotTreeWithBlobContents);
1828
1867
  const qValues = await readAndParse<[string, ICommittedProposal][]>(
1829
1868
  this.storageAdapter,
1830
1869
  baseTree.blobs.quorumValues,
@@ -1839,7 +1878,7 @@ export class Container
1839
1878
  );
1840
1879
  const codeDetails = this.getCodeDetailsFromQuorum();
1841
1880
 
1842
- await this.instantiateRuntime(codeDetails, snapshotTree);
1881
+ await this.instantiateRuntime(codeDetails, snapshotTreeWithBlobContents);
1843
1882
 
1844
1883
  this.setLoaded();
1845
1884
  }
@@ -2383,10 +2422,39 @@ export class Container
2383
2422
  return { snapshot, versionId: version?.id };
2384
2423
  }
2385
2424
 
2425
+ private async fetchSnapshot(
2426
+ specifiedVersion: string | undefined,
2427
+ ): Promise<{ snapshot?: ISnapshot | ISnapshotTree; versionId?: string }> {
2428
+ if (
2429
+ this.mc.config.getBoolean("Fluid.Container.FetchSnapshotUsingGetSnapshotApi") ===
2430
+ true &&
2431
+ this.service?.policies?.supportGetSnapshotApi === true
2432
+ ) {
2433
+ const snapshot = await this.storageAdapter.getSnapshot({
2434
+ versionId: specifiedVersion,
2435
+ });
2436
+ const version: IVersion = {
2437
+ id: snapshot.snapshotTree.id ?? "",
2438
+ treeId: snapshot.snapshotTree.id ?? "",
2439
+ };
2440
+ this._loadedFromVersion = version;
2441
+
2442
+ if (snapshot === undefined && specifiedVersion !== undefined) {
2443
+ this.mc.logger.sendErrorEvent({
2444
+ eventName: "getSnapshotTreeFailed",
2445
+ id: version.id,
2446
+ });
2447
+ }
2448
+ return { snapshot, versionId: version.id };
2449
+ }
2450
+ return this.fetchSnapshotTree(specifiedVersion);
2451
+ }
2452
+
2386
2453
  private async instantiateRuntime(
2387
2454
  codeDetails: IFluidCodeDetails,
2388
- snapshot: ISnapshotTree | undefined,
2455
+ snapshotTree: ISnapshotTree | undefined,
2389
2456
  pendingLocalState?: unknown,
2457
+ snapshot?: ISnapshot,
2390
2458
  ) {
2391
2459
  assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
2392
2460
 
@@ -2420,12 +2488,12 @@ export class Container
2420
2488
  (this.protocolHandler.quorum.get("code") ??
2421
2489
  this.protocolHandler.quorum.get("code2")) as IFluidCodeDetails | undefined;
2422
2490
 
2423
- const existing = snapshot !== undefined;
2491
+ const existing = snapshotTree !== undefined;
2424
2492
 
2425
2493
  const context = new ContainerContext(
2426
2494
  this.options,
2427
2495
  this.scope,
2428
- snapshot,
2496
+ snapshotTree,
2429
2497
  this._loadedFromVersion,
2430
2498
  this._deltaManager,
2431
2499
  this.storageAdapter,
@@ -2452,6 +2520,7 @@ export class Container
2452
2520
  existing,
2453
2521
  this.subLogger,
2454
2522
  pendingLocalState,
2523
+ snapshot,
2455
2524
  );
2456
2525
 
2457
2526
  this._runtime = await PerformanceEvent.timedExecAsync(