@fluidframework/odsp-driver 0.57.2 → 0.58.1000

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 (124) hide show
  1. package/dist/contracts.d.ts +4 -0
  2. package/dist/contracts.d.ts.map +1 -1
  3. package/dist/contracts.js.map +1 -1
  4. package/dist/createFile.d.ts.map +1 -1
  5. package/dist/createFile.js +3 -3
  6. package/dist/createFile.js.map +1 -1
  7. package/dist/epochTracker.d.ts.map +1 -1
  8. package/dist/epochTracker.js +2 -3
  9. package/dist/epochTracker.js.map +1 -1
  10. package/dist/fetchSnapshot.d.ts.map +1 -1
  11. package/dist/fetchSnapshot.js +66 -25
  12. package/dist/fetchSnapshot.js.map +1 -1
  13. package/dist/getFileLink.js +2 -2
  14. package/dist/getFileLink.js.map +1 -1
  15. package/dist/odspCache.d.ts +8 -3
  16. package/dist/odspCache.d.ts.map +1 -1
  17. package/dist/odspCache.js +1 -1
  18. package/dist/odspCache.js.map +1 -1
  19. package/dist/odspDocumentDeltaConnection.js +1 -1
  20. package/dist/odspDocumentDeltaConnection.js.map +1 -1
  21. package/dist/odspDocumentService.d.ts +5 -0
  22. package/dist/odspDocumentService.d.ts.map +1 -1
  23. package/dist/odspDocumentService.js +88 -25
  24. package/dist/odspDocumentService.js.map +1 -1
  25. package/dist/odspDocumentStorageManager.d.ts.map +1 -1
  26. package/dist/odspDocumentStorageManager.js +5 -5
  27. package/dist/odspDocumentStorageManager.js.map +1 -1
  28. package/dist/odspDriverUrlResolver.d.ts +2 -2
  29. package/dist/odspDriverUrlResolver.d.ts.map +1 -1
  30. package/dist/odspDriverUrlResolver.js +14 -3
  31. package/dist/odspDriverUrlResolver.js.map +1 -1
  32. package/dist/odspDriverUrlResolverForShareLink.d.ts +2 -2
  33. package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  34. package/dist/odspDriverUrlResolverForShareLink.js +15 -4
  35. package/dist/odspDriverUrlResolverForShareLink.js.map +1 -1
  36. package/dist/odspError.d.ts.map +1 -1
  37. package/dist/odspError.js +2 -2
  38. package/dist/odspError.js.map +1 -1
  39. package/dist/odspUtils.d.ts.map +1 -1
  40. package/dist/odspUtils.js +10 -10
  41. package/dist/odspUtils.js.map +1 -1
  42. package/dist/packageVersion.d.ts +1 -1
  43. package/dist/packageVersion.d.ts.map +1 -1
  44. package/dist/packageVersion.js +1 -1
  45. package/dist/packageVersion.js.map +1 -1
  46. package/dist/retryErrorsStorageAdapter.js +1 -1
  47. package/dist/retryErrorsStorageAdapter.js.map +1 -1
  48. package/dist/vroom.d.ts +2 -1
  49. package/dist/vroom.d.ts.map +1 -1
  50. package/dist/vroom.js +6 -2
  51. package/dist/vroom.js.map +1 -1
  52. package/dist/zipItDataRepresentationUtils.js +1 -1
  53. package/dist/zipItDataRepresentationUtils.js.map +1 -1
  54. package/lib/contracts.d.ts +4 -0
  55. package/lib/contracts.d.ts.map +1 -1
  56. package/lib/contracts.js.map +1 -1
  57. package/lib/createFile.d.ts.map +1 -1
  58. package/lib/createFile.js +3 -3
  59. package/lib/createFile.js.map +1 -1
  60. package/lib/epochTracker.d.ts.map +1 -1
  61. package/lib/epochTracker.js +2 -3
  62. package/lib/epochTracker.js.map +1 -1
  63. package/lib/fetchSnapshot.d.ts.map +1 -1
  64. package/lib/fetchSnapshot.js +66 -25
  65. package/lib/fetchSnapshot.js.map +1 -1
  66. package/lib/getFileLink.js +2 -2
  67. package/lib/getFileLink.js.map +1 -1
  68. package/lib/odspCache.d.ts +8 -3
  69. package/lib/odspCache.d.ts.map +1 -1
  70. package/lib/odspCache.js +1 -1
  71. package/lib/odspCache.js.map +1 -1
  72. package/lib/odspDocumentDeltaConnection.js +1 -1
  73. package/lib/odspDocumentDeltaConnection.js.map +1 -1
  74. package/lib/odspDocumentService.d.ts +5 -0
  75. package/lib/odspDocumentService.d.ts.map +1 -1
  76. package/lib/odspDocumentService.js +88 -25
  77. package/lib/odspDocumentService.js.map +1 -1
  78. package/lib/odspDocumentStorageManager.d.ts.map +1 -1
  79. package/lib/odspDocumentStorageManager.js +5 -5
  80. package/lib/odspDocumentStorageManager.js.map +1 -1
  81. package/lib/odspDriverUrlResolver.d.ts +2 -2
  82. package/lib/odspDriverUrlResolver.d.ts.map +1 -1
  83. package/lib/odspDriverUrlResolver.js +15 -4
  84. package/lib/odspDriverUrlResolver.js.map +1 -1
  85. package/lib/odspDriverUrlResolverForShareLink.d.ts +2 -2
  86. package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  87. package/lib/odspDriverUrlResolverForShareLink.js +15 -4
  88. package/lib/odspDriverUrlResolverForShareLink.js.map +1 -1
  89. package/lib/odspError.d.ts.map +1 -1
  90. package/lib/odspError.js +2 -2
  91. package/lib/odspError.js.map +1 -1
  92. package/lib/odspUtils.d.ts.map +1 -1
  93. package/lib/odspUtils.js +10 -10
  94. package/lib/odspUtils.js.map +1 -1
  95. package/lib/packageVersion.d.ts +1 -1
  96. package/lib/packageVersion.d.ts.map +1 -1
  97. package/lib/packageVersion.js +1 -1
  98. package/lib/packageVersion.js.map +1 -1
  99. package/lib/retryErrorsStorageAdapter.js +1 -1
  100. package/lib/retryErrorsStorageAdapter.js.map +1 -1
  101. package/lib/vroom.d.ts +2 -1
  102. package/lib/vroom.d.ts.map +1 -1
  103. package/lib/vroom.js +6 -2
  104. package/lib/vroom.js.map +1 -1
  105. package/lib/zipItDataRepresentationUtils.js +1 -1
  106. package/lib/zipItDataRepresentationUtils.js.map +1 -1
  107. package/package.json +12 -12
  108. package/src/contracts.ts +5 -0
  109. package/src/createFile.ts +2 -4
  110. package/src/epochTracker.ts +5 -4
  111. package/src/fetchSnapshot.ts +66 -33
  112. package/src/getFileLink.ts +0 -2
  113. package/src/odspCache.ts +3 -3
  114. package/src/odspDocumentDeltaConnection.ts +1 -1
  115. package/src/odspDocumentService.ts +110 -28
  116. package/src/odspDocumentStorageManager.ts +1 -6
  117. package/src/odspDriverUrlResolver.ts +18 -5
  118. package/src/odspDriverUrlResolverForShareLink.ts +17 -9
  119. package/src/odspError.ts +1 -2
  120. package/src/odspUtils.ts +10 -13
  121. package/src/packageVersion.ts +1 -1
  122. package/src/retryErrorsStorageAdapter.ts +1 -1
  123. package/src/vroom.ts +6 -0
  124. package/src/zipItDataRepresentationUtils.ts +1 -2
@@ -323,8 +323,6 @@ export class EpochTracker implements IPersistedFileCache {
323
323
  if (isFluidError(error) && error.errorType === DriverErrorType.fileOverwrittenInStorage) {
324
324
  const epochError = this.checkForEpochErrorCore(epochFromResponse);
325
325
  if (epochError !== undefined) {
326
- assert(isFluidError(epochError),
327
- 0x21f /* "epochError expected to be thrown by throwOdspNetworkError and of known type" */);
328
326
  epochError.addTelemetryProperties({
329
327
  fromCache,
330
328
  clientEpoch: this.fluidEpoch,
@@ -338,7 +336,10 @@ export class EpochTracker implements IPersistedFileCache {
338
336
  // If it was categorized as epoch error but the epoch returned in response matches with the client epoch
339
337
  // then it was coherency 409, so rethrow it as throttling error so that it can retried. Default throttling
340
338
  // time is 1s.
341
- throw new ThrottlingError("coherency409", error.message, 1, { [Odsp409Error]: true, driverVersion });
339
+ throw new ThrottlingError(
340
+ `Coherency 409: ${error.message}`,
341
+ 1 /* retryAfterSeconds */,
342
+ { [Odsp409Error]: true, driverVersion });
342
343
  }
343
344
  }
344
345
 
@@ -350,7 +351,7 @@ export class EpochTracker implements IPersistedFileCache {
350
351
  // This is similar in nature to how fluidEpochMismatchError (409) is handled.
351
352
  // Difference - client detected mismatch, instead of server detecting it.
352
353
  return new NonRetryableError(
353
- "epochMismatch", "Epoch mismatch", DriverErrorType.fileOverwrittenInStorage, { driverVersion });
354
+ "Epoch mismatch", DriverErrorType.fileOverwrittenInStorage, { driverVersion });
354
355
  }
355
356
  }
356
357
 
@@ -216,23 +216,33 @@ async function fetchLatestSnapshotCore(
216
216
  logger,
217
217
  perfEvent,
218
218
  async (event) => {
219
- const startTime = performance.now();
220
219
  const response = await snapshotDownloader(
221
220
  odspResolvedUrl,
222
221
  storageToken,
223
222
  snapshotOptions,
224
223
  controller,
225
224
  );
226
- const endTime = performance.now();
227
- const overallTime = endTime - startTime;
228
225
  const snapshot = response.odspSnapshotResponse.content;
229
- let dnstime: number | undefined; // domainLookupEnd - domainLookupStart
230
- let redirectTime: number | undefined; // redirectEnd -redirectStart
226
+ // From: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming
227
+ // fetchStart: immediately before the browser starts to fetch the resource.
228
+ // requestStart: immediately before the browser starts requesting the resource from the server
229
+ // responseStart: immediately after the browser receives the first byte of the response from the server.
230
+ // responseEnd: immediately after the browser receives the last byte of the resource
231
+ // or immediately before the transport connection is closed, whichever comes first.
232
+ // secureConnectionStart: immediately before the browser starts the handshake process to secure the
233
+ // current connection. If a secure connection is not used, this property returns zero.
234
+ // startTime: Time when the resource fetch started. This value is equivalent to fetchStart.
235
+ // domainLookupStart: immediately before the browser starts the domain name lookup for the resource.
236
+ // domainLookupEnd: immediately after the browser finishes the domain name lookup for the resource.
237
+ // redirectStart: start time of the fetch which that initiates the redirect.
238
+ // redirectEnd: immediately after receiving the last byte of the response of the last redirect.
239
+ let dnsLookupTime: number | undefined; // domainLookupEnd - domainLookupStart
240
+ let redirectTime: number | undefined; // redirectEnd - redirectStart
231
241
  let tcpHandshakeTime: number | undefined; // connectEnd - connectStart
232
- let secureConntime: number | undefined; // connectEnd - secureConnectionStart
233
- let responseTime: number | undefined; // responsEnd - responseStart
234
- let fetchStToRespEndTime: number | undefined; // responseEnd - fetchStart
235
- let reqStToRespEndTime: number | undefined; // responseEnd - requestStart
242
+ let secureConnectionTime: number | undefined; // connectEnd - secureConnectionStart
243
+ let responseNetworkTime: number | undefined; // responsEnd - responseStart
244
+ let fetchStartToResponseEndTime: number | undefined; // responseEnd - fetchStart
245
+ let reqStartToResponseEndTime: number | undefined; // responseEnd - requestStart
236
246
  let networkTime: number | undefined; // responseEnd - startTime
237
247
  const spReqDuration = response.odspSnapshotResponse.headers.get("sprequestduration");
238
248
 
@@ -246,17 +256,19 @@ async function fetchLatestSnapshotCore(
246
256
  if ((resource_initiatortype.localeCompare("fetch") === 0)
247
257
  && (resource_name.localeCompare(response.requestUrl) === 0)) {
248
258
  redirectTime = indResTime.redirectEnd - indResTime.redirectStart;
249
- dnstime = indResTime.domainLookupEnd - indResTime.domainLookupStart;
259
+ dnsLookupTime = indResTime.domainLookupEnd - indResTime.domainLookupStart;
250
260
  tcpHandshakeTime = indResTime.connectEnd - indResTime.connectStart;
251
- secureConntime = (indResTime.secureConnectionStart > 0) ?
252
- (indResTime.connectEnd - indResTime.secureConnectionStart) : 0;
253
- responseTime = indResTime.responseEnd - indResTime.responseStart;
254
- fetchStToRespEndTime = (indResTime.fetchStart > 0) ?
255
- (indResTime.responseEnd - indResTime.fetchStart) : 0;
256
- reqStToRespEndTime = (indResTime.requestStart > 0) ?
257
- (indResTime.responseEnd - indResTime.requestStart) : 0;
258
- networkTime = (indResTime.startTime > 0) ? (indResTime.responseEnd - indResTime.startTime) : 0;
259
- if (spReqDuration) {
261
+ secureConnectionTime = (indResTime.secureConnectionStart > 0) ?
262
+ (indResTime.connectEnd - indResTime.secureConnectionStart) : undefined;
263
+ responseNetworkTime = (indResTime.responseStart > 0) ?
264
+ (indResTime.responseEnd - indResTime.responseStart) : undefined;
265
+ fetchStartToResponseEndTime = (indResTime.fetchStart > 0) ?
266
+ (indResTime.responseEnd - indResTime.fetchStart) : undefined;
267
+ reqStartToResponseEndTime = (indResTime.requestStart > 0) ?
268
+ (indResTime.responseEnd - indResTime.requestStart) : undefined;
269
+ networkTime = (indResTime.startTime > 0) ?
270
+ (indResTime.responseEnd - indResTime.fetchStart) : undefined;
271
+ if (spReqDuration !== undefined && networkTime !== undefined) {
260
272
  networkTime = networkTime - parseInt(spReqDuration, 10);
261
273
  }
262
274
  break;
@@ -265,7 +277,6 @@ async function fetchLatestSnapshotCore(
265
277
 
266
278
  const { numTrees, numBlobs, encodedBlobsSize } =
267
279
  validateAndEvalBlobsAndTrees(response.odspSnapshotResponse.content);
268
- const clientTime = networkTime ? overallTime - networkTime : undefined;
269
280
 
270
281
  // There are some scenarios in ODSP where we cannot cache, trees/latest will explicitly tell us when we
271
282
  // cannot cache using an HTTP response header.
@@ -303,16 +314,23 @@ async function fetchLatestSnapshotCore(
303
314
  sequenceNumber,
304
315
  ops: snapshot.ops?.length ?? 0,
305
316
  headers: Object.keys(response.requestHeaders).length !== 0 ? true : undefined,
306
- redirecttime: redirectTime,
307
- dnsLookuptime: dnstime,
308
- responsenetworkTime: responseTime,
309
- tcphandshakeTime: tcpHandshakeTime,
310
- secureconnectiontime: secureConntime,
311
- fetchstarttorespendtime: fetchStToRespEndTime,
312
- reqstarttorespendtime: reqStToRespEndTime,
313
- overalltime: overallTime,
314
- networktime: networkTime,
315
- clienttime: clientTime,
317
+ // Interval between the first fetch until the last byte of the last redirect.
318
+ redirectTime,
319
+ // Interval between start and finish of the domain name lookup for the resource.
320
+ dnsLookupTime,
321
+ // Interval to receive all (first to last) bytes form the server.
322
+ responseNetworkTime,
323
+ // Time to establish the connection to the server to retrieve the resource.
324
+ tcpHandshakeTime,
325
+ // Time from the end of the connection until the inital handshake process to secure the connection.
326
+ secureConnectionTime,
327
+ // Interval between the initial fetch until the last byte is received.
328
+ fetchStartToResponseEndTime,
329
+ // Interval between starting the request for the resource until receiving the last byte.
330
+ reqStartToResponseEndTime,
331
+ // Interval between starting the request for the resource until receiving the last byte but
332
+ // excluding the Snaphot request duration indicated on the snapshot response header.
333
+ networkTime,
316
334
  // Sharing link telemetry regarding sharing link redeem status and performance. Ex: FRL; dur=100,
317
335
  // Azure Fluid Relay service; desc=S, FRP; desc=False. Here, FRL is the duration taken for redeem,
318
336
  // Azure Fluid Relay service is the redeem status (S means success), and FRP is a flag to indicate
@@ -360,7 +378,14 @@ async function fetchSnapshotContentsCoreV1(
360
378
  ): Promise<ISnapshotRequestAndResponseOptions> {
361
379
  const snapshotUrl = odspResolvedUrl.endpoints.snapshotStorageUrl;
362
380
  const url = `${snapshotUrl}/trees/latest?ump=1`;
363
- const { body, headers } = getFormBodyAndHeaders(odspResolvedUrl, storageToken, snapshotOptions);
381
+ // The location of file can move on Spo in which case server returns 308(Permanent Redirect) error.
382
+ // Adding below header will make VROOM API return 404 instead of 308 and browser can intercept it.
383
+ // This error thrown by server will contain the new redirect location. Look at the 404 error parsing
384
+ // for futher reference here: \packages\utils\odsp-doclib-utils\src\odspErrorUtils.ts
385
+ const header = {"prefer": "manualredirect"};
386
+ const { body, headers } = getFormBodyAndHeaders(
387
+ odspResolvedUrl, storageToken, snapshotOptions, header);
388
+ headers.accept = "application/json";
364
389
  const fetchOptions = {
365
390
  body,
366
391
  headers,
@@ -422,6 +447,7 @@ function getFormBodyAndHeaders(
422
447
  odspResolvedUrl: IOdspResolvedUrl,
423
448
  storageToken: string,
424
449
  snapshotOptions: ISnapshotOptions | undefined,
450
+ headers?: {[index: string]: string},
425
451
  ) {
426
452
  const formBoundary = uuid();
427
453
  const formParams: string[] = [];
@@ -435,16 +461,23 @@ function getFormBodyAndHeaders(
435
461
  }
436
462
  });
437
463
  }
464
+ if (headers !== undefined) {
465
+ Object.entries(headers).forEach(([key, value]) => {
466
+ if (value !== undefined) {
467
+ formParams.push(`${key}: ${value}`);
468
+ }
469
+ });
470
+ }
438
471
  if (odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem) {
439
472
  formParams.push(`sl: ${odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem}`);
440
473
  }
441
474
  formParams.push(`_post: 1`);
442
475
  formParams.push(`\r\n--${formBoundary}--`);
443
476
  const postBody = formParams.join("\r\n");
444
- const headers: {[index: string]: any} = {
477
+ const header: {[index: string]: any} = {
445
478
  "Content-Type": `multipart/form-data;boundary=${formBoundary}`,
446
479
  };
447
- return { body: postBody, headers };
480
+ return { body: postBody, headers: header };
448
481
  }
449
482
 
450
483
  function validateAndEvalBlobsAndTrees(snapshot: ISnapshotContents) {
@@ -125,7 +125,6 @@ async function getFileLinkCore(
125
125
  if (typeof directUrl !== "string") {
126
126
  // This will retry once in getWithRetryForTokenRefresh
127
127
  throw new NonRetryableError(
128
- "getFileLinkCoreMalformedResponse",
129
128
  "Malformed GetSharingInformation response",
130
129
  DriverErrorType.incorrectServerResponse,
131
130
  { driverVersion });
@@ -182,7 +181,6 @@ async function getFileItemLite(
182
181
  if (!isFileItemLite(responseJson)) {
183
182
  // This will retry once in getWithRetryForTokenRefresh
184
183
  throw new NonRetryableError(
185
- "getFileItemLiteMalformedResponse",
186
184
  "Malformed getFileItemLite response",
187
185
  DriverErrorType.incorrectServerResponse,
188
186
  { driverVersion });
package/src/odspCache.ts CHANGED
@@ -113,9 +113,8 @@ export class PromiseCacheWithOneHourSlidingExpiry<T> extends PromiseCache<string
113
113
  export interface INonPersistentCache {
114
114
  /**
115
115
  * Cache of joined/joining session info
116
- * This cache will use a one hour sliding expiration window.
117
116
  */
118
- readonly sessionJoinCache: PromiseCacheWithOneHourSlidingExpiry<ISocketStorageDiscovery>;
117
+ readonly sessionJoinCache: PromiseCache<string, {entryTime: number, joinSessionResponse: ISocketStorageDiscovery}>;
119
118
 
120
119
  /**
121
120
  * Cache of resolved/resolving file URLs
@@ -134,7 +133,8 @@ export interface IOdspCache extends INonPersistentCache {
134
133
  }
135
134
 
136
135
  export class NonPersistentCache implements INonPersistentCache {
137
- public readonly sessionJoinCache = new PromiseCacheWithOneHourSlidingExpiry<ISocketStorageDiscovery>();
136
+ public readonly sessionJoinCache =
137
+ new PromiseCache<string, {entryTime: number, joinSessionResponse: ISocketStorageDiscovery}>();
138
138
 
139
139
  public readonly fileUrlCache = new PromiseCache<string, IOdspResolvedUrl>();
140
140
  }
@@ -425,7 +425,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
425
425
  this.pushCallCounter++;
426
426
  const nonce = `${this.requestOpsNoncePrefix}${this.pushCallCounter}`;
427
427
  // There should be only one flush ops in flight, kicked out by upload summary workflow
428
- // That said, it could timeout, and request could be repeated, so theoretically we can
428
+ // That said, it could timeout and request could be repeated, so theoretically we can
429
429
  // get overlapping requests, but it should be very rare
430
430
  if (this.flushDeferred !== undefined) {
431
431
  this.logger.sendErrorEvent({ eventName: "FlushOpsTooMany" });
@@ -52,7 +52,8 @@ import { pkgVersion as driverVersion } from "./packageVersion";
52
52
  */
53
53
  export class OdspDocumentService implements IDocumentService {
54
54
  private _policies: IDocumentServicePolicies;
55
-
55
+ // Timer which runs and executes the join session call after intervals.
56
+ private joinSessionRefreshTimer: ReturnType<typeof setTimeout> | undefined;
56
57
  /**
57
58
  * @param resolvedUrl - resolved url identifying document that will be managed by returned service instance.
58
59
  * @param getStorageToken - function that can provide the storage token. This is is also referred to as
@@ -237,27 +238,7 @@ export class OdspDocumentService implements IDocumentService {
237
238
  ? Promise.resolve(null)
238
239
  : this.getWebsocketToken!(options);
239
240
 
240
- const joinSessionPromise = this.joinSession(requestWebsocketTokenFromJoinSession, options).catch((e) => {
241
- const likelyFacetCodes = e as IFacetCodes;
242
- if (Array.isArray(likelyFacetCodes.facetCodes)) {
243
- for (const code of likelyFacetCodes.facetCodes) {
244
- switch (code) {
245
- case "sessionForbiddenOnPreservedFiles":
246
- case "sessionForbiddenOnModerationEnabledLibrary":
247
- case "sessionForbiddenOnRequireCheckout":
248
- // This document can only be opened in storage-only mode.
249
- // DeltaManager will recognize this error
250
- // and load without a delta stream connection.
251
- this._policies = {...this._policies,storageOnly: true};
252
- throw new DeltaStreamConnectionForbiddenError(code, { driverVersion });
253
- default:
254
- continue;
255
- }
256
- }
257
- }
258
- throw e;
259
- });
260
-
241
+ const joinSessionPromise = this.joinSession(requestWebsocketTokenFromJoinSession, options);
261
242
  const [websocketEndpoint, websocketToken, io] =
262
243
  await Promise.all([
263
244
  joinSessionPromise,
@@ -268,7 +249,6 @@ export class OdspDocumentService implements IDocumentService {
268
249
  const finalWebsocketToken = websocketToken ?? (websocketEndpoint.socketToken || null);
269
250
  if (finalWebsocketToken === null) {
270
251
  throw new NonRetryableError(
271
- "pushTokenIsNull",
272
252
  "Websocket token is null",
273
253
  OdspErrorType.fetchTokenError,
274
254
  { driverVersion });
@@ -287,6 +267,8 @@ export class OdspDocumentService implements IDocumentService {
287
267
  // On disconnect with 401/403 error code, we can just clear the joinSession cache as we will again
288
268
  // get the auth error on reconnecting and face latency.
289
269
  connection.on("disconnect", (error: any) => {
270
+ // Clear the join session refresh timer so that it can be restarted on reconnection.
271
+ this.clearJoinSessionTimer();
290
272
  if (typeof error === "object" && error !== null
291
273
  && error.errorType === DriverErrorType.authorizationError) {
292
274
  this.cache.sessionJoinCache.remove(this.joinSessionKey);
@@ -304,12 +286,59 @@ export class OdspDocumentService implements IDocumentService {
304
286
  });
305
287
  }
306
288
 
289
+ private clearJoinSessionTimer() {
290
+ if (this.joinSessionRefreshTimer !== undefined) {
291
+ clearTimeout(this.joinSessionRefreshTimer);
292
+ this.joinSessionRefreshTimer = undefined;
293
+ }
294
+ }
295
+
296
+ private async scheduleJoinSessionRefresh(delta: number) {
297
+ await new Promise<void>((resolve, reject) => {
298
+ this.joinSessionRefreshTimer = setTimeout(() => {
299
+ getWithRetryForTokenRefresh(async (options) => {
300
+ await this.joinSession(false, options);
301
+ resolve();
302
+ }).catch((error) => {
303
+ reject(error);
304
+ });
305
+ }, delta);
306
+ });
307
+ }
308
+
307
309
  private async joinSession(
308
310
  requestSocketToken: boolean,
309
311
  options: TokenFetchOptionsEx,
312
+ ) {
313
+ return this.joinSessionCore(requestSocketToken, options).catch((e) => {
314
+ const likelyFacetCodes = e as IFacetCodes;
315
+ if (Array.isArray(likelyFacetCodes.facetCodes)) {
316
+ for (const code of likelyFacetCodes.facetCodes) {
317
+ switch (code) {
318
+ case "sessionForbiddenOnPreservedFiles":
319
+ case "sessionForbiddenOnModerationEnabledLibrary":
320
+ case "sessionForbiddenOnRequireCheckout":
321
+ // This document can only be opened in storage-only mode.
322
+ // DeltaManager will recognize this error
323
+ // and load without a delta stream connection.
324
+ this._policies = {...this._policies,storageOnly: true};
325
+ throw new DeltaStreamConnectionForbiddenError(code, { driverVersion });
326
+ default:
327
+ continue;
328
+ }
329
+ }
330
+ }
331
+ throw e;
332
+ });
333
+ }
334
+
335
+ private async joinSessionCore(
336
+ requestSocketToken: boolean,
337
+ options: TokenFetchOptionsEx,
310
338
  ): Promise<ISocketStorageDiscovery> {
311
- const executeFetch = async () =>
312
- fetchJoinSession(
339
+ const disableJoinSessionRefresh = this.mc.config.getBoolean("Fluid.Driver.Odsp.disableJoinSessionRefresh");
340
+ const executeFetch = async () => {
341
+ const joinSessionResponse = await fetchJoinSession(
313
342
  this.odspResolvedUrl,
314
343
  "opStream/joinSession",
315
344
  "POST",
@@ -318,12 +347,65 @@ export class OdspDocumentService implements IDocumentService {
318
347
  this.epochTracker,
319
348
  requestSocketToken,
320
349
  options,
350
+ disableJoinSessionRefresh,
321
351
  this.hostPolicy.sessionOptions?.unauthenticatedUserDisplayName,
322
352
  );
353
+ return {
354
+ entryTime: Date.now(),
355
+ joinSessionResponse,
356
+ };
357
+ };
358
+
359
+ const getResponseAndRefreshAfterDeltaMs = async () => {
360
+ let response = await this.cache.sessionJoinCache.addOrGet(this.joinSessionKey, executeFetch);
361
+ // If the response does not contain refreshSessionDurationSeconds, then treat it as old flow and let the
362
+ // cache entry to be treated as expired after 1 hour.
363
+ response.joinSessionResponse.refreshSessionDurationSeconds =
364
+ response.joinSessionResponse.refreshSessionDurationSeconds ?? 3600;
365
+ return {
366
+ ...response,
367
+ refreshAfterDeltaMs: this.calculateJoinSessionRefreshDelta(
368
+ response.entryTime, response.joinSessionResponse.refreshSessionDurationSeconds),
369
+ }
370
+ };
371
+ let response = await getResponseAndRefreshAfterDeltaMs();
372
+ // This means that the cached entry has expired(This should not be possible if the response is fetched
373
+ // from the network call). In this case we remove the cached entry and fetch the new response.
374
+ if (response.refreshAfterDeltaMs <= 0) {
375
+ this.cache.sessionJoinCache.remove(this.joinSessionKey);
376
+ response = await getResponseAndRefreshAfterDeltaMs();
377
+ }
378
+ if (!disableJoinSessionRefresh) {
379
+ const props = {
380
+ entryTime: response.entryTime,
381
+ refreshSessionDurationSeconds:
382
+ response.joinSessionResponse.refreshSessionDurationSeconds,
383
+ refreshAfterDeltaMs: response.refreshAfterDeltaMs,
384
+ };
385
+ if (response.refreshAfterDeltaMs > 0) {
386
+ this.scheduleJoinSessionRefresh(response.refreshAfterDeltaMs)
387
+ .catch((error) => {
388
+ this.mc.logger.sendErrorEvent({
389
+ eventName: "JoinSessionRefreshError",
390
+ ...props,
391
+ },
392
+ error,
393
+ )
394
+ });;
395
+ } else {
396
+ // Logging just for informational purposes to help with debugging as this is a new feature.
397
+ this.mc.logger.sendErrorEvent({
398
+ eventName: "JoinSessionRefreshNotScheduled",
399
+ ...props,
400
+ });
401
+ }
402
+ }
403
+ return response.joinSessionResponse;
404
+ }
323
405
 
324
- // Note: The sessionCache is configured with a sliding expiry of 1 hour,
325
- // so if we've fetched the join session within the last hour we won't run executeFetch again now.
326
- return this.cache.sessionJoinCache.addOrGet(this.joinSessionKey, executeFetch);
406
+ private calculateJoinSessionRefreshDelta(responseFetchTime: number, refreshSessionDurationSeconds: number) {
407
+ // 30 seconds is buffer time to refresh the session.
408
+ return responseFetchTime + ((refreshSessionDurationSeconds * 1000) - 30000) - Date.now();
327
409
  }
328
410
 
329
411
  /**
@@ -529,15 +529,13 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
529
529
  const versionsResponse = response.content;
530
530
  if (!versionsResponse) {
531
531
  throw new NonRetryableError(
532
- "getVersionsReturnedNoResponse",
533
532
  "No response from /versions endpoint",
534
533
  DriverErrorType.genericNetworkError,
535
534
  { driverVersion });
536
535
  }
537
536
  if (!Array.isArray(versionsResponse.value)) {
538
537
  throw new NonRetryableError(
539
- "getVersionsReturnedNonArrayResponse",
540
- "Incorrect response from /versions endpoint",
538
+ "Incorrect response from /versions endpoint, expected an array",
541
539
  DriverErrorType.genericNetworkError,
542
540
  { driverVersion });
543
541
  }
@@ -714,7 +712,6 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
714
712
  private checkSnapshotUrl() {
715
713
  if (!this.snapshotUrl) {
716
714
  throw new NonRetryableError(
717
- "noSnapshotUrlProvided",
718
715
  "Method failed because no snapshot url was available",
719
716
  DriverErrorType.genericError,
720
717
  { driverVersion });
@@ -724,7 +721,6 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
724
721
  private checkAttachmentPOSTUrl() {
725
722
  if (!this.attachmentPOSTUrl) {
726
723
  throw new NonRetryableError(
727
- "noAttachmentPOSTUrlProvided",
728
724
  "Method failed because no attachment POST url was available",
729
725
  DriverErrorType.genericError,
730
726
  { driverVersion });
@@ -734,7 +730,6 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
734
730
  private checkAttachmentGETUrl() {
735
731
  if (!this.attachmentGETUrl) {
736
732
  throw new NonRetryableError(
737
- "noAttachmentGETUrlWasProvided",
738
733
  "Method failed because no attachment GET url was available",
739
734
  DriverErrorType.genericError,
740
735
  { driverVersion });
@@ -5,7 +5,12 @@
5
5
 
6
6
  import { assert } from "@fluidframework/common-utils";
7
7
  import { IFluidCodeDetails, IRequest, isFluidPackage } from "@fluidframework/core-interfaces";
8
- import { DriverHeader, IResolvedUrl, IUrlResolver } from "@fluidframework/driver-definitions";
8
+ import {
9
+ DriverHeader,
10
+ IContainerPackageInfo,
11
+ IResolvedUrl,
12
+ IUrlResolver,
13
+ } from "@fluidframework/driver-definitions";
9
14
  import { IOdspResolvedUrl, ShareLinkTypes, ShareLinkInfoType } from "@fluidframework/odsp-driver-definitions";
10
15
  import { createOdspUrl } from "./createOdspUrl";
11
16
  import { getApiRoot } from "./odspUrlHelper";
@@ -151,16 +156,24 @@ export class OdspDriverUrlResolver implements IUrlResolver {
151
156
  public async getAbsoluteUrl(
152
157
  resolvedUrl: IResolvedUrl,
153
158
  relativeUrl: string,
154
- codeDetails?: IFluidCodeDetails,
159
+ packageInfoSource?: IContainerPackageInfo | IFluidCodeDetails,
155
160
  ): Promise<string> {
156
161
  let dataStorePath = relativeUrl;
157
162
  if (dataStorePath.startsWith("/")) {
158
163
  dataStorePath = dataStorePath.substr(1);
159
164
  }
160
165
  const odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);
161
- const containerPackageName =
162
- isFluidPackage(codeDetails?.package) ? codeDetails?.package.name : codeDetails?.package ??
163
- odspResolvedUrl.codeHint?.containerPackageName;
166
+
167
+ // back-compat: IFluidCodeDetails usage to be removed in 0.58.0
168
+ let containerPackageName;
169
+ if (packageInfoSource && "name" in packageInfoSource) {
170
+ containerPackageName = packageInfoSource.name
171
+ } else if (isFluidPackage(packageInfoSource?.package)) {
172
+ containerPackageName = packageInfoSource?.package.name
173
+ } else {
174
+ containerPackageName = packageInfoSource?.package
175
+ }
176
+ containerPackageName = containerPackageName ?? odspResolvedUrl.codeHint?.containerPackageName
164
177
 
165
178
  return createOdspUrl({
166
179
  ... odspResolvedUrl,
@@ -5,7 +5,11 @@
5
5
 
6
6
  import { PromiseCache } from "@fluidframework/common-utils";
7
7
  import { IFluidCodeDetails, IRequest, isFluidPackage } from "@fluidframework/core-interfaces";
8
- import { IResolvedUrl, IUrlResolver } from "@fluidframework/driver-definitions";
8
+ import {
9
+ IContainerPackageInfo,
10
+ IResolvedUrl,
11
+ IUrlResolver,
12
+ } from "@fluidframework/driver-definitions";
9
13
  import { ITelemetryBaseLogger, ITelemetryLogger } from "@fluidframework/common-definitions";
10
14
  import { NonRetryableError } from "@fluidframework/driver-utils";
11
15
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
@@ -164,8 +168,7 @@ export class OdspDriverUrlResolverForShareLink implements IUrlResolver {
164
168
  async (event) => tokenFetcher(options).then((tokenResponse) => {
165
169
  if (tokenResponse === null) {
166
170
  throw new NonRetryableError(
167
- "shareLinkTokenIsNull",
168
- "Token callback returned null",
171
+ "Token callback returned null for share link",
169
172
  OdspErrorType.fetchTokenError,
170
173
  { driverVersion });
171
174
  }
@@ -214,17 +217,22 @@ export class OdspDriverUrlResolverForShareLink implements IUrlResolver {
214
217
  public async getAbsoluteUrl(
215
218
  resolvedUrl: IResolvedUrl,
216
219
  dataStorePath: string,
217
- codeDetails?: IFluidCodeDetails,
220
+ packageInfoSource?: IContainerPackageInfo | IFluidCodeDetails,
218
221
  ): Promise<string> {
219
222
  const odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);
220
-
221
223
  const shareLink = await this.getShareLinkPromise(odspResolvedUrl);
222
-
223
224
  const shareLinkUrl = new URL(shareLink);
224
225
 
225
- const containerPackageName =
226
- isFluidPackage(codeDetails?.package) ? codeDetails?.package.name : codeDetails?.package ??
227
- odspResolvedUrl.codeHint?.containerPackageName;
226
+ // back-compat: IFluidCodeDetails usage to be removed in 0.58.0
227
+ let containerPackageName;
228
+ if (packageInfoSource && "name" in packageInfoSource) {
229
+ containerPackageName = packageInfoSource.name
230
+ } else if (isFluidPackage(packageInfoSource?.package)) {
231
+ containerPackageName = packageInfoSource?.package.name
232
+ } else {
233
+ containerPackageName = packageInfoSource?.package
234
+ }
235
+ containerPackageName = containerPackageName ?? odspResolvedUrl.codeHint?.containerPackageName
228
236
 
229
237
  storeLocatorInOdspUrl(shareLinkUrl, {
230
238
  siteUrl: odspResolvedUrl.siteUrl,
package/src/odspError.ts CHANGED
@@ -10,9 +10,8 @@ import { IOdspSocketError } from "./contracts";
10
10
  * Returns network error based on error object from ODSP socket (IOdspSocketError)
11
11
  */
12
12
  export function errorObjectFromSocketError(socketError: IOdspSocketError, handler: string) {
13
- const message = `OdspSocketError (${handler}): ${socketError.message}`;
13
+ const message = `ODSP socket error (${handler}): ${socketError.message}`;
14
14
  const error = createOdspNetworkError(
15
- `odspSocketError [${handler}]`,
16
15
  message,
17
16
  socketError.code,
18
17
  socketError.retryAfter);