@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.
- package/dist/contracts.d.ts +4 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/createFile.d.ts.map +1 -1
- package/dist/createFile.js +3 -3
- package/dist/createFile.js.map +1 -1
- package/dist/epochTracker.d.ts.map +1 -1
- package/dist/epochTracker.js +2 -3
- package/dist/epochTracker.js.map +1 -1
- package/dist/fetchSnapshot.d.ts.map +1 -1
- package/dist/fetchSnapshot.js +66 -25
- package/dist/fetchSnapshot.js.map +1 -1
- package/dist/getFileLink.js +2 -2
- package/dist/getFileLink.js.map +1 -1
- package/dist/odspCache.d.ts +8 -3
- package/dist/odspCache.d.ts.map +1 -1
- package/dist/odspCache.js +1 -1
- package/dist/odspCache.js.map +1 -1
- package/dist/odspDocumentDeltaConnection.js +1 -1
- package/dist/odspDocumentDeltaConnection.js.map +1 -1
- package/dist/odspDocumentService.d.ts +5 -0
- package/dist/odspDocumentService.d.ts.map +1 -1
- package/dist/odspDocumentService.js +88 -25
- package/dist/odspDocumentService.js.map +1 -1
- package/dist/odspDocumentStorageManager.d.ts.map +1 -1
- package/dist/odspDocumentStorageManager.js +5 -5
- package/dist/odspDocumentStorageManager.js.map +1 -1
- package/dist/odspDriverUrlResolver.d.ts +2 -2
- package/dist/odspDriverUrlResolver.d.ts.map +1 -1
- package/dist/odspDriverUrlResolver.js +14 -3
- package/dist/odspDriverUrlResolver.js.map +1 -1
- package/dist/odspDriverUrlResolverForShareLink.d.ts +2 -2
- package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
- package/dist/odspDriverUrlResolverForShareLink.js +15 -4
- package/dist/odspDriverUrlResolverForShareLink.js.map +1 -1
- package/dist/odspError.d.ts.map +1 -1
- package/dist/odspError.js +2 -2
- package/dist/odspError.js.map +1 -1
- package/dist/odspUtils.d.ts.map +1 -1
- package/dist/odspUtils.js +10 -10
- package/dist/odspUtils.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/retryErrorsStorageAdapter.js +1 -1
- package/dist/retryErrorsStorageAdapter.js.map +1 -1
- package/dist/vroom.d.ts +2 -1
- package/dist/vroom.d.ts.map +1 -1
- package/dist/vroom.js +6 -2
- package/dist/vroom.js.map +1 -1
- package/dist/zipItDataRepresentationUtils.js +1 -1
- package/dist/zipItDataRepresentationUtils.js.map +1 -1
- package/lib/contracts.d.ts +4 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/contracts.js.map +1 -1
- package/lib/createFile.d.ts.map +1 -1
- package/lib/createFile.js +3 -3
- package/lib/createFile.js.map +1 -1
- package/lib/epochTracker.d.ts.map +1 -1
- package/lib/epochTracker.js +2 -3
- package/lib/epochTracker.js.map +1 -1
- package/lib/fetchSnapshot.d.ts.map +1 -1
- package/lib/fetchSnapshot.js +66 -25
- package/lib/fetchSnapshot.js.map +1 -1
- package/lib/getFileLink.js +2 -2
- package/lib/getFileLink.js.map +1 -1
- package/lib/odspCache.d.ts +8 -3
- package/lib/odspCache.d.ts.map +1 -1
- package/lib/odspCache.js +1 -1
- package/lib/odspCache.js.map +1 -1
- package/lib/odspDocumentDeltaConnection.js +1 -1
- package/lib/odspDocumentDeltaConnection.js.map +1 -1
- package/lib/odspDocumentService.d.ts +5 -0
- package/lib/odspDocumentService.d.ts.map +1 -1
- package/lib/odspDocumentService.js +88 -25
- package/lib/odspDocumentService.js.map +1 -1
- package/lib/odspDocumentStorageManager.d.ts.map +1 -1
- package/lib/odspDocumentStorageManager.js +5 -5
- package/lib/odspDocumentStorageManager.js.map +1 -1
- package/lib/odspDriverUrlResolver.d.ts +2 -2
- package/lib/odspDriverUrlResolver.d.ts.map +1 -1
- package/lib/odspDriverUrlResolver.js +15 -4
- package/lib/odspDriverUrlResolver.js.map +1 -1
- package/lib/odspDriverUrlResolverForShareLink.d.ts +2 -2
- package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
- package/lib/odspDriverUrlResolverForShareLink.js +15 -4
- package/lib/odspDriverUrlResolverForShareLink.js.map +1 -1
- package/lib/odspError.d.ts.map +1 -1
- package/lib/odspError.js +2 -2
- package/lib/odspError.js.map +1 -1
- package/lib/odspUtils.d.ts.map +1 -1
- package/lib/odspUtils.js +10 -10
- package/lib/odspUtils.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/retryErrorsStorageAdapter.js +1 -1
- package/lib/retryErrorsStorageAdapter.js.map +1 -1
- package/lib/vroom.d.ts +2 -1
- package/lib/vroom.d.ts.map +1 -1
- package/lib/vroom.js +6 -2
- package/lib/vroom.js.map +1 -1
- package/lib/zipItDataRepresentationUtils.js +1 -1
- package/lib/zipItDataRepresentationUtils.js.map +1 -1
- package/package.json +12 -12
- package/src/contracts.ts +5 -0
- package/src/createFile.ts +2 -4
- package/src/epochTracker.ts +5 -4
- package/src/fetchSnapshot.ts +66 -33
- package/src/getFileLink.ts +0 -2
- package/src/odspCache.ts +3 -3
- package/src/odspDocumentDeltaConnection.ts +1 -1
- package/src/odspDocumentService.ts +110 -28
- package/src/odspDocumentStorageManager.ts +1 -6
- package/src/odspDriverUrlResolver.ts +18 -5
- package/src/odspDriverUrlResolverForShareLink.ts +17 -9
- package/src/odspError.ts +1 -2
- package/src/odspUtils.ts +10 -13
- package/src/packageVersion.ts +1 -1
- package/src/retryErrorsStorageAdapter.ts +1 -1
- package/src/vroom.ts +6 -0
- package/src/zipItDataRepresentationUtils.ts +1 -2
package/src/epochTracker.ts
CHANGED
|
@@ -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(
|
|
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
|
-
"
|
|
354
|
+
"Epoch mismatch", DriverErrorType.fileOverwrittenInStorage, { driverVersion });
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
357
|
|
package/src/fetchSnapshot.ts
CHANGED
|
@@ -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
|
-
|
|
230
|
-
|
|
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
|
|
233
|
-
let
|
|
234
|
-
let
|
|
235
|
-
let
|
|
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
|
-
|
|
259
|
+
dnsLookupTime = indResTime.domainLookupEnd - indResTime.domainLookupStart;
|
|
250
260
|
tcpHandshakeTime = indResTime.connectEnd - indResTime.connectStart;
|
|
251
|
-
|
|
252
|
-
(indResTime.connectEnd - indResTime.secureConnectionStart) :
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|
package/src/getFileLink.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
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)
|
|
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
|
|
312
|
-
|
|
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
|
-
|
|
325
|
-
//
|
|
326
|
-
return
|
|
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
|
-
"
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 {
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 = `
|
|
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);
|