@fluidframework/odsp-driver 0.51.3 → 0.53.0-46105

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 (126) hide show
  1. package/dist/contractsPublic.d.ts +0 -4
  2. package/dist/contractsPublic.d.ts.map +1 -1
  3. package/dist/contractsPublic.js.map +1 -1
  4. package/dist/createFile.d.ts.map +1 -1
  5. package/dist/createFile.js +11 -9
  6. package/dist/createFile.js.map +1 -1
  7. package/dist/createNewUtils.js +5 -4
  8. package/dist/createNewUtils.js.map +1 -1
  9. package/dist/epochTracker.d.ts +2 -1
  10. package/dist/epochTracker.d.ts.map +1 -1
  11. package/dist/epochTracker.js +51 -25
  12. package/dist/epochTracker.js.map +1 -1
  13. package/dist/fetchSnapshot.d.ts.map +1 -1
  14. package/dist/fetchSnapshot.js +1 -1
  15. package/dist/fetchSnapshot.js.map +1 -1
  16. package/dist/getFileLink.d.ts +1 -1
  17. package/dist/getFileLink.d.ts.map +1 -1
  18. package/dist/getFileLink.js +32 -22
  19. package/dist/getFileLink.js.map +1 -1
  20. package/dist/odspDeltaStorageService.d.ts +3 -3
  21. package/dist/odspDeltaStorageService.d.ts.map +1 -1
  22. package/dist/odspDeltaStorageService.js +7 -4
  23. package/dist/odspDeltaStorageService.js.map +1 -1
  24. package/dist/odspDocumentDeltaConnection.d.ts.map +1 -1
  25. package/dist/odspDocumentDeltaConnection.js +2 -0
  26. package/dist/odspDocumentDeltaConnection.js.map +1 -1
  27. package/dist/odspDocumentService.d.ts.map +1 -1
  28. package/dist/odspDocumentService.js +3 -3
  29. package/dist/odspDocumentService.js.map +1 -1
  30. package/dist/odspDocumentStorageManager.d.ts.map +1 -1
  31. package/dist/odspDocumentStorageManager.js +30 -34
  32. package/dist/odspDocumentStorageManager.js.map +1 -1
  33. package/dist/odspDriverUrlResolver.d.ts +0 -4
  34. package/dist/odspDriverUrlResolver.d.ts.map +1 -1
  35. package/dist/odspDriverUrlResolver.js +0 -7
  36. package/dist/odspDriverUrlResolver.js.map +1 -1
  37. package/dist/odspDriverUrlResolverForShareLink.d.ts +2 -6
  38. package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  39. package/dist/odspDriverUrlResolverForShareLink.js +4 -15
  40. package/dist/odspDriverUrlResolverForShareLink.js.map +1 -1
  41. package/dist/odspError.d.ts.map +1 -1
  42. package/dist/odspError.js +3 -1
  43. package/dist/odspError.js.map +1 -1
  44. package/dist/odspSummaryUploadManager.js +24 -4
  45. package/dist/odspSummaryUploadManager.js.map +1 -1
  46. package/dist/odspUtils.d.ts.map +1 -1
  47. package/dist/odspUtils.js +31 -23
  48. package/dist/odspUtils.js.map +1 -1
  49. package/dist/packageVersion.d.ts +1 -1
  50. package/dist/packageVersion.d.ts.map +1 -1
  51. package/dist/packageVersion.js +1 -1
  52. package/dist/packageVersion.js.map +1 -1
  53. package/dist/zipItDataRepresentationUtils.d.ts.map +1 -1
  54. package/dist/zipItDataRepresentationUtils.js +3 -4
  55. package/dist/zipItDataRepresentationUtils.js.map +1 -1
  56. package/lib/contractsPublic.d.ts +0 -4
  57. package/lib/contractsPublic.d.ts.map +1 -1
  58. package/lib/contractsPublic.js.map +1 -1
  59. package/lib/createFile.d.ts.map +1 -1
  60. package/lib/createFile.js +12 -10
  61. package/lib/createFile.js.map +1 -1
  62. package/lib/createNewUtils.js +5 -4
  63. package/lib/createNewUtils.js.map +1 -1
  64. package/lib/epochTracker.d.ts +2 -1
  65. package/lib/epochTracker.d.ts.map +1 -1
  66. package/lib/epochTracker.js +52 -26
  67. package/lib/epochTracker.js.map +1 -1
  68. package/lib/fetchSnapshot.d.ts.map +1 -1
  69. package/lib/fetchSnapshot.js +1 -1
  70. package/lib/fetchSnapshot.js.map +1 -1
  71. package/lib/getFileLink.d.ts +1 -1
  72. package/lib/getFileLink.d.ts.map +1 -1
  73. package/lib/getFileLink.js +34 -24
  74. package/lib/getFileLink.js.map +1 -1
  75. package/lib/odspDeltaStorageService.d.ts +3 -3
  76. package/lib/odspDeltaStorageService.d.ts.map +1 -1
  77. package/lib/odspDeltaStorageService.js +7 -4
  78. package/lib/odspDeltaStorageService.js.map +1 -1
  79. package/lib/odspDocumentDeltaConnection.d.ts.map +1 -1
  80. package/lib/odspDocumentDeltaConnection.js +2 -0
  81. package/lib/odspDocumentDeltaConnection.js.map +1 -1
  82. package/lib/odspDocumentService.d.ts.map +1 -1
  83. package/lib/odspDocumentService.js +4 -4
  84. package/lib/odspDocumentService.js.map +1 -1
  85. package/lib/odspDocumentStorageManager.d.ts.map +1 -1
  86. package/lib/odspDocumentStorageManager.js +33 -37
  87. package/lib/odspDocumentStorageManager.js.map +1 -1
  88. package/lib/odspDriverUrlResolver.d.ts +0 -4
  89. package/lib/odspDriverUrlResolver.d.ts.map +1 -1
  90. package/lib/odspDriverUrlResolver.js +0 -7
  91. package/lib/odspDriverUrlResolver.js.map +1 -1
  92. package/lib/odspDriverUrlResolverForShareLink.d.ts +2 -6
  93. package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  94. package/lib/odspDriverUrlResolverForShareLink.js +5 -16
  95. package/lib/odspDriverUrlResolverForShareLink.js.map +1 -1
  96. package/lib/odspError.d.ts.map +1 -1
  97. package/lib/odspError.js +3 -1
  98. package/lib/odspError.js.map +1 -1
  99. package/lib/odspSummaryUploadManager.js +5 -4
  100. package/lib/odspSummaryUploadManager.js.map +1 -1
  101. package/lib/odspUtils.d.ts.map +1 -1
  102. package/lib/odspUtils.js +33 -25
  103. package/lib/odspUtils.js.map +1 -1
  104. package/lib/packageVersion.d.ts +1 -1
  105. package/lib/packageVersion.d.ts.map +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/zipItDataRepresentationUtils.d.ts.map +1 -1
  109. package/lib/zipItDataRepresentationUtils.js +3 -4
  110. package/lib/zipItDataRepresentationUtils.js.map +1 -1
  111. package/package.json +12 -12
  112. package/src/contractsPublic.ts +0 -5
  113. package/src/createFile.ts +13 -9
  114. package/src/epochTracker.ts +52 -25
  115. package/src/fetchSnapshot.ts +2 -1
  116. package/src/getFileLink.ts +47 -27
  117. package/src/odspDeltaStorageService.ts +10 -3
  118. package/src/odspDocumentDeltaConnection.ts +2 -0
  119. package/src/odspDocumentService.ts +8 -4
  120. package/src/odspDocumentStorageManager.ts +49 -38
  121. package/src/odspDriverUrlResolver.ts +0 -13
  122. package/src/odspDriverUrlResolverForShareLink.ts +10 -23
  123. package/src/odspError.ts +5 -1
  124. package/src/odspUtils.ts +43 -35
  125. package/src/packageVersion.ts +1 -1
  126. package/src/zipItDataRepresentationUtils.ts +4 -8
@@ -6,8 +6,7 @@
6
6
  import { v4 as uuid } from "uuid";
7
7
  import { assert, Deferred } from "@fluidframework/common-utils";
8
8
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
9
- import { fluidEpochMismatchError, throwOdspNetworkError } from "@fluidframework/odsp-doclib-utils";
10
- import { ThrottlingError, RateLimiter } from "@fluidframework/driver-utils";
9
+ import { ThrottlingError, RateLimiter, NonRetryableError } from "@fluidframework/driver-utils";
11
10
  import { IConnected } from "@fluidframework/protocol-definitions";
12
11
  import {
13
12
  snapshotKey,
@@ -18,7 +17,7 @@ import {
18
17
  IOdspError,
19
18
  } from "@fluidframework/odsp-driver-definitions";
20
19
  import { DriverErrorType } from "@fluidframework/driver-definitions";
21
- import { PerformanceEvent, isValidLegacyError, isFluidError, normalizeError } from "@fluidframework/telemetry-utils";
20
+ import { PerformanceEvent, isFluidError, normalizeError } from "@fluidframework/telemetry-utils";
22
21
  import { fetchAndParseAsJSONHelper, fetchArray, IOdspResponse } from "./odspUtils";
23
22
  import {
24
23
  IOdspCache,
@@ -34,6 +33,8 @@ export type FetchTypeInternal = FetchType | "cache";
34
33
 
35
34
  export const Odsp409Error = "Odsp409Error";
36
35
 
36
+ export const defaultCacheExpiryTimeoutMs: number = 2 * 24 * 60 * 60 * 1000;
37
+
37
38
  /**
38
39
  * This class is a wrapper around fetch calls. It adds epoch to the request made so that the
39
40
  * server can match it with its epoch value in order to match the version.
@@ -75,16 +76,37 @@ export class EpochTracker implements IPersistedFileCache {
75
76
  entry: IEntry,
76
77
  ): Promise<any> {
77
78
  try {
79
+ // Return undefined so that the ops/snapshots are grabbed from the server instead of the cache
78
80
  const value: IVersionedValueWithEpoch = await this.cache.get(this.fileEntryFromEntry(entry));
81
+ // Version mismatch between what the runtime expects and what it recieved.
82
+ // The cached value should not be used
79
83
  if (value === undefined || value.version !== persistedCacheValueVersion) {
80
84
  return undefined;
81
85
  }
82
86
  assert(value.fluidEpoch !== undefined, 0x1dc /* "all entries have to have epoch" */);
83
87
  if (this._fluidEpoch === undefined) {
84
88
  this.setEpoch(value.fluidEpoch, true, "cache");
89
+ // Epoch mismatch, the cached value is considerably different from what the current state of
90
+ // the runtime and should not be used
85
91
  } else if (this._fluidEpoch !== value.fluidEpoch) {
86
92
  return undefined;
87
93
  }
94
+ // Expire the cached snapshot if it's older than the defaultCacheExpiryTimeoutMs and immediately
95
+ // expire all old caches that do not have cacheEntryTime
96
+ if (entry.type === snapshotKey) {
97
+ const cacheTime = value.value?.cacheEntryTime;
98
+ const currentTime = Date.now();
99
+ if (cacheTime === undefined || currentTime - cacheTime >= defaultCacheExpiryTimeoutMs) {
100
+ this.logger.sendTelemetryEvent(
101
+ {
102
+ eventName: "odspVersionsCacheExpired",
103
+ duration: currentTime - cacheTime,
104
+ maxCacheAgeMs: defaultCacheExpiryTimeoutMs,
105
+ });
106
+ await this.removeEntries();
107
+ return undefined;
108
+ }
109
+ }
88
110
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
89
111
  return value.value;
90
112
  } catch (error) {
@@ -95,6 +117,11 @@ export class EpochTracker implements IPersistedFileCache {
95
117
 
96
118
  public async put(entry: IEntry, value: any) {
97
119
  assert(this._fluidEpoch !== undefined, 0x1dd /* "no epoch" */);
120
+ // For snapshots, the value should have the cacheEntryTime. This will be used to expire snapshots older
121
+ // than the defaultCacheExpiryTimeoutMs.
122
+ if (entry.type === snapshotKey) {
123
+ value.cacheEntryTime = value.cacheEntryTime ?? Date.now();
124
+ }
98
125
  const data: IVersionedValueWithEpoch = {
99
126
  value,
100
127
  version: persistedCacheValueVersion,
@@ -143,9 +170,9 @@ export class EpochTracker implements IPersistedFileCache {
143
170
  fetchType: FetchType,
144
171
  addInBody: boolean = false,
145
172
  ): Promise<IOdspResponse<T>> {
146
- const clientCorelationId = this.formatClientCorelationId();
173
+ const clientCorrelationId = this.formatClientCorrelationId();
147
174
  // Add epoch in fetch request.
148
- this.addEpochInRequest(fetchOptions, addInBody, clientCorelationId);
175
+ this.addEpochInRequest(fetchOptions, addInBody, clientCorrelationId);
149
176
  let epochFromResponse: string | undefined;
150
177
  return this.rateLimiter.schedule(
151
178
  async () => fetchAndParseAsJSONHelper<T>(url, fetchOptions),
@@ -154,7 +181,7 @@ export class EpochTracker implements IPersistedFileCache {
154
181
  this.validateEpochFromResponse(epochFromResponse, fetchType);
155
182
  response.commonSpoHeaders = {
156
183
  ...response.commonSpoHeaders,
157
- "X-RequestStats": clientCorelationId,
184
+ "X-RequestStats": clientCorrelationId,
158
185
  };
159
186
  return response;
160
187
  }).catch(async (error) => {
@@ -166,8 +193,7 @@ export class EpochTracker implements IPersistedFileCache {
166
193
  await this.checkForEpochError(error, epochFromResponse, fetchType);
167
194
  throw error;
168
195
  }).catch((error) => {
169
- const fluidError = normalizeError(error, {props: {"X-RequestStats": clientCorelationId}});
170
- // eslint-disable-next-line @typescript-eslint/no-throw-literal
196
+ const fluidError = normalizeError(error, { props: { XRequestStatsHeader: clientCorrelationId }});
171
197
  throw fluidError;
172
198
  });
173
199
  }
@@ -185,9 +211,9 @@ export class EpochTracker implements IPersistedFileCache {
185
211
  fetchType: FetchType,
186
212
  addInBody: boolean = false,
187
213
  ) {
188
- const clientCorelationId = this.formatClientCorelationId();
214
+ const clientCorrelationId = this.formatClientCorrelationId();
189
215
  // Add epoch in fetch request.
190
- this.addEpochInRequest(fetchOptions, addInBody, clientCorelationId);
216
+ this.addEpochInRequest(fetchOptions, addInBody, clientCorrelationId);
191
217
  let epochFromResponse: string | undefined;
192
218
  return this.rateLimiter.schedule(
193
219
  async () => fetchArray(url, fetchOptions),
@@ -196,7 +222,7 @@ export class EpochTracker implements IPersistedFileCache {
196
222
  this.validateEpochFromResponse(epochFromResponse, fetchType);
197
223
  response.commonSpoHeaders = {
198
224
  ...response.commonSpoHeaders,
199
- "X-RequestStats": clientCorelationId,
225
+ "X-RequestStats": clientCorrelationId,
200
226
  };
201
227
  return response;
202
228
  }).catch(async (error) => {
@@ -208,8 +234,7 @@ export class EpochTracker implements IPersistedFileCache {
208
234
  await this.checkForEpochError(error, epochFromResponse, fetchType);
209
235
  throw error;
210
236
  }).catch((error) => {
211
- const fluidError = normalizeError(error, {props: {"X-RequestStats": clientCorelationId}});
212
- // eslint-disable-next-line @typescript-eslint/no-throw-literal
237
+ const fluidError = normalizeError(error, { props: { XRequestStatsHeader: clientCorrelationId }});
213
238
  throw fluidError;
214
239
  });
215
240
  }
@@ -217,11 +242,11 @@ export class EpochTracker implements IPersistedFileCache {
217
242
  private addEpochInRequest(
218
243
  fetchOptions: RequestInit,
219
244
  addInBody: boolean,
220
- clientCorelationId: string,
245
+ clientCorrelationId: string,
221
246
  ) {
222
247
  if (addInBody) {
223
248
  const headers: {[key: string]: string} = {};
224
- headers["X-RequestStats"] = clientCorelationId;
249
+ headers["X-RequestStats"] = clientCorrelationId;
225
250
  if (this.fluidEpoch !== undefined) {
226
251
  headers["x-fluid-epoch"] = this.fluidEpoch;
227
252
  }
@@ -234,7 +259,7 @@ export class EpochTracker implements IPersistedFileCache {
234
259
  assert(fetchOptions.headers !== undefined, 0x282 /* "Headers should be present now" */);
235
260
  fetchOptions.headers[key] = val;
236
261
  };
237
- addHeader("X-RequestStats", clientCorelationId);
262
+ addHeader("X-RequestStats", clientCorrelationId);
238
263
  if (this.fluidEpoch !== undefined) {
239
264
  addHeader("x-fluid-epoch", this.fluidEpoch);
240
265
  }
@@ -259,7 +284,7 @@ export class EpochTracker implements IPersistedFileCache {
259
284
  fetchOptions.body = formParams.join("\r\n");
260
285
  }
261
286
 
262
- private formatClientCorelationId() {
287
+ private formatClientCorrelationId() {
263
288
  return `driverId=${this.driverId}, RequestNumber=${this.networkCallNumber++}`;
264
289
  }
265
290
 
@@ -268,7 +293,10 @@ export class EpochTracker implements IPersistedFileCache {
268
293
  fetchType: FetchTypeInternal,
269
294
  fromCache: boolean = false,
270
295
  ) {
271
- this.checkForEpochErrorCore(epochFromResponse);
296
+ const error = this.checkForEpochErrorCore(epochFromResponse);
297
+ if (error !== undefined) {
298
+ throw error;
299
+ }
272
300
  if (epochFromResponse !== undefined) {
273
301
  if (this._fluidEpoch === undefined) {
274
302
  this.setEpoch(epochFromResponse, fromCache, fetchType);
@@ -283,11 +311,9 @@ export class EpochTracker implements IPersistedFileCache {
283
311
  fromCache: boolean = false,
284
312
  ) {
285
313
  if (isFluidError(error) && error.errorType === DriverErrorType.fileOverwrittenInStorage) {
286
- try {
287
- // This will only throw if it is an epoch error.
288
- this.checkForEpochErrorCore(epochFromResponse);
289
- } catch (epochError) {
290
- assert(isValidLegacyError(epochError),
314
+ const epochError = this.checkForEpochErrorCore(epochFromResponse);
315
+ if (epochError !== undefined) {
316
+ assert(isFluidError(epochError),
291
317
  0x21f /* "epochError expected to be thrown by throwOdspNetworkError and of known type" */);
292
318
  epochError.addTelemetryProperties({
293
319
  fromCache,
@@ -297,7 +323,6 @@ export class EpochTracker implements IPersistedFileCache {
297
323
  this.logger.sendErrorEvent({ eventName: "fileOverwrittenInStorage" }, epochError);
298
324
  // If the epoch mismatches, then clear all entries for such file entry from cache.
299
325
  await this.removeEntries();
300
- // eslint-disable-next-line @typescript-eslint/no-throw-literal
301
326
  throw epochError;
302
327
  }
303
328
  // If it was categorized as epoch error but the epoch returned in response matches with the client epoch
@@ -312,7 +337,9 @@ export class EpochTracker implements IPersistedFileCache {
312
337
  // initializes this value. Sometimes response does not contain epoch as it is still in
313
338
  // implementation phase at server side. In that case also, don't compare it with our epoch value.
314
339
  if (this.fluidEpoch && epochFromResponse && (this.fluidEpoch !== epochFromResponse)) {
315
- throwOdspNetworkError("epochMismatch", fluidEpochMismatchError);
340
+ // This is similar in nature to how fluidEpochMismatchError (409) is handled.
341
+ // Difference - client detected mismatch, instead of server detecting it.
342
+ return new NonRetryableError("epochMismatch", "Epoch mismatch", DriverErrorType.fileOverwrittenInStorage);
316
343
  }
317
344
  }
318
345
 
@@ -101,10 +101,11 @@ export async function fetchSnapshotWithRedeem(
101
101
  logger.sendErrorEvent({
102
102
  eventName: "RedeemFallback",
103
103
  errorType: error.errorType,
104
- });
104
+ }, error);
105
105
  await redeemSharingLink(odspResolvedUrl, storageTokenFetcher, logger);
106
106
  const odspResolvedUrlWithoutShareLink: IOdspResolvedUrl =
107
107
  { ...odspResolvedUrl, sharingLinkToRedeem: undefined };
108
+
108
109
  if(odspResolvedUrlWithoutShareLink.shareLinkInfo) {
109
110
  odspResolvedUrlWithoutShareLink.shareLinkInfo = {
110
111
  ...odspResolvedUrlWithoutShareLink.shareLinkInfo,
@@ -4,9 +4,10 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
- import { delay, PromiseCache } from "@fluidframework/common-utils";
8
- import { canRetryOnError, getRetryDelayFromError } from "@fluidframework/driver-utils";
7
+ import { assert, delay } from "@fluidframework/common-utils";
8
+ import { canRetryOnError, getRetryDelayFromError, NonRetryableError } from "@fluidframework/driver-utils";
9
9
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
10
+ import { DriverErrorType } from "@fluidframework/driver-definitions";
10
11
  import {
11
12
  IOdspUrlParts,
12
13
  OdspResourceTokenFetchOptions,
@@ -18,7 +19,7 @@ import { getUrlAndHeadersWithAuth } from "./getUrlAndHeadersWithAuth";
18
19
  import { fetchHelper, getWithRetryForTokenRefresh } from "./odspUtils";
19
20
 
20
21
  // Store cached responses for the lifetime of web session as file link remains the same for given file item
21
- const fileLinkCache = new PromiseCache<string, string | undefined>();
22
+ const fileLinkCache = new Map<string, Promise<string>>();
22
23
 
23
24
  /**
24
25
  * Returns file link for a file with given drive and item ids.
@@ -39,10 +40,11 @@ export async function getFileLink(
39
40
  odspUrlParts: IOdspUrlParts,
40
41
  identityType: IdentityType,
41
42
  logger: ITelemetryLogger,
42
- ): Promise<string | undefined> {
43
+ ): Promise<string> {
43
44
  const cacheKey = `${odspUrlParts.siteUrl}_${odspUrlParts.driveId}_${odspUrlParts.itemId}`;
44
- if (fileLinkCache.has(cacheKey)) {
45
- return fileLinkCache.get(cacheKey);
45
+ const maybeFileLinkCacheEntry = fileLinkCache.get(cacheKey);
46
+ if (maybeFileLinkCacheEntry !== undefined) {
47
+ return maybeFileLinkCacheEntry;
46
48
  }
47
49
 
48
50
  const valueGenerator = async function() {
@@ -54,10 +56,11 @@ export async function getFileLink(
54
56
  result = await getFileLinkCore(getToken, odspUrlParts, identityType, logger);
55
57
  success = true;
56
58
  } catch (err) {
57
- // If it is not retriable, then just return undefined
59
+ // If it is not retriable, then just throw
58
60
  if (!canRetryOnError(err)) {
59
- fileLinkCache.remove(cacheKey);
60
- return undefined;
61
+ // Delete from the cache to permit retrying later.
62
+ fileLinkCache.delete(cacheKey);
63
+ throw err;
61
64
  }
62
65
  // If the error is throttling error, then wait for the specified time before retrying.
63
66
  // If the waitTime is not specified, then we start with retrying immediately to max of 8s.
@@ -65,10 +68,14 @@ export async function getFileLink(
65
68
  await delay(retryAfterMs);
66
69
  }
67
70
  } while (!success);
71
+
72
+ // We are guaranteed to run the getFileLinkCore at least once with successful result (which must be a string)
73
+ assert(result !== undefined, 0x292 /* "Unexpected undefined result from getFileLinkCore" */);
68
74
  return result;
69
75
  };
70
- fileLinkCache.add(cacheKey, valueGenerator);
71
- return fileLinkCache.get(cacheKey);
76
+ const fileLink = valueGenerator();
77
+ fileLinkCache.set(cacheKey, fileLink);
78
+ return fileLink;
72
79
  }
73
80
 
74
81
  async function getFileLinkCore(
@@ -76,11 +83,8 @@ async function getFileLinkCore(
76
83
  odspUrlParts: IOdspUrlParts,
77
84
  identityType: IdentityType,
78
85
  logger: ITelemetryLogger,
79
- ): Promise<string | undefined> {
86
+ ): Promise<string> {
80
87
  const fileItem = await getFileItemLite(getToken, odspUrlParts, logger);
81
- if (!fileItem) {
82
- return undefined;
83
- }
84
88
 
85
89
  // ODC canonical link does not require any additional processing
86
90
  if (identityType === "Consumer") {
@@ -111,11 +115,17 @@ async function getFileLinkCore(
111
115
  };
112
116
  const response = await fetchHelper(url, requestInit);
113
117
  additionalProps = response.commonSpoHeaders;
114
- if (response.content.ok) {
115
- const sharingInfo = await response.content.json();
116
- return sharingInfo?.d?.directUrl as string;
118
+
119
+ const sharingInfo = await response.content.json();
120
+ const directUrl = sharingInfo?.d?.directUrl;
121
+ if (typeof directUrl !== "string") {
122
+ // This will retry once in getWithRetryForTokenRefresh
123
+ throw new NonRetryableError(
124
+ "getFileLinkCoreMalformedResponse",
125
+ "Malformed GetSharingInformation response",
126
+ DriverErrorType.incorrectServerResponse);
117
127
  }
118
- return undefined;
128
+ return directUrl;
119
129
  });
120
130
  event.end({ ...additionalProps, attempts });
121
131
  return fileLink;
@@ -131,11 +141,18 @@ interface FileItemLite {
131
141
  webDavUrl: string;
132
142
  }
133
143
 
144
+ const isFileItemLite = (maybeFileItemLite: any): maybeFileItemLite is FileItemLite => {
145
+ if (typeof maybeFileItemLite.webUrl !== "string" || typeof maybeFileItemLite.webDavUrl !== "string") {
146
+ return false;
147
+ }
148
+ return true;
149
+ };
150
+
134
151
  async function getFileItemLite(
135
152
  getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
136
153
  odspUrlParts: IOdspUrlParts,
137
154
  logger: ITelemetryLogger,
138
- ): Promise<FileItemLite | undefined> {
155
+ ): Promise<FileItemLite> {
139
156
  return PerformanceEvent.timedExecAsync(
140
157
  logger,
141
158
  { eventName: "odspFileLink", requestName: "getFileItemLite" },
@@ -153,16 +170,19 @@ async function getFileItemLite(
153
170
  const requestInit = { method: "GET", headers };
154
171
  const response = await fetchHelper(url, requestInit);
155
172
  additionalProps = response.commonSpoHeaders;
156
- if (response.content.ok) {
157
- return await response.content.json() as FileItemLite;
173
+
174
+ const responseJson = await response.content.json();
175
+ if (!isFileItemLite(responseJson)) {
176
+ // This will retry once in getWithRetryForTokenRefresh
177
+ throw new NonRetryableError(
178
+ "getFileItemLiteMalformedResponse",
179
+ "Malformed getFileItemLite response",
180
+ DriverErrorType.incorrectServerResponse);
158
181
  }
159
- return undefined;
182
+ return responseJson;
160
183
  });
161
184
  event.end({ ...additionalProps, attempts });
162
- if (fileItem && fileItem.webDavUrl && fileItem.webUrl) {
163
- return fileItem;
164
- }
165
- return undefined;
185
+ return fileItem;
166
186
  },
167
187
  );
168
188
  }
@@ -41,6 +41,7 @@ export class OdspDeltaStorageService {
41
41
  from: number,
42
42
  to: number,
43
43
  telemetryProps: ITelemetryProperties,
44
+ fetchReason?: string,
44
45
  ): Promise<IDeltasFetchResult> {
45
46
  return getWithRetryForTokenRefresh(async (options) => {
46
47
  // Note - this call ends up in getSocketStorageDiscovery() and can refresh token
@@ -52,6 +53,9 @@ export class OdspDeltaStorageService {
52
53
  let postBody = `--${formBoundary}\r\n`;
53
54
  postBody += `Authorization: Bearer ${storageToken}\r\n`;
54
55
  postBody += `X-HTTP-Method-Override: GET\r\n`;
56
+ if (fetchReason !== undefined) {
57
+ postBody += `fetchReason: ${fetchReason}\r\n`;
58
+ }
55
59
 
56
60
  postBody += `_post: 1\r\n`;
57
61
  postBody += `\r\n--${formBoundary}--`;
@@ -123,7 +127,8 @@ export class OdspDeltaStorageWithCache implements IDocumentDeltaStorageService {
123
127
  private readonly getFromStorage: (
124
128
  from: number,
125
129
  to: number,
126
- telemetryProps: ITelemetryProperties) => Promise<IDeltasFetchResult>,
130
+ telemetryProps: ITelemetryProperties,
131
+ fetchReason?: string) => Promise<IDeltasFetchResult>,
127
132
  private readonly getCached: (from: number, to: number) => Promise<ISequencedDocumentMessage[]>,
128
133
  private readonly requestFromSocket: (from: number, to: number) => void,
129
134
  private readonly opsReceived: (ops: ISequencedDocumentMessage[]) => void,
@@ -151,7 +156,8 @@ export class OdspDeltaStorageWithCache implements IDocumentDeltaStorageService {
151
156
  fromTotal: number,
152
157
  toTotal: number | undefined,
153
158
  abortSignal?: AbortSignal,
154
- cachedOnly?: boolean)
159
+ cachedOnly?: boolean,
160
+ fetchReason?: string)
155
161
  {
156
162
  // We do not control what's in the cache. Current API assumes that fetchMessages() keeps banging on
157
163
  // storage / cache until it gets ops it needs. This would result in deadlock if fixed range is asked from
@@ -198,7 +204,7 @@ export class OdspDeltaStorageWithCache implements IDocumentDeltaStorageService {
198
204
  return { messages: [], partialResult: false };
199
205
  }
200
206
 
201
- const ops = await this.getFromStorage(from, to, telemetryProps);
207
+ const ops = await this.getFromStorage(from, to, telemetryProps, fetchReason);
202
208
  this.validateMessages("storage", ops.messages, from);
203
209
  opsFromStorage += ops.messages.length;
204
210
  this.opsReceived(ops.messages);
@@ -220,6 +226,7 @@ export class OdspDeltaStorageWithCache implements IDocumentDeltaStorageService {
220
226
  this.batchSize,
221
227
  this.logger,
222
228
  abortSignal,
229
+ fetchReason,
223
230
  );
224
231
 
225
232
  return streamObserver(stream, (result) => {
@@ -20,6 +20,7 @@ import { v4 as uuid } from "uuid";
20
20
  import { IOdspSocketError, IGetOpsResponse, IFlushOpsResponse } from "./contracts";
21
21
  import { EpochTracker } from "./epochTracker";
22
22
  import { errorObjectFromSocketError } from "./odspError";
23
+ import { pkgVersion } from "./packageVersion";
23
24
 
24
25
  const protocolVersions = ["^0.4.0", "^0.3.0", "^0.2.0", "^0.1.0"];
25
26
  const feature_get_ops = "api_get_ops";
@@ -226,6 +227,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
226
227
  versions: protocolVersions,
227
228
  nonce: uuid(),
228
229
  epoch: epochTracker.fluidEpoch,
230
+ relayUserAgent: [client.details.environment, ` driverVersion:${pkgVersion}`].join(";"),
229
231
  };
230
232
 
231
233
  // Reference to this client supporting get_ops flow.
@@ -14,8 +14,8 @@ import {
14
14
  IDocumentStorageService,
15
15
  IDocumentServicePolicies,
16
16
  } from "@fluidframework/driver-definitions";
17
- import { DeltaStreamConnectionForbiddenError } from "@fluidframework/driver-utils";
18
- import { fetchTokenErrorCode, IFacetCodes, throwOdspNetworkError } from "@fluidframework/odsp-doclib-utils";
17
+ import { DeltaStreamConnectionForbiddenError, NonRetryableError } from "@fluidframework/driver-utils";
18
+ import { IFacetCodes } from "@fluidframework/odsp-doclib-utils";
19
19
  import {
20
20
  IClient,
21
21
  ISequencedDocumentMessage,
@@ -26,6 +26,7 @@ import {
26
26
  IEntry,
27
27
  HostStoragePolicy,
28
28
  InstrumentedStorageTokenFetcher,
29
+ OdspErrorType,
29
30
  } from "@fluidframework/odsp-driver-definitions";
30
31
  import { HostStoragePolicyInternal, ISocketStorageDiscovery } from "./contracts";
31
32
  import { IOdspCache } from "./odspCache";
@@ -211,7 +212,7 @@ export class OdspDocumentService implements IDocumentService {
211
212
  this.logger,
212
213
  batchSize,
213
214
  concurrency,
214
- async (from, to, telemetryProps) => service.get(from, to, telemetryProps),
215
+ async (from, to, telemetryProps, fetchReason) => service.get(from, to, telemetryProps, fetchReason),
215
216
  async (from, to) => {
216
217
  const res = await this.opsCache?.get(from, to);
217
218
  return res as ISequencedDocumentMessage[] ?? [];
@@ -270,7 +271,10 @@ export class OdspDocumentService implements IDocumentService {
270
271
 
271
272
  const finalWebsocketToken = websocketToken ?? (websocketEndpoint.socketToken || null);
272
273
  if (finalWebsocketToken === null) {
273
- throwOdspNetworkError("pushTokenIsNull", fetchTokenErrorCode);
274
+ throw new NonRetryableError(
275
+ "pushTokenIsNull",
276
+ "Websocket token is null",
277
+ OdspErrorType.fetchTokenError);
274
278
  }
275
279
  try {
276
280
  const connection = await this.connectToDeltaStreamWithRetry(