@fluidframework/container-loader 2.0.0-rc.3.0.2 → 2.0.0-rc.4.0.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 (188) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/api-report/container-loader.api.md +5 -1
  3. package/dist/attachment.d.ts +3 -2
  4. package/dist/attachment.d.ts.map +1 -1
  5. package/dist/attachment.js +5 -5
  6. package/dist/attachment.js.map +1 -1
  7. package/dist/audience.d.ts +6 -4
  8. package/dist/audience.d.ts.map +1 -1
  9. package/dist/audience.js +18 -3
  10. package/dist/audience.js.map +1 -1
  11. package/dist/catchUpMonitor.d.ts +1 -1
  12. package/dist/catchUpMonitor.d.ts.map +1 -1
  13. package/dist/catchUpMonitor.js.map +1 -1
  14. package/dist/connectionManager.d.ts +7 -3
  15. package/dist/connectionManager.d.ts.map +1 -1
  16. package/dist/connectionManager.js +57 -38
  17. package/dist/connectionManager.js.map +1 -1
  18. package/dist/connectionStateHandler.d.ts +31 -10
  19. package/dist/connectionStateHandler.d.ts.map +1 -1
  20. package/dist/connectionStateHandler.js +49 -36
  21. package/dist/connectionStateHandler.js.map +1 -1
  22. package/dist/container.d.ts +22 -13
  23. package/dist/container.d.ts.map +1 -1
  24. package/dist/container.js +145 -117
  25. package/dist/container.js.map +1 -1
  26. package/dist/containerContext.d.ts +3 -3
  27. package/dist/containerContext.d.ts.map +1 -1
  28. package/dist/containerContext.js.map +1 -1
  29. package/dist/containerStorageAdapter.d.ts +12 -3
  30. package/dist/containerStorageAdapter.d.ts.map +1 -1
  31. package/dist/containerStorageAdapter.js +42 -4
  32. package/dist/containerStorageAdapter.js.map +1 -1
  33. package/dist/contracts.d.ts +2 -2
  34. package/dist/contracts.d.ts.map +1 -1
  35. package/dist/contracts.js.map +1 -1
  36. package/dist/debugLogger.d.ts +1 -2
  37. package/dist/debugLogger.d.ts.map +1 -1
  38. package/dist/debugLogger.js.map +1 -1
  39. package/dist/deltaManager.d.ts +5 -6
  40. package/dist/deltaManager.d.ts.map +1 -1
  41. package/dist/deltaManager.js +29 -24
  42. package/dist/deltaManager.js.map +1 -1
  43. package/dist/deltaQueue.d.ts +1 -1
  44. package/dist/deltaQueue.d.ts.map +1 -1
  45. package/dist/deltaQueue.js.map +1 -1
  46. package/dist/error.d.ts +1 -2
  47. package/dist/error.d.ts.map +1 -1
  48. package/dist/error.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/legacy.d.ts +2 -2
  54. package/dist/loadPaused.d.ts +35 -0
  55. package/dist/loadPaused.d.ts.map +1 -0
  56. package/dist/loadPaused.js +115 -0
  57. package/dist/loadPaused.js.map +1 -0
  58. package/dist/loader.d.ts +1 -1
  59. package/dist/loader.d.ts.map +1 -1
  60. package/dist/loader.js +1 -14
  61. package/dist/loader.js.map +1 -1
  62. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  63. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js +4 -4
  64. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  65. package/dist/packageVersion.d.ts +1 -1
  66. package/dist/packageVersion.js +1 -1
  67. package/dist/packageVersion.js.map +1 -1
  68. package/dist/protocol.d.ts.map +1 -1
  69. package/dist/protocol.js +3 -0
  70. package/dist/protocol.js.map +1 -1
  71. package/dist/public.d.ts +1 -1
  72. package/dist/retriableDocumentStorageService.d.ts +1 -1
  73. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  74. package/dist/retriableDocumentStorageService.js.map +1 -1
  75. package/dist/serializedStateManager.d.ts +89 -9
  76. package/dist/serializedStateManager.d.ts.map +1 -1
  77. package/dist/serializedStateManager.js +150 -34
  78. package/dist/serializedStateManager.js.map +1 -1
  79. package/dist/utils.d.ts +11 -1
  80. package/dist/utils.d.ts.map +1 -1
  81. package/dist/utils.js +29 -14
  82. package/dist/utils.js.map +1 -1
  83. package/lib/attachment.d.ts +3 -2
  84. package/lib/attachment.d.ts.map +1 -1
  85. package/lib/attachment.js +5 -5
  86. package/lib/attachment.js.map +1 -1
  87. package/lib/audience.d.ts +6 -4
  88. package/lib/audience.d.ts.map +1 -1
  89. package/lib/audience.js +19 -4
  90. package/lib/audience.js.map +1 -1
  91. package/lib/catchUpMonitor.d.ts +1 -1
  92. package/lib/catchUpMonitor.d.ts.map +1 -1
  93. package/lib/catchUpMonitor.js.map +1 -1
  94. package/lib/connectionManager.d.ts +7 -3
  95. package/lib/connectionManager.d.ts.map +1 -1
  96. package/lib/connectionManager.js +36 -17
  97. package/lib/connectionManager.js.map +1 -1
  98. package/lib/connectionStateHandler.d.ts +31 -10
  99. package/lib/connectionStateHandler.d.ts.map +1 -1
  100. package/lib/connectionStateHandler.js +49 -36
  101. package/lib/connectionStateHandler.js.map +1 -1
  102. package/lib/container.d.ts +22 -13
  103. package/lib/container.d.ts.map +1 -1
  104. package/lib/container.js +146 -118
  105. package/lib/container.js.map +1 -1
  106. package/lib/containerContext.d.ts +3 -3
  107. package/lib/containerContext.d.ts.map +1 -1
  108. package/lib/containerContext.js.map +1 -1
  109. package/lib/containerStorageAdapter.d.ts +12 -3
  110. package/lib/containerStorageAdapter.d.ts.map +1 -1
  111. package/lib/containerStorageAdapter.js +42 -4
  112. package/lib/containerStorageAdapter.js.map +1 -1
  113. package/lib/contracts.d.ts +2 -2
  114. package/lib/contracts.d.ts.map +1 -1
  115. package/lib/contracts.js +1 -1
  116. package/lib/contracts.js.map +1 -1
  117. package/lib/debugLogger.d.ts +1 -2
  118. package/lib/debugLogger.d.ts.map +1 -1
  119. package/lib/debugLogger.js.map +1 -1
  120. package/lib/deltaManager.d.ts +5 -6
  121. package/lib/deltaManager.d.ts.map +1 -1
  122. package/lib/deltaManager.js +10 -5
  123. package/lib/deltaManager.js.map +1 -1
  124. package/lib/deltaQueue.d.ts +1 -1
  125. package/lib/deltaQueue.d.ts.map +1 -1
  126. package/lib/deltaQueue.js.map +1 -1
  127. package/lib/error.d.ts +1 -2
  128. package/lib/error.d.ts.map +1 -1
  129. package/lib/error.js.map +1 -1
  130. package/lib/index.d.ts +1 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +1 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/legacy.d.ts +2 -2
  135. package/lib/loadPaused.d.ts +35 -0
  136. package/lib/loadPaused.d.ts.map +1 -0
  137. package/lib/loadPaused.js +111 -0
  138. package/lib/loadPaused.js.map +1 -0
  139. package/lib/loader.d.ts +1 -1
  140. package/lib/loader.d.ts.map +1 -1
  141. package/lib/loader.js +3 -16
  142. package/lib/loader.js.map +1 -1
  143. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  144. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js +1 -1
  145. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  146. package/lib/packageVersion.d.ts +1 -1
  147. package/lib/packageVersion.js +1 -1
  148. package/lib/packageVersion.js.map +1 -1
  149. package/lib/protocol.d.ts.map +1 -1
  150. package/lib/protocol.js +3 -0
  151. package/lib/protocol.js.map +1 -1
  152. package/lib/public.d.ts +1 -1
  153. package/lib/retriableDocumentStorageService.d.ts +1 -1
  154. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  155. package/lib/retriableDocumentStorageService.js +1 -1
  156. package/lib/retriableDocumentStorageService.js.map +1 -1
  157. package/lib/serializedStateManager.d.ts +89 -9
  158. package/lib/serializedStateManager.d.ts.map +1 -1
  159. package/lib/serializedStateManager.js +146 -30
  160. package/lib/serializedStateManager.js.map +1 -1
  161. package/lib/tsdoc-metadata.json +1 -1
  162. package/lib/utils.d.ts +11 -1
  163. package/lib/utils.d.ts.map +1 -1
  164. package/lib/utils.js +15 -1
  165. package/lib/utils.js.map +1 -1
  166. package/package.json +24 -21
  167. package/src/attachment.ts +12 -13
  168. package/src/audience.ts +30 -9
  169. package/src/catchUpMonitor.ts +1 -1
  170. package/src/connectionManager.ts +45 -22
  171. package/src/connectionStateHandler.ts +78 -45
  172. package/src/container.ts +181 -160
  173. package/src/containerContext.ts +2 -2
  174. package/src/containerStorageAdapter.ts +61 -6
  175. package/src/contracts.ts +5 -4
  176. package/src/debugLogger.ts +1 -1
  177. package/src/deltaManager.ts +15 -8
  178. package/src/deltaQueue.ts +1 -1
  179. package/src/error.ts +1 -1
  180. package/src/index.ts +1 -0
  181. package/src/loadPaused.ts +140 -0
  182. package/src/loader.ts +6 -23
  183. package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +1 -1
  184. package/src/packageVersion.ts +1 -1
  185. package/src/protocol.ts +4 -0
  186. package/src/retriableDocumentStorageService.ts +5 -2
  187. package/src/serializedStateManager.ts +215 -48
  188. package/src/utils.ts +19 -1
@@ -7,30 +7,36 @@ import {
7
7
  IGetPendingLocalStateProps,
8
8
  IRuntime,
9
9
  } from "@fluidframework/container-definitions/internal";
10
+ import { stringToBuffer } from "@fluid-internal/client-utils";
10
11
  import { assert } from "@fluidframework/core-utils/internal";
11
12
  import {
13
+ FetchSource,
12
14
  IDocumentStorageService,
13
15
  IResolvedUrl,
14
16
  ISnapshot,
15
17
  } from "@fluidframework/driver-definitions/internal";
16
- import { isInstanceOfISnapshot } from "@fluidframework/driver-utils/internal";
18
+ import { getSnapshotTree } from "@fluidframework/driver-utils/internal";
17
19
  import {
18
20
  type IDocumentAttributes,
19
21
  ISequencedDocumentMessage,
20
22
  ISnapshotTree,
21
23
  IVersion,
22
24
  } from "@fluidframework/protocol-definitions";
23
- import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
24
25
  import {
25
26
  MonitoringContext,
26
27
  PerformanceEvent,
27
28
  UsageError,
28
29
  createChildMonitoringContext,
29
30
  } from "@fluidframework/telemetry-utils/internal";
30
-
31
+ import type { ITelemetryBaseLogger, IEventProvider, IEvent } from "@fluidframework/core-interfaces";
31
32
  import { ISerializableBlobContents, getBlobContentsFromTree } from "./containerStorageAdapter.js";
32
- import { getDocumentAttributes } from "./utils.js";
33
+ import { convertSnapshotToSnapshotInfo, getDocumentAttributes } from "./utils.js";
33
34
 
35
+ /**
36
+ * This is very similar to {@link @fluidframework/protocol-definitions/internal#ISnapshot}, but the difference is
37
+ * that the blobs of ISnapshot are of type ArrayBufferLike, while the blobs of this interface are serializable because
38
+ * they are already converted to string.
39
+ */
34
40
  export interface SnapshotWithBlobs {
35
41
  /**
36
42
  * Snapshot from which container initially loaded.
@@ -42,21 +48,38 @@ export interface SnapshotWithBlobs {
42
48
  */
43
49
  snapshotBlobs: ISerializableBlobContents;
44
50
  }
51
+
45
52
  /**
46
53
  * State saved by a container at close time, to be used to load a new instance
47
54
  * of the container to the same state
55
+ *
56
+ * This is very similar to {@link @fluidframework/protocol-definitions/internal#ISnapshot}, but the difference is
57
+ * that the blobs of ISnapshot are of type ArrayBufferLike, while the blobs of this interface are serializable because
58
+ * they are already converted to string.
59
+ *
48
60
  * @internal
49
61
  */
50
62
  export interface IPendingContainerState extends SnapshotWithBlobs {
63
+ /** This container was attached (as opposed to IPendingDetachedContainerState.attached which is false) */
51
64
  attached: true;
65
+ /**
66
+ * Runtime-specific state that will be needed to properly rehydrate
67
+ * (it's included in ContainerContext passed to instantiateRuntime)
68
+ */
52
69
  pendingRuntimeState: unknown;
70
+ /**
71
+ * Any group snapshots (aka delay-loaded) we've downloaded from the service for this container
72
+ */
73
+ loadedGroupIdSnapshots?: Record<string, ISnapshotInfo>;
53
74
  /**
54
75
  * All ops since base snapshot sequence number up to the latest op
55
76
  * seen when the container was closed. Used to apply stashed (saved pending)
56
77
  * ops at the same sequence number at which they were made.
57
78
  */
58
79
  savedOps: ISequencedDocumentMessage[];
80
+ /** The Container's URL in the service, needed to hook up the driver during rehydration */
59
81
  url: string;
82
+ /** If the Container was connected when serialized, its clientId. Used as the initial clientId upon rehydration, until reconnected. */
60
83
  clientId?: string;
61
84
  }
62
85
 
@@ -66,42 +89,91 @@ export interface IPendingContainerState extends SnapshotWithBlobs {
66
89
  * @internal
67
90
  */
68
91
  export interface IPendingDetachedContainerState extends SnapshotWithBlobs {
92
+ /** This container was not attached (as opposed to IPendingContainerState.attached which is true) */
69
93
  attached: false;
94
+ /** Indicates whether we expect the rehydrated container to have non-empty Detached Blob Storage */
70
95
  hasAttachmentBlobs: boolean;
96
+ /**
97
+ * Runtime-specific state that will be needed to properly rehydrate
98
+ * (it's included in ContainerContext passed to instantiateRuntime)
99
+ */
71
100
  pendingRuntimeState?: unknown;
72
101
  }
73
102
 
74
103
  export interface ISnapshotInfo extends SnapshotWithBlobs {
75
104
  snapshotSequenceNumber: number;
105
+ snapshotFetchedTime?: number | undefined;
106
+ }
107
+
108
+ export type ISerializedStateManagerDocumentStorageService = Pick<
109
+ IDocumentStorageService,
110
+ "getSnapshot" | "getSnapshotTree" | "getVersions" | "readBlob"
111
+ > & {
112
+ loadedGroupIdSnapshots: Record<string, ISnapshot>;
113
+ };
114
+
115
+ interface ISerializerEvent extends IEvent {
116
+ (event: "saved", listener: (dirty: boolean) => void): void;
76
117
  }
77
118
 
119
+ /**
120
+ * Helper class to manage the state of the container needed for proper serialization.
121
+ *
122
+ * It holds the pendingLocalState the container was rehydrated from (if any),
123
+ * as well as the snapshot to be used for serialization.
124
+ * It also keeps track of container dirty state and which local ops have been processed
125
+ */
78
126
  export class SerializedStateManager {
79
127
  private readonly processedOps: ISequencedDocumentMessage[] = [];
80
- private snapshot: ISnapshotInfo | undefined;
81
128
  private readonly mc: MonitoringContext;
129
+ private snapshot: ISnapshotInfo | undefined;
82
130
  private latestSnapshot: ISnapshotInfo | undefined;
83
- private refreshSnapshot: Promise<void> | undefined;
131
+ private refreshSnapshotP: Promise<void> | undefined;
132
+ private readonly lastSavedOpSequenceNumber: number = 0;
84
133
 
134
+ /**
135
+ * @param pendingLocalState - The pendingLocalState being rehydrated, if any (undefined when loading directly from storage)
136
+ * @param subLogger - Container's logger to use as parent for our logger
137
+ * @param storageAdapter - Storage adapter for fetching snapshots
138
+ * @param _offlineLoadEnabled - Is serializing/rehydrating containers allowed?
139
+ * @param containerEvent - Source of the "saved" event when the container has all its pending state uploaded
140
+ * @param containerDirty - Is the container "dirty"? That's the opposite of "saved" - there is pending state that may not have been received yet by the service.
141
+ */
85
142
  constructor(
86
143
  private readonly pendingLocalState: IPendingContainerState | undefined,
87
- subLogger: ITelemetryLoggerExt,
88
- private readonly storageAdapter: Pick<
89
- IDocumentStorageService,
90
- "readBlob" | "getSnapshotTree" | "getSnapshot" | "getVersions"
91
- >,
144
+ subLogger: ITelemetryBaseLogger,
145
+ private readonly storageAdapter: ISerializedStateManagerDocumentStorageService,
92
146
  private readonly _offlineLoadEnabled: boolean,
93
- private readonly newSnapshotFetched?: () => void,
147
+ containerEvent: IEventProvider<ISerializerEvent>,
148
+ private readonly containerDirty: () => boolean,
94
149
  ) {
95
150
  this.mc = createChildMonitoringContext({
96
151
  logger: subLogger,
97
152
  namespace: "serializedStateManager",
98
153
  });
154
+
155
+ if (pendingLocalState && pendingLocalState.savedOps.length > 0) {
156
+ const savedOpsSize = pendingLocalState.savedOps.length;
157
+ this.lastSavedOpSequenceNumber =
158
+ pendingLocalState.savedOps[savedOpsSize - 1].sequenceNumber;
159
+ }
160
+ containerEvent.once("saved", () => this.updateSnapshotAndProcessedOpsMaybe());
99
161
  }
100
162
 
101
163
  public get offlineLoadEnabled(): boolean {
102
164
  return this._offlineLoadEnabled;
103
165
  }
104
166
 
167
+ /**
168
+ * Promise that will resolve (or reject) once we've tried to download the latest snapshot(s) from storage
169
+ */
170
+ public get waitForInitialRefresh(): Promise<void> | undefined {
171
+ return this.refreshSnapshotP;
172
+ }
173
+
174
+ /**
175
+ * Called whenever an incoming op is processed by the Container
176
+ */
105
177
  public addProcessedOp(message: ISequencedDocumentMessage) {
106
178
  if (this.offlineLoadEnabled) {
107
179
  this.processedOps.push(message);
@@ -109,26 +181,40 @@ export class SerializedStateManager {
109
181
  }
110
182
  }
111
183
 
184
+ /**
185
+ * This wraps the basic functionality of fetching the snapshot for this container during Container load.
186
+ *
187
+ * If we have pendingLocalState, we get the snapshot from there.
188
+ * Otherwise, fetch it from storage (according to specifiedVersion if provided)
189
+ *
190
+ * @param specifiedVersion - If a version is specified and we don't have pendingLocalState, fetch this version from storage
191
+ * @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree.
192
+ * @returns The snapshot to boot the container from
193
+ */
112
194
  public async fetchSnapshot(
113
195
  specifiedVersion: string | undefined,
114
196
  supportGetSnapshotApi: boolean,
115
197
  ) {
116
198
  if (this.pendingLocalState === undefined) {
117
- const { baseSnapshot, version } = await getSnapshotTree(
199
+ const { baseSnapshot, version } = await getSnapshot(
118
200
  this.mc,
119
201
  this.storageAdapter,
120
202
  supportGetSnapshotApi,
121
203
  specifiedVersion,
122
204
  );
205
+ const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
123
206
  // non-interactive clients will not have any pending state we want to save
124
207
  if (this.offlineLoadEnabled) {
125
208
  const snapshotBlobs = await getBlobContentsFromTree(
126
- baseSnapshot,
209
+ baseSnapshotTree,
210
+ this.storageAdapter,
211
+ );
212
+ const attributes = await getDocumentAttributes(
127
213
  this.storageAdapter,
214
+ baseSnapshotTree,
128
215
  );
129
- const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshot);
130
216
  this.snapshot = {
131
- baseSnapshot,
217
+ baseSnapshot: baseSnapshotTree,
132
218
  snapshotBlobs,
133
219
  snapshotSequenceNumber: attributes.sequenceNumber,
134
220
  };
@@ -142,25 +228,83 @@ export class SerializedStateManager {
142
228
  snapshotBlobs,
143
229
  snapshotSequenceNumber: attributes.sequenceNumber,
144
230
  };
145
- this.refreshSnapshot ??= (async () => {
146
- this.latestSnapshot = await getLatestSnapshotInfo(
147
- this.mc,
148
- this.storageAdapter,
149
- supportGetSnapshotApi,
150
- );
151
- this.newSnapshotFetched?.();
152
- this.updateSnapshotAndProcessedOpsMaybe();
153
- })();
154
231
 
155
- return { baseSnapshot, version: undefined };
232
+ if (
233
+ this.refreshSnapshotP === undefined &&
234
+ this.mc.config.getBoolean("Fluid.Container.enableOfflineSnapshotRefresh") === true
235
+ ) {
236
+ // Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation
237
+ this.refreshSnapshotP = this.refreshLatestSnapshot(supportGetSnapshotApi);
238
+ this.refreshSnapshotP.catch((e) => {
239
+ this.mc.logger.sendErrorEvent({
240
+ eventName: "RefreshLatestSnapshotFailed",
241
+ error: e,
242
+ });
243
+ });
244
+ }
245
+
246
+ const blobContents = new Map<string, ArrayBuffer>();
247
+ for (const [id, value] of Object.entries(snapshotBlobs)) {
248
+ blobContents.set(id, stringToBuffer(value, "utf8"));
249
+ }
250
+ const iSnapshot: ISnapshot = {
251
+ sequenceNumber: this.snapshot.snapshotSequenceNumber,
252
+ snapshotTree: baseSnapshot,
253
+ blobContents,
254
+ latestSequenceNumber: undefined,
255
+ ops: [],
256
+ snapshotFormatV: 1,
257
+ };
258
+ return { baseSnapshot: iSnapshot, version: undefined };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Fetch the latest snapshot for the container, including delay-loaded groupIds if pendingLocalState was provided and contained any groupIds.
264
+ * Note that this will update the StorageAdapter's cached snapshots for the groupIds (if present)
265
+ *
266
+ * @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree (must be true to fetch by groupIds)
267
+ */
268
+ private async refreshLatestSnapshot(supportGetSnapshotApi: boolean): Promise<void> {
269
+ this.latestSnapshot = await getLatestSnapshotInfo(
270
+ this.mc,
271
+ this.storageAdapter,
272
+ supportGetSnapshotApi,
273
+ );
274
+
275
+ // These are loading groupIds that the containerRuntime has requested over its lifetime.
276
+ // We will fetch the latest snapshot for the groupIds, which will update storageAdapter.loadedGroupIdSnapshots's cache
277
+ const downloadedGroupIds = Object.keys(this.storageAdapter.loadedGroupIdSnapshots);
278
+ if (supportGetSnapshotApi && downloadedGroupIds.length > 0) {
279
+ assert(
280
+ this.storageAdapter.getSnapshot !== undefined,
281
+ 0x972 /* getSnapshot should exist */,
282
+ );
283
+ // (This is a separate network call from above because it requires work for storage to add a special base groupId)
284
+ const snapshot = await this.storageAdapter.getSnapshot({
285
+ versionId: undefined,
286
+ scenarioName: "getLatestSnapshotInfo",
287
+ cacheSnapshot: false,
288
+ loadingGroupIds: downloadedGroupIds,
289
+ fetchSource: FetchSource.noCache,
290
+ });
291
+ assert(snapshot !== undefined, 0x973 /* Snapshot should exist */);
156
292
  }
293
+
294
+ this.updateSnapshotAndProcessedOpsMaybe();
157
295
  }
158
296
 
159
297
  /**
160
298
  * Updates class snapshot and processedOps if we have a new snapshot and it's among processedOps range.
161
299
  */
162
300
  private updateSnapshotAndProcessedOpsMaybe() {
163
- if (this.latestSnapshot === undefined || this.processedOps.length === 0) {
301
+ if (
302
+ this.latestSnapshot === undefined ||
303
+ this.processedOps.length === 0 ||
304
+ this.processedOps[this.processedOps.length - 1].sequenceNumber <
305
+ this.lastSavedOpSequenceNumber ||
306
+ this.containerDirty()
307
+ ) {
164
308
  // can't refresh latest snapshot until we have processed the ops up to it.
165
309
  // Pending state would be behind the latest snapshot.
166
310
  return;
@@ -203,9 +347,10 @@ export class SerializedStateManager {
203
347
  }
204
348
 
205
349
  /**
350
+ * When the Container attaches, we need to stash the initial snapshot (a form of the attach summary).
206
351
  * This method is only meant to be used by Container.attach() to set the initial
207
352
  * base snapshot when attaching.
208
- * @param snapshot - snapshot and blobs collected while attaching
353
+ * @param snapshot - snapshot and blobs collected while attaching (a form of the attach summary)
209
354
  */
210
355
  public setInitialSnapshot(snapshot: SnapshotWithBlobs | undefined) {
211
356
  if (this.offlineLoadEnabled) {
@@ -228,12 +373,16 @@ export class SerializedStateManager {
228
373
  }
229
374
  }
230
375
 
231
- public async getPendingLocalStateCore(
376
+ /**
377
+ * Assembles and serializes the {@link IPendingContainerState} for the container,
378
+ * to be stored and used to rehydrate the container at a later time.
379
+ */
380
+ public async getPendingLocalState(
232
381
  props: IGetPendingLocalStateProps,
233
382
  clientId: string | undefined,
234
383
  runtime: Pick<IRuntime, "getPendingLocalState">,
235
384
  resolvedUrl: IResolvedUrl,
236
- ) {
385
+ ): Promise<string> {
237
386
  return PerformanceEvent.timedExecAsync(
238
387
  this.mc.logger,
239
388
  {
@@ -249,16 +398,32 @@ export class SerializedStateManager {
249
398
  );
250
399
  }
251
400
  assert(this.snapshot !== undefined, 0x8e5 /* no base data */);
252
- const pendingRuntimeState = await runtime.getPendingLocalState(props);
401
+ const pendingRuntimeState = await runtime.getPendingLocalState({
402
+ ...props,
403
+ snapshotSequenceNumber: this.snapshot.snapshotSequenceNumber,
404
+ sessionExpiryTimerStarted: this.snapshot.snapshotFetchedTime,
405
+ });
406
+ // This conversion is required because ArrayBufferLike doesn't survive JSON.stringify
407
+ const loadedGroupIdSnapshots = {};
408
+ let hasGroupIdSnapshots = false;
409
+ const groupIdSnapshots = Object.entries(this.storageAdapter.loadedGroupIdSnapshots);
410
+ if (groupIdSnapshots.length > 0) {
411
+ for (const [groupId, snapshot] of groupIdSnapshots) {
412
+ hasGroupIdSnapshots = true;
413
+ loadedGroupIdSnapshots[groupId] = convertSnapshotToSnapshotInfo(snapshot);
414
+ }
415
+ }
253
416
  const pendingState: IPendingContainerState = {
254
417
  attached: true,
255
418
  pendingRuntimeState,
256
419
  baseSnapshot: this.snapshot.baseSnapshot,
257
420
  snapshotBlobs: this.snapshot.snapshotBlobs,
421
+ loadedGroupIdSnapshots: hasGroupIdSnapshots
422
+ ? loadedGroupIdSnapshots
423
+ : undefined,
258
424
  savedOps: this.processedOps,
259
425
  url: resolvedUrl.url,
260
- // no need to save this if there is no pending runtime state
261
- clientId: pendingRuntimeState !== undefined ? clientId : undefined,
426
+ clientId,
262
427
  };
263
428
 
264
429
  return JSON.stringify(pendingState);
@@ -277,29 +442,34 @@ export class SerializedStateManager {
277
442
  */
278
443
  export async function getLatestSnapshotInfo(
279
444
  mc: MonitoringContext,
280
- storageAdapter: Pick<
281
- IDocumentStorageService,
282
- "getSnapshot" | "getSnapshotTree" | "getVersions" | "readBlob"
283
- >,
445
+ storageAdapter: ISerializedStateManagerDocumentStorageService,
284
446
  supportGetSnapshotApi: boolean,
285
447
  ): Promise<ISnapshotInfo | undefined> {
286
448
  return PerformanceEvent.timedExecAsync(
287
449
  mc.logger,
288
450
  { eventName: "GetLatestSnapshotInfo" },
289
451
  async () => {
290
- const { baseSnapshot } = await getSnapshotTree(
452
+ const { baseSnapshot } = await getSnapshot(
291
453
  mc,
292
454
  storageAdapter,
293
455
  supportGetSnapshotApi,
294
456
  undefined,
295
457
  );
296
- const snapshotBlobs = await getBlobContentsFromTree(baseSnapshot, storageAdapter);
458
+
459
+ const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
460
+ const snapshotFetchedTime = Date.now();
461
+ const snapshotBlobs = await getBlobContentsFromTree(baseSnapshotTree, storageAdapter);
297
462
  const attributes: IDocumentAttributes = await getDocumentAttributes(
298
463
  storageAdapter,
299
- baseSnapshot,
464
+ baseSnapshotTree,
300
465
  );
301
466
  const snapshotSequenceNumber = attributes.sequenceNumber;
302
- return { baseSnapshot, snapshotBlobs, snapshotSequenceNumber };
467
+ return {
468
+ baseSnapshot: baseSnapshotTree,
469
+ snapshotBlobs,
470
+ snapshotSequenceNumber,
471
+ snapshotFetchedTime,
472
+ };
303
473
  },
304
474
  ).catch(() => undefined);
305
475
  }
@@ -313,7 +483,7 @@ export async function getLatestSnapshotInfo(
313
483
  * @param specifiedVersion - An optional version string specifying the version of the snapshot tree to fetch.
314
484
  * @returns - An ISnapshotTree and its version.
315
485
  */
316
- async function getSnapshotTree(
486
+ async function getSnapshot(
317
487
  mc: MonitoringContext,
318
488
  storageAdapter: Pick<
319
489
  IDocumentStorageService,
@@ -321,15 +491,12 @@ async function getSnapshotTree(
321
491
  >,
322
492
  supportGetSnapshotApi: boolean,
323
493
  specifiedVersion: string | undefined,
324
- ): Promise<{ baseSnapshot: ISnapshotTree; version?: IVersion }> {
494
+ ): Promise<{ baseSnapshot: ISnapshot | ISnapshotTree; version?: IVersion }> {
325
495
  const { snapshot, version } = supportGetSnapshotApi
326
496
  ? await fetchISnapshot(mc, storageAdapter, specifiedVersion)
327
497
  : await fetchISnapshotTree(mc, storageAdapter, specifiedVersion);
328
- const baseSnapshot: ISnapshotTree | undefined = isInstanceOfISnapshot(snapshot)
329
- ? snapshot.snapshotTree
330
- : snapshot;
331
- assert(baseSnapshot !== undefined, 0x8e4 /* Snapshot should exist */);
332
- return { baseSnapshot, version };
498
+ assert(snapshot !== undefined, 0x8e4 /* Snapshot should exist */);
499
+ return { baseSnapshot: snapshot, version };
333
500
  }
334
501
 
335
502
  /**
package/src/utils.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { Uint8ArrayToString, bufferToString, stringToBuffer } from "@fluid-internal/client-utils";
7
7
  import { assert, compareArrays, unreachableCase } from "@fluidframework/core-utils/internal";
8
- import { DriverErrorTypes } from "@fluidframework/driver-definitions";
8
+ import { DriverErrorTypes } from "@fluidframework/driver-definitions/internal";
9
9
  import {
10
10
  IDocumentStorageService,
11
11
  type ISnapshot,
@@ -27,6 +27,7 @@ import { v4 as uuid } from "uuid";
27
27
 
28
28
  import { ISerializableBlobContents } from "./containerStorageAdapter.js";
29
29
  import type {
30
+ IPendingContainerState,
30
31
  IPendingDetachedContainerState,
31
32
  ISnapshotInfo,
32
33
  SnapshotWithBlobs,
@@ -313,6 +314,11 @@ function isPendingDetachedContainerState(
313
314
  return true;
314
315
  }
315
316
 
317
+ /**
318
+ * Parses the given string into {@link IPendingDetachedContainerState} format,
319
+ * with validation (if invalid, throws a UsageError).
320
+ * This is the inverse of the JSON.stringify call in {@link Container.serialize}
321
+ */
316
322
  export function getDetachedContainerStateFromSerializedContainer(
317
323
  serializedContainer: string,
318
324
  ): IPendingDetachedContainerState {
@@ -335,6 +341,18 @@ export function getDetachedContainerStateFromSerializedContainer(
335
341
  }
336
342
  }
337
343
 
344
+ /**
345
+ * Blindly parses the given string into {@link IPendingContainerState} format.
346
+ * This is the inverse of the JSON.stringify call in {@link SerializedStateManager.getPendingLocalState}
347
+ */
348
+ export function getAttachedContainerStateFromSerializedContainer(
349
+ serializedContainer: string | undefined,
350
+ ): IPendingContainerState | undefined {
351
+ return serializedContainer !== undefined
352
+ ? (JSON.parse(serializedContainer) as IPendingContainerState)
353
+ : undefined;
354
+ }
355
+
338
356
  /**
339
357
  * Ensures only a single instance of the provided async function is running.
340
358
  * If there are multiple calls they will all get the same promise to wait on.