@fluidframework/container-loader 2.0.0-rc.2.0.1 → 2.0.0-rc.3.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 (246) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/api-report/container-loader.api.md +13 -13
  3. package/dist/attachment.d.ts +6 -9
  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 +1 -1
  8. package/dist/audience.d.ts.map +1 -1
  9. package/dist/audience.js +4 -4
  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 +2 -2
  14. package/dist/catchUpMonitor.js.map +1 -1
  15. package/dist/connectionManager.d.ts +4 -4
  16. package/dist/connectionManager.d.ts.map +1 -1
  17. package/dist/connectionManager.js +48 -43
  18. package/dist/connectionManager.js.map +1 -1
  19. package/dist/connectionStateHandler.d.ts +3 -3
  20. package/dist/connectionStateHandler.d.ts.map +1 -1
  21. package/dist/connectionStateHandler.js +27 -27
  22. package/dist/connectionStateHandler.js.map +1 -1
  23. package/dist/container.d.ts +9 -46
  24. package/dist/container.d.ts.map +1 -1
  25. package/dist/container.js +105 -116
  26. package/dist/container.js.map +1 -1
  27. package/dist/containerContext.d.ts +19 -7
  28. package/dist/containerContext.d.ts.map +1 -1
  29. package/dist/containerContext.js +7 -2
  30. package/dist/containerContext.js.map +1 -1
  31. package/dist/containerStorageAdapter.d.ts +3 -3
  32. package/dist/containerStorageAdapter.d.ts.map +1 -1
  33. package/dist/containerStorageAdapter.js +6 -6
  34. package/dist/containerStorageAdapter.js.map +1 -1
  35. package/dist/contracts.d.ts +4 -3
  36. package/dist/contracts.d.ts.map +1 -1
  37. package/dist/contracts.js +2 -2
  38. package/dist/contracts.js.map +1 -1
  39. package/dist/debugLogger.d.ts +2 -1
  40. package/dist/debugLogger.d.ts.map +1 -1
  41. package/dist/debugLogger.js +4 -4
  42. package/dist/debugLogger.js.map +1 -1
  43. package/dist/deltaManager.d.ts +11 -7
  44. package/dist/deltaManager.d.ts.map +1 -1
  45. package/dist/deltaManager.js +53 -50
  46. package/dist/deltaManager.js.map +1 -1
  47. package/dist/deltaQueue.d.ts +1 -1
  48. package/dist/deltaQueue.d.ts.map +1 -1
  49. package/dist/deltaQueue.js +5 -5
  50. package/dist/deltaQueue.js.map +1 -1
  51. package/dist/error.d.ts +3 -2
  52. package/dist/error.d.ts.map +1 -1
  53. package/dist/error.js +5 -5
  54. package/dist/error.js.map +1 -1
  55. package/dist/legacy.d.ts +29 -0
  56. package/dist/loader.d.ts +4 -4
  57. package/dist/loader.d.ts.map +1 -1
  58. package/dist/loader.js +23 -23
  59. package/dist/loader.js.map +1 -1
  60. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts +2 -2
  61. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  62. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js +2 -2
  63. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  64. package/dist/noopHeuristic.d.ts +1 -1
  65. package/dist/noopHeuristic.d.ts.map +1 -1
  66. package/dist/noopHeuristic.js +6 -6
  67. package/dist/noopHeuristic.js.map +1 -1
  68. package/dist/packageVersion.d.ts +1 -1
  69. package/dist/packageVersion.js +1 -1
  70. package/dist/packageVersion.js.map +1 -1
  71. package/dist/protocol.d.ts +1 -1
  72. package/dist/protocol.d.ts.map +1 -1
  73. package/dist/protocol.js +2 -2
  74. package/dist/protocol.js.map +1 -1
  75. package/dist/protocolTreeDocumentStorageService.d.ts +4 -4
  76. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  77. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  78. package/dist/public.d.ts +14 -0
  79. package/dist/quorum.d.ts +1 -1
  80. package/dist/quorum.d.ts.map +1 -1
  81. package/dist/quorum.js +4 -0
  82. package/dist/quorum.js.map +1 -1
  83. package/dist/retriableDocumentStorageService.d.ts +2 -2
  84. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  85. package/dist/retriableDocumentStorageService.js +7 -7
  86. package/dist/retriableDocumentStorageService.js.map +1 -1
  87. package/dist/serializedStateManager.d.ts +86 -16
  88. package/dist/serializedStateManager.d.ts.map +1 -1
  89. package/dist/serializedStateManager.js +182 -82
  90. package/dist/serializedStateManager.js.map +1 -1
  91. package/dist/utils.d.ts +24 -9
  92. package/dist/utils.d.ts.map +1 -1
  93. package/dist/utils.js +82 -25
  94. package/dist/utils.js.map +1 -1
  95. package/internal.d.ts +11 -0
  96. package/legacy.d.ts +11 -0
  97. package/lib/attachment.d.ts +6 -9
  98. package/lib/attachment.d.ts.map +1 -1
  99. package/lib/attachment.js +1 -1
  100. package/lib/attachment.js.map +1 -1
  101. package/lib/audience.d.ts +1 -1
  102. package/lib/audience.d.ts.map +1 -1
  103. package/lib/audience.js +1 -1
  104. package/lib/audience.js.map +1 -1
  105. package/lib/catchUpMonitor.d.ts +1 -1
  106. package/lib/catchUpMonitor.d.ts.map +1 -1
  107. package/lib/catchUpMonitor.js +1 -1
  108. package/lib/catchUpMonitor.js.map +1 -1
  109. package/lib/connectionManager.d.ts +4 -4
  110. package/lib/connectionManager.d.ts.map +1 -1
  111. package/lib/connectionManager.js +11 -6
  112. package/lib/connectionManager.js.map +1 -1
  113. package/lib/connectionStateHandler.d.ts +3 -3
  114. package/lib/connectionStateHandler.d.ts.map +1 -1
  115. package/lib/connectionStateHandler.js +2 -2
  116. package/lib/connectionStateHandler.js.map +1 -1
  117. package/lib/container.d.ts +9 -46
  118. package/lib/container.d.ts.map +1 -1
  119. package/lib/container.js +37 -48
  120. package/lib/container.js.map +1 -1
  121. package/lib/containerContext.d.ts +19 -7
  122. package/lib/containerContext.d.ts.map +1 -1
  123. package/lib/containerContext.js +7 -2
  124. package/lib/containerContext.js.map +1 -1
  125. package/lib/containerStorageAdapter.d.ts +3 -3
  126. package/lib/containerStorageAdapter.d.ts.map +1 -1
  127. package/lib/containerStorageAdapter.js +2 -2
  128. package/lib/containerStorageAdapter.js.map +1 -1
  129. package/lib/contracts.d.ts +4 -3
  130. package/lib/contracts.d.ts.map +1 -1
  131. package/lib/contracts.js +1 -1
  132. package/lib/contracts.js.map +1 -1
  133. package/lib/debugLogger.d.ts +2 -1
  134. package/lib/debugLogger.d.ts.map +1 -1
  135. package/lib/debugLogger.js +1 -1
  136. package/lib/debugLogger.js.map +1 -1
  137. package/lib/deltaManager.d.ts +11 -7
  138. package/lib/deltaManager.d.ts.map +1 -1
  139. package/lib/deltaManager.js +13 -10
  140. package/lib/deltaManager.js.map +1 -1
  141. package/lib/deltaQueue.d.ts +1 -1
  142. package/lib/deltaQueue.d.ts.map +1 -1
  143. package/lib/deltaQueue.js +2 -2
  144. package/lib/deltaQueue.js.map +1 -1
  145. package/lib/error.d.ts +3 -2
  146. package/lib/error.d.ts.map +1 -1
  147. package/lib/error.js +2 -2
  148. package/lib/error.js.map +1 -1
  149. package/lib/legacy.d.ts +29 -0
  150. package/lib/loader.d.ts +4 -4
  151. package/lib/loader.d.ts.map +1 -1
  152. package/lib/loader.js +4 -4
  153. package/lib/loader.js.map +1 -1
  154. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.ts +2 -2
  155. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  156. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js +2 -2
  157. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  158. package/lib/noopHeuristic.d.ts +1 -1
  159. package/lib/noopHeuristic.d.ts.map +1 -1
  160. package/lib/noopHeuristic.js +2 -2
  161. package/lib/noopHeuristic.js.map +1 -1
  162. package/lib/packageVersion.d.ts +1 -1
  163. package/lib/packageVersion.js +1 -1
  164. package/lib/packageVersion.js.map +1 -1
  165. package/lib/protocol.d.ts +1 -1
  166. package/lib/protocol.d.ts.map +1 -1
  167. package/lib/protocol.js +1 -1
  168. package/lib/protocol.js.map +1 -1
  169. package/lib/protocolTreeDocumentStorageService.d.ts +4 -4
  170. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  171. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  172. package/lib/public.d.ts +14 -0
  173. package/lib/quorum.d.ts +1 -1
  174. package/lib/quorum.d.ts.map +1 -1
  175. package/lib/quorum.js +4 -0
  176. package/lib/quorum.js.map +1 -1
  177. package/lib/retriableDocumentStorageService.d.ts +2 -2
  178. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  179. package/lib/retriableDocumentStorageService.js +3 -3
  180. package/lib/retriableDocumentStorageService.js.map +1 -1
  181. package/lib/serializedStateManager.d.ts +86 -16
  182. package/lib/serializedStateManager.d.ts.map +1 -1
  183. package/lib/serializedStateManager.js +174 -77
  184. package/lib/serializedStateManager.js.map +1 -1
  185. package/lib/utils.d.ts +24 -9
  186. package/lib/utils.d.ts.map +1 -1
  187. package/lib/utils.js +69 -15
  188. package/lib/utils.js.map +1 -1
  189. package/package.json +37 -58
  190. package/src/attachment.ts +10 -8
  191. package/src/audience.ts +3 -2
  192. package/src/catchUpMonitor.ts +2 -2
  193. package/src/connectionManager.ts +27 -20
  194. package/src/connectionStateHandler.ts +7 -7
  195. package/src/container.ts +90 -143
  196. package/src/containerContext.ts +22 -12
  197. package/src/containerStorageAdapter.ts +7 -6
  198. package/src/contracts.ts +4 -5
  199. package/src/debugLogger.ts +3 -4
  200. package/src/deltaManager.ts +40 -30
  201. package/src/deltaQueue.ts +2 -2
  202. package/src/error.ts +5 -4
  203. package/src/loader.ts +25 -23
  204. package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +4 -4
  205. package/src/noopHeuristic.ts +3 -3
  206. package/src/packageVersion.ts +1 -1
  207. package/src/protocol.ts +2 -2
  208. package/src/protocolTreeDocumentStorageService.ts +4 -1
  209. package/src/quorum.ts +2 -1
  210. package/src/retriableDocumentStorageService.ts +6 -5
  211. package/src/serializedStateManager.ts +299 -111
  212. package/src/utils.ts +103 -24
  213. package/api-extractor-cjs.json +0 -8
  214. package/dist/container-loader-alpha.d.ts +0 -275
  215. package/dist/container-loader-beta.d.ts +0 -101
  216. package/dist/container-loader-public.d.ts +0 -101
  217. package/dist/container-loader-untrimmed.d.ts +0 -331
  218. package/lib/container-loader-alpha.d.ts +0 -275
  219. package/lib/container-loader-beta.d.ts +0 -101
  220. package/lib/container-loader-public.d.ts +0 -101
  221. package/lib/container-loader-untrimmed.d.ts +0 -331
  222. package/lib/test/attachment.spec.js +0 -380
  223. package/lib/test/attachment.spec.js.map +0 -1
  224. package/lib/test/catchUpMonitor.spec.js +0 -88
  225. package/lib/test/catchUpMonitor.spec.js.map +0 -1
  226. package/lib/test/connectionManager.spec.js +0 -201
  227. package/lib/test/connectionManager.spec.js.map +0 -1
  228. package/lib/test/connectionStateHandler.spec.js +0 -555
  229. package/lib/test/connectionStateHandler.spec.js.map +0 -1
  230. package/lib/test/container.spec.js +0 -64
  231. package/lib/test/container.spec.js.map +0 -1
  232. package/lib/test/deltaManager.spec.js +0 -405
  233. package/lib/test/deltaManager.spec.js.map +0 -1
  234. package/lib/test/loader.spec.js +0 -212
  235. package/lib/test/loader.spec.js.map +0 -1
  236. package/lib/test/locationRedirectionTests.spec.js +0 -44
  237. package/lib/test/locationRedirectionTests.spec.js.map +0 -1
  238. package/lib/test/serializedStateManager.spec.js +0 -148
  239. package/lib/test/serializedStateManager.spec.js.map +0 -1
  240. package/lib/test/snapshotConversionTest.spec.js +0 -79
  241. package/lib/test/snapshotConversionTest.spec.js.map +0 -1
  242. package/lib/test/types/validateContainerLoaderPrevious.generated.js +0 -38
  243. package/lib/test/types/validateContainerLoaderPrevious.generated.js.map +0 -1
  244. package/lib/test/utils.spec.js +0 -31
  245. package/lib/test/utils.spec.js.map +0 -1
  246. /package/{dist → lib}/tsdoc-metadata.json +0 -0
@@ -4,37 +4,83 @@
4
4
  */
5
5
 
6
6
  import {
7
+ IGetPendingLocalStateProps,
8
+ IRuntime,
9
+ } from "@fluidframework/container-definitions/internal";
10
+ import { assert } from "@fluidframework/core-utils/internal";
11
+ import {
12
+ IDocumentStorageService,
13
+ IResolvedUrl,
14
+ ISnapshot,
15
+ } from "@fluidframework/driver-definitions/internal";
16
+ import { isInstanceOfISnapshot } from "@fluidframework/driver-utils/internal";
17
+ import {
18
+ type IDocumentAttributes,
7
19
  ISequencedDocumentMessage,
8
20
  ISnapshotTree,
9
21
  IVersion,
10
22
  } from "@fluidframework/protocol-definitions";
11
- import { IGetPendingLocalStateProps, IRuntime } from "@fluidframework/container-definitions";
23
+ import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
12
24
  import {
13
- ITelemetryLoggerExt,
14
25
  MonitoringContext,
15
26
  PerformanceEvent,
16
27
  UsageError,
17
28
  createChildMonitoringContext,
18
- } from "@fluidframework/telemetry-utils";
19
- import { assert } from "@fluidframework/core-utils";
20
- import {
21
- IDocumentStorageService,
22
- IResolvedUrl,
23
- ISnapshot,
24
- } from "@fluidframework/driver-definitions";
25
- import { isInstanceOfISnapshot } from "@fluidframework/driver-utils";
29
+ } from "@fluidframework/telemetry-utils/internal";
30
+
26
31
  import { ISerializableBlobContents, getBlobContentsFromTree } from "./containerStorageAdapter.js";
27
- import { IPendingContainerState } from "./container.js";
32
+ import { getDocumentAttributes } from "./utils.js";
33
+
34
+ export interface SnapshotWithBlobs {
35
+ /**
36
+ * Snapshot from which container initially loaded.
37
+ */
38
+ baseSnapshot: ISnapshotTree;
39
+ /**
40
+ * Serializable blobs from the base snapshot. Used to load offline since
41
+ * storage is not available.
42
+ */
43
+ snapshotBlobs: ISerializableBlobContents;
44
+ }
45
+ /**
46
+ * State saved by a container at close time, to be used to load a new instance
47
+ * of the container to the same state
48
+ * @internal
49
+ */
50
+ export interface IPendingContainerState extends SnapshotWithBlobs {
51
+ attached: true;
52
+ pendingRuntimeState: unknown;
53
+ /**
54
+ * All ops since base snapshot sequence number up to the latest op
55
+ * seen when the container was closed. Used to apply stashed (saved pending)
56
+ * ops at the same sequence number at which they were made.
57
+ */
58
+ savedOps: ISequencedDocumentMessage[];
59
+ url: string;
60
+ clientId?: string;
61
+ }
62
+
63
+ /**
64
+ * State saved by a container in detached state, to be used to load a new instance
65
+ * of the container to the same state (rehydrate)
66
+ * @internal
67
+ */
68
+ export interface IPendingDetachedContainerState extends SnapshotWithBlobs {
69
+ attached: false;
70
+ hasAttachmentBlobs: boolean;
71
+ pendingRuntimeState?: unknown;
72
+ }
73
+
74
+ export interface ISnapshotInfo extends SnapshotWithBlobs {
75
+ snapshotSequenceNumber: number;
76
+ }
28
77
 
29
78
  export class SerializedStateManager {
30
79
  private readonly processedOps: ISequencedDocumentMessage[] = [];
31
- private snapshot:
32
- | {
33
- tree: ISnapshotTree;
34
- blobs: ISerializableBlobContents;
35
- }
36
- | undefined;
80
+ private snapshot: ISnapshotInfo | undefined;
37
81
  private readonly mc: MonitoringContext;
82
+ private latestSnapshot: ISnapshotInfo | undefined;
83
+ private refreshSnapshot: Promise<void> | undefined;
38
84
 
39
85
  constructor(
40
86
  private readonly pendingLocalState: IPendingContainerState | undefined,
@@ -44,6 +90,7 @@ export class SerializedStateManager {
44
90
  "readBlob" | "getSnapshotTree" | "getSnapshot" | "getVersions"
45
91
  >,
46
92
  private readonly _offlineLoadEnabled: boolean,
93
+ private readonly newSnapshotFetched?: () => void,
47
94
  ) {
48
95
  this.mc = createChildMonitoringContext({
49
96
  logger: subLogger,
@@ -58,107 +105,101 @@ export class SerializedStateManager {
58
105
  public addProcessedOp(message: ISequencedDocumentMessage) {
59
106
  if (this.offlineLoadEnabled) {
60
107
  this.processedOps.push(message);
108
+ this.updateSnapshotAndProcessedOpsMaybe();
61
109
  }
62
110
  }
63
111
 
64
- private async getVersion(version: string | null): Promise<IVersion | undefined> {
65
- const versions = await this.storageAdapter.getVersions(version, 1);
66
- return versions[0];
67
- }
68
-
69
112
  public async fetchSnapshot(
70
113
  specifiedVersion: string | undefined,
71
- supportGetSnapshotApi: boolean | undefined,
114
+ supportGetSnapshotApi: boolean,
72
115
  ) {
73
- const { snapshot, version } =
74
- this.pendingLocalState === undefined
75
- ? await this.fetchSnapshotCore(specifiedVersion, supportGetSnapshotApi)
76
- : { snapshot: this.pendingLocalState.baseSnapshot, version: undefined };
77
- const snapshotTree: ISnapshotTree | undefined = isInstanceOfISnapshot(snapshot)
78
- ? snapshot.snapshotTree
79
- : snapshot;
80
- if (this.pendingLocalState) {
81
- this.snapshot = {
82
- tree: this.pendingLocalState.baseSnapshot,
83
- blobs: this.pendingLocalState.snapshotBlobs,
84
- };
85
- } else {
86
- assert(snapshotTree !== undefined, 0x8e4 /* Snapshot should exist */);
116
+ if (this.pendingLocalState === undefined) {
117
+ const { baseSnapshot, version } = await getSnapshotTree(
118
+ this.mc,
119
+ this.storageAdapter,
120
+ supportGetSnapshotApi,
121
+ specifiedVersion,
122
+ );
87
123
  // non-interactive clients will not have any pending state we want to save
88
124
  if (this.offlineLoadEnabled) {
89
- const blobs = await getBlobContentsFromTree(snapshotTree, this.storageAdapter);
90
- this.snapshot = { tree: snapshotTree, blobs };
125
+ const snapshotBlobs = await getBlobContentsFromTree(
126
+ baseSnapshot,
127
+ this.storageAdapter,
128
+ );
129
+ const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshot);
130
+ this.snapshot = {
131
+ baseSnapshot,
132
+ snapshotBlobs,
133
+ snapshotSequenceNumber: attributes.sequenceNumber,
134
+ };
91
135
  }
92
- }
93
- return { snapshotTree, version };
94
- }
136
+ return { baseSnapshot, version };
137
+ } else {
138
+ const { baseSnapshot, snapshotBlobs } = this.pendingLocalState;
139
+ const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshot);
140
+ this.snapshot = {
141
+ baseSnapshot,
142
+ snapshotBlobs,
143
+ snapshotSequenceNumber: attributes.sequenceNumber,
144
+ };
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
+ })();
95
154
 
96
- private async fetchSnapshotCore(
97
- specifiedVersion: string | undefined,
98
- supportGetSnapshotApi: boolean | undefined,
99
- ): Promise<{ snapshot?: ISnapshot | ISnapshotTree; version?: IVersion }> {
100
- if (
101
- this.mc.config.getBoolean("Fluid.Container.UseLoadingGroupIdForSnapshotFetch") ===
102
- true &&
103
- supportGetSnapshotApi === true
104
- ) {
105
- const snapshot =
106
- (await this.storageAdapter.getSnapshot?.({
107
- versionId: specifiedVersion,
108
- })) ?? undefined;
109
- const version: IVersion | undefined =
110
- snapshot?.snapshotTree.id === undefined
111
- ? undefined
112
- : {
113
- id: snapshot.snapshotTree.id,
114
- treeId: snapshot.snapshotTree.id,
115
- };
116
-
117
- if (snapshot === undefined && specifiedVersion !== undefined) {
118
- this.mc.logger.sendErrorEvent({
119
- eventName: "getSnapshotTreeFailed",
120
- id: specifiedVersion,
121
- });
122
- // Not sure if this should be here actually
123
- } else if (snapshot !== undefined && version?.id === undefined) {
124
- this.mc.logger.sendErrorEvent({
125
- eventName: "getSnapshotFetchedTreeWithoutVersionId",
126
- hasVersion: version !== undefined, // if hasVersion is true, this means that the contract with the service was broken.
127
- });
128
- }
129
- return { snapshot, version };
155
+ return { baseSnapshot, version: undefined };
130
156
  }
131
- return this.fetchSnapshotTree(specifiedVersion);
132
157
  }
133
158
 
134
159
  /**
135
- * Get the most recent snapshot, or a specific version.
136
- * @param specifiedVersion - The specific version of the snapshot to retrieve
137
- * @returns The snapshot requested, or the latest snapshot if no version was specified, plus version ID
160
+ * Updates class snapshot and processedOps if we have a new snapshot and it's among processedOps range.
138
161
  */
139
- private async fetchSnapshotTree(
140
- specifiedVersion: string | undefined,
141
- ): Promise<{ snapshot?: ISnapshotTree; version?: IVersion | undefined }> {
142
- const version = await this.getVersion(specifiedVersion ?? null);
143
-
144
- if (version === undefined && specifiedVersion !== undefined) {
145
- // We should have a defined version to load from if specified version requested
146
- this.mc.logger.sendErrorEvent({
147
- eventName: "NoVersionFoundWhenSpecified",
148
- id: specifiedVersion,
149
- });
162
+ private updateSnapshotAndProcessedOpsMaybe() {
163
+ if (this.latestSnapshot === undefined || this.processedOps.length === 0) {
164
+ // can't refresh latest snapshot until we have processed the ops up to it.
165
+ // Pending state would be behind the latest snapshot.
166
+ return;
150
167
  }
151
- const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
152
-
153
- if (snapshot === undefined && version !== undefined) {
154
- this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
155
- } else if (snapshot !== undefined && version?.id === undefined) {
156
- this.mc.logger.sendErrorEvent({
157
- eventName: "getSnapshotFetchedTreeWithoutVersionId",
158
- hasVersion: version !== undefined, // if hasVersion is true, this means that the contract with the service was broken.
168
+ const snapshotSequenceNumber = this.latestSnapshot.snapshotSequenceNumber;
169
+ const firstProcessedOpSequenceNumber = this.processedOps[0].sequenceNumber;
170
+ const lastProcessedOpSequenceNumber =
171
+ this.processedOps[this.processedOps.length - 1].sequenceNumber;
172
+
173
+ if (snapshotSequenceNumber < firstProcessedOpSequenceNumber) {
174
+ // Snapshot seq number is older than our first processed op, which could mean we're fetching
175
+ // the same snapshot that we already have or snapshot is too old, implicating an unexpected behavior.
176
+ this.mc.logger.sendTelemetryEvent({
177
+ eventName: "OldSnapshotFetchWhileRefreshing",
178
+ snapshotSequenceNumber,
179
+ firstProcessedOpSequenceNumber,
180
+ lastProcessedOpSequenceNumber,
181
+ stashedSnapshotSequenceNumber: this.snapshot?.snapshotSequenceNumber,
182
+ });
183
+ this.latestSnapshot = undefined;
184
+ } else if (snapshotSequenceNumber <= lastProcessedOpSequenceNumber) {
185
+ // Snapshot seq num is between the first and last processed op.
186
+ // Remove the ops that are already part of the snapshot
187
+ this.processedOps.splice(
188
+ 0,
189
+ snapshotSequenceNumber - firstProcessedOpSequenceNumber + 1,
190
+ );
191
+ this.snapshot = this.latestSnapshot;
192
+ this.latestSnapshot = undefined;
193
+ this.mc.logger.sendTelemetryEvent({
194
+ eventName: "SnapshotRefreshed",
195
+ snapshotSequenceNumber,
196
+ firstProcessedOpSequenceNumber,
197
+ newFirstProcessedOpSequenceNumber:
198
+ this.processedOps.length === 0
199
+ ? undefined
200
+ : this.processedOps[0].sequenceNumber,
159
201
  });
160
202
  }
161
- return { snapshot, version };
162
203
  }
163
204
 
164
205
  /**
@@ -166,15 +207,25 @@ export class SerializedStateManager {
166
207
  * base snapshot when attaching.
167
208
  * @param snapshot - snapshot and blobs collected while attaching
168
209
  */
169
- public setSnapshot(
170
- snapshot:
171
- | {
172
- tree: ISnapshotTree;
173
- blobs: ISerializableBlobContents;
174
- }
175
- | undefined,
176
- ) {
177
- this.snapshot = snapshot;
210
+ public setInitialSnapshot(snapshot: SnapshotWithBlobs | undefined) {
211
+ if (this.offlineLoadEnabled) {
212
+ assert(
213
+ this.snapshot === undefined,
214
+ 0x937 /* inital snapshot should only be defined once */,
215
+ );
216
+ assert(snapshot !== undefined, 0x938 /* attachment snapshot should be defined */);
217
+ const { baseSnapshot, snapshotBlobs } = snapshot;
218
+ const attributesHash =
219
+ ".protocol" in baseSnapshot.trees
220
+ ? baseSnapshot.trees[".protocol"].blobs.attributes
221
+ : baseSnapshot.blobs[".attributes"];
222
+ const attributes = JSON.parse(snapshotBlobs[attributesHash]);
223
+ assert(
224
+ attributes.sequenceNumber === 0,
225
+ 0x939 /* trying to set a non attachment snapshot */,
226
+ );
227
+ this.snapshot = { ...snapshot, snapshotSequenceNumber: attributes.sequenceNumber };
228
+ }
178
229
  }
179
230
 
180
231
  public async getPendingLocalStateCore(
@@ -202,8 +253,8 @@ export class SerializedStateManager {
202
253
  const pendingState: IPendingContainerState = {
203
254
  attached: true,
204
255
  pendingRuntimeState,
205
- baseSnapshot: this.snapshot.tree,
206
- snapshotBlobs: this.snapshot.blobs,
256
+ baseSnapshot: this.snapshot.baseSnapshot,
257
+ snapshotBlobs: this.snapshot.snapshotBlobs,
207
258
  savedOps: this.processedOps,
208
259
  url: resolvedUrl.url,
209
260
  // no need to save this if there is no pending runtime state
@@ -215,3 +266,140 @@ export class SerializedStateManager {
215
266
  );
216
267
  }
217
268
  }
269
+
270
+ /**
271
+ * Retrieves the most recent snapshot and returns its info.
272
+ *
273
+ * @param mc - The monitoring context.
274
+ * @param storageAdapter - The storage adapter providing methods to retrieve the snapshot.
275
+ * @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree.
276
+ * @returns a SnapshotInfo object containing the snapshot tree, snapshot blobs and its sequence number.
277
+ */
278
+ export async function getLatestSnapshotInfo(
279
+ mc: MonitoringContext,
280
+ storageAdapter: Pick<
281
+ IDocumentStorageService,
282
+ "getSnapshot" | "getSnapshotTree" | "getVersions" | "readBlob"
283
+ >,
284
+ supportGetSnapshotApi: boolean,
285
+ ): Promise<ISnapshotInfo | undefined> {
286
+ return PerformanceEvent.timedExecAsync(
287
+ mc.logger,
288
+ { eventName: "GetLatestSnapshotInfo" },
289
+ async () => {
290
+ const { baseSnapshot } = await getSnapshotTree(
291
+ mc,
292
+ storageAdapter,
293
+ supportGetSnapshotApi,
294
+ undefined,
295
+ );
296
+ const snapshotBlobs = await getBlobContentsFromTree(baseSnapshot, storageAdapter);
297
+ const attributes: IDocumentAttributes = await getDocumentAttributes(
298
+ storageAdapter,
299
+ baseSnapshot,
300
+ );
301
+ const snapshotSequenceNumber = attributes.sequenceNumber;
302
+ return { baseSnapshot, snapshotBlobs, snapshotSequenceNumber };
303
+ },
304
+ ).catch(() => undefined);
305
+ }
306
+
307
+ /**
308
+ * Retrieves a snapshot from the storage adapter and transforms it into an ISnapshotTree object.
309
+ *
310
+ * @param mc - The monitoring context.
311
+ * @param storageAdapter - The storage adapter providing methods to retrieve the snapshot.
312
+ * @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree.
313
+ * @param specifiedVersion - An optional version string specifying the version of the snapshot tree to fetch.
314
+ * @returns - An ISnapshotTree and its version.
315
+ */
316
+ async function getSnapshotTree(
317
+ mc: MonitoringContext,
318
+ storageAdapter: Pick<
319
+ IDocumentStorageService,
320
+ "getSnapshot" | "getSnapshotTree" | "getVersions"
321
+ >,
322
+ supportGetSnapshotApi: boolean,
323
+ specifiedVersion: string | undefined,
324
+ ): Promise<{ baseSnapshot: ISnapshotTree; version?: IVersion }> {
325
+ const { snapshot, version } = supportGetSnapshotApi
326
+ ? await fetchISnapshot(mc, storageAdapter, specifiedVersion)
327
+ : 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 };
333
+ }
334
+
335
+ /**
336
+ * Fetches an ISnapshot from a storage adapter based on the specified version.
337
+ *
338
+ * @param mc - The monitoring context.
339
+ * @param storageAdapter - The storage adapter providing a getSnapshot method to retrieve the ISnapshot and version.
340
+ * @param specifiedVersion - An optional version string specifying the version of the snapshot tree to fetch.
341
+ * @returns - The fetched snapshot tree and its version.
342
+ */
343
+ export async function fetchISnapshot(
344
+ mc: MonitoringContext,
345
+ storageAdapter: Pick<IDocumentStorageService, "getSnapshot">,
346
+ specifiedVersion: string | undefined,
347
+ ): Promise<{ snapshot?: ISnapshot; version?: IVersion }> {
348
+ const snapshot = await storageAdapter.getSnapshot?.({ versionId: specifiedVersion });
349
+ const version: IVersion | undefined =
350
+ snapshot?.snapshotTree.id === undefined
351
+ ? undefined
352
+ : {
353
+ id: snapshot.snapshotTree.id,
354
+ treeId: snapshot.snapshotTree.id,
355
+ };
356
+
357
+ if (snapshot === undefined && specifiedVersion !== undefined) {
358
+ mc.logger.sendErrorEvent({
359
+ eventName: "getSnapshotTreeFailed",
360
+ id: specifiedVersion,
361
+ });
362
+ } else if (snapshot !== undefined && version?.id === undefined) {
363
+ mc.logger.sendErrorEvent({
364
+ eventName: "getSnapshotFetchedTreeWithoutVersionId",
365
+ hasVersion: version !== undefined, // if hasVersion is true, this means that the contract with the service was broken.
366
+ });
367
+ }
368
+ return { snapshot, version };
369
+ }
370
+
371
+ /**
372
+ * Fetches an ISnapshotTree from a storage adapter based on the specified version.
373
+ *
374
+ * @param mc - The monitoring context.
375
+ * @param storageAdapter - The storage adapter providing methods to retrieve the ISnapshotTree and version.
376
+ * @param specifiedVersion - An optional version string specifying the version of the snapshot tree to fetch.
377
+ * @returns - The fetched snapshot tree and its version.
378
+ */
379
+ export async function fetchISnapshotTree(
380
+ mc: MonitoringContext,
381
+ storageAdapter: Pick<IDocumentStorageService, "getSnapshotTree" | "getVersions">,
382
+ specifiedVersion: string | undefined,
383
+ ): Promise<{ snapshot?: ISnapshotTree; version?: IVersion | undefined }> {
384
+ const versions = await storageAdapter.getVersions(specifiedVersion ?? null, 1);
385
+ const version = versions[0];
386
+
387
+ if (version === undefined && specifiedVersion !== undefined) {
388
+ // We should have a defined version to load from if specified version requested
389
+ mc.logger.sendErrorEvent({
390
+ eventName: "NoVersionFoundWhenSpecified",
391
+ id: specifiedVersion,
392
+ });
393
+ }
394
+ const snapshot = (await storageAdapter.getSnapshotTree(version)) ?? undefined;
395
+
396
+ if (snapshot === undefined && version !== undefined) {
397
+ mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
398
+ } else if (snapshot !== undefined && version?.id === undefined) {
399
+ mc.logger.sendErrorEvent({
400
+ eventName: "getSnapshotFetchedTreeWithoutVersionId",
401
+ hasVersion: version !== undefined, // if hasVersion is true, this means that the contract with the service was broken.
402
+ });
403
+ }
404
+ return { snapshot, version };
405
+ }
package/src/utils.ts CHANGED
@@ -3,19 +3,34 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { v4 as uuid } from "uuid";
7
- import { Uint8ArrayToString, stringToBuffer } from "@fluid-internal/client-utils";
8
- import { assert, compareArrays, unreachableCase } from "@fluidframework/core-utils";
9
- import { ISummaryTree, ISnapshotTree, SummaryType } from "@fluidframework/protocol-definitions";
10
- import { LoggingError, UsageError } from "@fluidframework/telemetry-utils";
6
+ import { Uint8ArrayToString, bufferToString, stringToBuffer } from "@fluid-internal/client-utils";
7
+ import { assert, compareArrays, unreachableCase } from "@fluidframework/core-utils/internal";
8
+ import { DriverErrorTypes } from "@fluidframework/driver-definitions";
9
+ import {
10
+ IDocumentStorageService,
11
+ type ISnapshot,
12
+ } from "@fluidframework/driver-definitions/internal";
11
13
  import {
12
14
  CombinedAppAndProtocolSummary,
13
15
  DeltaStreamConnectionForbiddenError,
14
16
  isCombinedAppAndProtocolSummary,
15
- } from "@fluidframework/driver-utils";
16
- import { DriverErrorTypes } from "@fluidframework/driver-definitions";
17
+ readAndParse,
18
+ } from "@fluidframework/driver-utils/internal";
19
+ import {
20
+ IDocumentAttributes,
21
+ ISnapshotTree,
22
+ ISummaryTree,
23
+ SummaryType,
24
+ } from "@fluidframework/protocol-definitions";
25
+ import { LoggingError, UsageError } from "@fluidframework/telemetry-utils/internal";
26
+ import { v4 as uuid } from "uuid";
27
+
17
28
  import { ISerializableBlobContents } from "./containerStorageAdapter.js";
18
- import { IPendingDetachedContainerState } from "./container.js";
29
+ import type {
30
+ IPendingDetachedContainerState,
31
+ ISnapshotInfo,
32
+ SnapshotWithBlobs,
33
+ } from "./serializedStateManager.js";
19
34
 
20
35
  // This is used when we rehydrate a container from the snapshot. Here we put the blob contents
21
36
  // in separate property: blobContents.
@@ -28,7 +43,7 @@ export interface ISnapshotTreeWithBlobContents extends ISnapshotTree {
28
43
  * Interface to represent the parsed parts of IResolvedUrl.url to help
29
44
  * in getting info about different parts of the url.
30
45
  * May not be compatible or relevant for any Url Resolver
31
- * @internal
46
+ * @alpha
32
47
  */
33
48
  export interface IParsedUrl {
34
49
  /**
@@ -57,7 +72,7 @@ export interface IParsedUrl {
57
72
  * with urls of type: protocol://<string>/.../..?<querystring>
58
73
  * @param url - This is the IResolvedUrl.url part of the resolved url.
59
74
  * @returns The IParsedUrl representing the input URL, or undefined if the format was not supported
60
- * @internal
75
+ * @alpha
61
76
  */
62
77
  export function tryParseCompatibleResolvedUrl(url: string): IParsedUrl | undefined {
63
78
  const parsed = new URL(url);
@@ -111,10 +126,7 @@ export function combineAppAndProtocolSummary(
111
126
  * to align detached container format with IPendingContainerState
112
127
  * @param summary - ISummaryTree
113
128
  */
114
- function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): {
115
- tree: ISnapshotTree;
116
- blobs: ISerializableBlobContents;
117
- } {
129
+ function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): SnapshotWithBlobs {
118
130
  let blobContents: ISerializableBlobContents = {};
119
131
  const treeNode: ISnapshotTree = {
120
132
  blobs: {},
@@ -129,9 +141,9 @@ function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): {
129
141
 
130
142
  switch (summaryObject.type) {
131
143
  case SummaryType.Tree: {
132
- const { tree, blobs } = convertSummaryToSnapshotAndBlobs(summaryObject);
133
- treeNode.trees[key] = tree;
134
- blobContents = { ...blobContents, ...blobs };
144
+ const innerSnapshot = convertSummaryToSnapshotAndBlobs(summaryObject);
145
+ treeNode.trees[key] = innerSnapshot.baseSnapshot;
146
+ blobContents = { ...blobContents, ...innerSnapshot.snapshotBlobs };
135
147
  break;
136
148
  }
137
149
  case SummaryType.Attachment:
@@ -151,13 +163,58 @@ function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): {
151
163
  throw new LoggingError(
152
164
  "No handles should be there in summary in detached container!!",
153
165
  );
154
- break;
155
166
  default: {
156
167
  unreachableCase(summaryObject, `Unknown tree type ${(summaryObject as any).type}`);
157
168
  }
158
169
  }
159
170
  }
160
- return { tree: treeNode, blobs: blobContents };
171
+ const pendingSnapshot = { baseSnapshot: treeNode, snapshotBlobs: blobContents };
172
+ return pendingSnapshot;
173
+ }
174
+
175
+ /**
176
+ * Converts a snapshot to snapshotInfo with its blob contents
177
+ * to align detached container format with IPendingContainerState
178
+ *
179
+ * Note, this assumes the ISnapshot sequence number is defined. Otherwise an assert will be thrown
180
+ * @param snapshot - ISnapshot
181
+ */
182
+ export function convertSnapshotToSnapshotInfo(snapshot: ISnapshot): ISnapshotInfo {
183
+ assert(snapshot.sequenceNumber !== undefined, 0x93a /* Snapshot sequence number is missing */);
184
+ const snapshotBlobs: ISerializableBlobContents = {};
185
+ for (const [blobId, arrayBufferLike] of snapshot.blobContents.entries()) {
186
+ snapshotBlobs[blobId] = bufferToString(arrayBufferLike, "utf8");
187
+ }
188
+ return {
189
+ baseSnapshot: snapshot.snapshotTree,
190
+ snapshotBlobs,
191
+ snapshotSequenceNumber: snapshot.sequenceNumber,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Converts a snapshot to snapshotInfo with its blob contents
197
+ * to align detached container format with IPendingContainerState
198
+ *
199
+ * Note, this assumes the ISnapshot sequence number is defined. Otherwise an assert will be thrown
200
+ * @param snapshot - ISnapshot
201
+ */
202
+ export function convertSnapshotInfoToSnapshot(
203
+ snapshotInfo: ISnapshotInfo,
204
+ snapshotSequenceNumber: number,
205
+ ): ISnapshot {
206
+ const blobContents = new Map<string, ArrayBufferLike>();
207
+ for (const [blobId, serializedContent] of Object.entries(snapshotInfo.snapshotBlobs)) {
208
+ blobContents.set(blobId, stringToBuffer(serializedContent, "utf8"));
209
+ }
210
+ return {
211
+ snapshotTree: snapshotInfo.baseSnapshot,
212
+ blobContents,
213
+ ops: [],
214
+ sequenceNumber: snapshotSequenceNumber,
215
+ latestSequenceNumber: undefined,
216
+ snapshotFormatV: 1,
217
+ };
161
218
  }
162
219
 
163
220
  /**
@@ -168,7 +225,7 @@ function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): {
168
225
  function convertProtocolAndAppSummaryToSnapshotAndBlobs(
169
226
  protocolSummaryTree: ISummaryTree,
170
227
  appSummaryTree: ISummaryTree,
171
- ): { tree: ISnapshotTree; blobs: ISerializableBlobContents } {
228
+ ): SnapshotWithBlobs {
172
229
  const combinedSummary: ISummaryTree = {
173
230
  type: SummaryType.Tree,
174
231
  tree: { ...appSummaryTree.tree },
@@ -181,7 +238,7 @@ function convertProtocolAndAppSummaryToSnapshotAndBlobs(
181
238
 
182
239
  export const getSnapshotTreeAndBlobsFromSerializedContainer = (
183
240
  detachedContainerSnapshot: ISummaryTree,
184
- ): { tree: ISnapshotTree; blobs: ISerializableBlobContents } => {
241
+ ): SnapshotWithBlobs => {
185
242
  assert(
186
243
  isCombinedAppAndProtocolSummary(detachedContainerSnapshot),
187
244
  0x8e6 /* Protocol and App summary trees should be present */,
@@ -264,12 +321,12 @@ export function getDetachedContainerStateFromSerializedContainer(
264
321
  if (isPendingDetachedContainerState(parsedContainerState)) {
265
322
  return parsedContainerState;
266
323
  } else if (isCombinedAppAndProtocolSummary(parsedContainerState)) {
267
- const { tree, blobs } =
324
+ const { baseSnapshot, snapshotBlobs } =
268
325
  getSnapshotTreeAndBlobsFromSerializedContainer(parsedContainerState);
269
326
  const detachedContainerState: IPendingDetachedContainerState = {
270
327
  attached: false,
271
- baseSnapshot: tree,
272
- snapshotBlobs: blobs,
328
+ baseSnapshot,
329
+ snapshotBlobs,
273
330
  hasAttachmentBlobs: parsedContainerState.tree[hasBlobsSummaryTree] !== undefined,
274
331
  };
275
332
  return detachedContainerState;
@@ -305,3 +362,25 @@ export const runSingle = <A extends any[], R>(func: (...args: A) => Promise<R>)
305
362
  return running.result;
306
363
  };
307
364
  };
365
+
366
+ export async function getDocumentAttributes(
367
+ storage: Pick<IDocumentStorageService, "readBlob">,
368
+ tree: ISnapshotTree | undefined,
369
+ ): Promise<IDocumentAttributes> {
370
+ if (tree === undefined) {
371
+ return {
372
+ minimumSequenceNumber: 0,
373
+ sequenceNumber: 0,
374
+ };
375
+ }
376
+
377
+ // Backward compatibility: old docs would have ".attributes" instead of "attributes"
378
+ const attributesHash =
379
+ ".protocol" in tree.trees
380
+ ? tree.trees[".protocol"].blobs.attributes
381
+ : tree.blobs[".attributes"];
382
+
383
+ const attributes = await readAndParse<IDocumentAttributes>(storage, attributesHash);
384
+
385
+ return attributes;
386
+ }