@fluidframework/odsp-driver 1.2.1 → 2.0.0-internal.1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compactSnapshotParser.d.ts.map +1 -1
- package/dist/compactSnapshotParser.js +4 -1
- package/dist/compactSnapshotParser.js.map +1 -1
- package/dist/compactSnapshotWriter.d.ts.map +1 -1
- package/dist/compactSnapshotWriter.js +6 -3
- package/dist/compactSnapshotWriter.js.map +1 -1
- package/dist/epochTracker.d.ts +1 -0
- package/dist/epochTracker.d.ts.map +1 -1
- package/dist/epochTracker.js +24 -5
- package/dist/epochTracker.js.map +1 -1
- package/dist/fetchSnapshot.d.ts.map +1 -1
- package/dist/fetchSnapshot.js +15 -12
- package/dist/fetchSnapshot.js.map +1 -1
- package/dist/getFileLink.d.ts +3 -6
- package/dist/getFileLink.d.ts.map +1 -1
- package/dist/getFileLink.js +27 -38
- package/dist/getFileLink.js.map +1 -1
- package/dist/odspDeltaStorageService.d.ts +2 -1
- package/dist/odspDeltaStorageService.d.ts.map +1 -1
- package/dist/odspDeltaStorageService.js +5 -4
- package/dist/odspDeltaStorageService.js.map +1 -1
- package/dist/odspDocumentService.d.ts +1 -0
- package/dist/odspDocumentService.d.ts.map +1 -1
- package/dist/odspDocumentService.js +11 -5
- package/dist/odspDocumentService.js.map +1 -1
- package/dist/odspDocumentStorageManager.d.ts +4 -3
- package/dist/odspDocumentStorageManager.d.ts.map +1 -1
- package/dist/odspDocumentStorageManager.js +67 -60
- package/dist/odspDocumentStorageManager.js.map +1 -1
- package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
- package/dist/odspDriverUrlResolverForShareLink.js +4 -4
- package/dist/odspDriverUrlResolverForShareLink.js.map +1 -1
- package/dist/odspLocationRedirection.d.ts +14 -0
- package/dist/odspLocationRedirection.d.ts.map +1 -0
- package/dist/odspLocationRedirection.js +24 -0
- package/dist/odspLocationRedirection.js.map +1 -0
- package/dist/odspSummaryUploadManager.d.ts +2 -1
- package/dist/odspSummaryUploadManager.d.ts.map +1 -1
- package/dist/odspSummaryUploadManager.js +3 -4
- package/dist/odspSummaryUploadManager.js.map +1 -1
- package/dist/odspUrlHelper.js +2 -1
- package/dist/odspUrlHelper.js.map +1 -1
- package/dist/odspUtils.d.ts.map +1 -1
- package/dist/odspUtils.js +10 -2
- 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/retryUtils.d.ts.map +1 -1
- package/dist/retryUtils.js +8 -4
- package/dist/retryUtils.js.map +1 -1
- package/lib/compactSnapshotParser.d.ts.map +1 -1
- package/lib/compactSnapshotParser.js +4 -1
- package/lib/compactSnapshotParser.js.map +1 -1
- package/lib/compactSnapshotWriter.d.ts.map +1 -1
- package/lib/compactSnapshotWriter.js +6 -3
- package/lib/compactSnapshotWriter.js.map +1 -1
- package/lib/epochTracker.d.ts +1 -0
- package/lib/epochTracker.d.ts.map +1 -1
- package/lib/epochTracker.js +26 -7
- package/lib/epochTracker.js.map +1 -1
- package/lib/fetchSnapshot.d.ts.map +1 -1
- package/lib/fetchSnapshot.js +15 -12
- package/lib/fetchSnapshot.js.map +1 -1
- package/lib/getFileLink.d.ts +3 -6
- package/lib/getFileLink.d.ts.map +1 -1
- package/lib/getFileLink.js +29 -40
- package/lib/getFileLink.js.map +1 -1
- package/lib/odspDeltaStorageService.d.ts +2 -1
- package/lib/odspDeltaStorageService.d.ts.map +1 -1
- package/lib/odspDeltaStorageService.js +5 -4
- package/lib/odspDeltaStorageService.js.map +1 -1
- package/lib/odspDocumentService.d.ts +1 -0
- package/lib/odspDocumentService.d.ts.map +1 -1
- package/lib/odspDocumentService.js +13 -7
- package/lib/odspDocumentService.js.map +1 -1
- package/lib/odspDocumentStorageManager.d.ts +4 -3
- package/lib/odspDocumentStorageManager.d.ts.map +1 -1
- package/lib/odspDocumentStorageManager.js +68 -61
- package/lib/odspDocumentStorageManager.js.map +1 -1
- package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
- package/lib/odspDriverUrlResolverForShareLink.js +4 -4
- package/lib/odspDriverUrlResolverForShareLink.js.map +1 -1
- package/lib/odspLocationRedirection.d.ts +14 -0
- package/lib/odspLocationRedirection.d.ts.map +1 -0
- package/lib/odspLocationRedirection.js +20 -0
- package/lib/odspLocationRedirection.js.map +1 -0
- package/lib/odspSummaryUploadManager.d.ts +2 -1
- package/lib/odspSummaryUploadManager.d.ts.map +1 -1
- package/lib/odspSummaryUploadManager.js +3 -4
- package/lib/odspSummaryUploadManager.js.map +1 -1
- package/lib/odspUrlHelper.js +2 -1
- package/lib/odspUrlHelper.js.map +1 -1
- package/lib/odspUtils.d.ts.map +1 -1
- package/lib/odspUtils.js +11 -3
- 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/retryUtils.d.ts.map +1 -1
- package/lib/retryUtils.js +9 -5
- package/lib/retryUtils.js.map +1 -1
- package/package.json +16 -16
- package/src/compactSnapshotParser.ts +3 -1
- package/src/compactSnapshotWriter.ts +6 -3
- package/src/epochTracker.ts +47 -7
- package/src/fetchSnapshot.ts +29 -15
- package/src/getFileLink.ts +33 -46
- package/src/odspDeltaStorageService.ts +4 -2
- package/src/odspDocumentService.ts +16 -12
- package/src/odspDocumentStorageManager.ts +49 -41
- package/src/odspDriverUrlResolverForShareLink.ts +2 -5
- package/src/odspLocationRedirection.ts +23 -0
- package/src/odspSummaryUploadManager.ts +3 -3
- package/src/odspUrlHelper.ts +1 -1
- package/src/odspUtils.ts +15 -4
- package/src/packageVersion.ts +1 -1
- package/src/retryUtils.ts +8 -5
package/src/epochTracker.ts
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
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 {
|
|
9
|
+
import {
|
|
10
|
+
ThrottlingError,
|
|
11
|
+
RateLimiter,
|
|
12
|
+
NonRetryableError,
|
|
13
|
+
LocationRedirectionError,
|
|
14
|
+
} from "@fluidframework/driver-utils";
|
|
10
15
|
import { IConnected } from "@fluidframework/protocol-definitions";
|
|
11
16
|
import {
|
|
12
17
|
snapshotKey,
|
|
@@ -15,9 +20,16 @@ import {
|
|
|
15
20
|
IFileEntry,
|
|
16
21
|
IPersistedCache,
|
|
17
22
|
IOdspError,
|
|
23
|
+
IOdspErrorAugmentations,
|
|
24
|
+
IOdspResolvedUrl,
|
|
18
25
|
} from "@fluidframework/odsp-driver-definitions";
|
|
19
26
|
import { DriverErrorType } from "@fluidframework/driver-definitions";
|
|
20
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
PerformanceEvent,
|
|
29
|
+
isFluidError,
|
|
30
|
+
normalizeError,
|
|
31
|
+
loggerToMonitoringContext,
|
|
32
|
+
} from "@fluidframework/telemetry-utils";
|
|
21
33
|
import { fetchAndParseAsJSONHelper, fetchArray, fetchHelper, getOdspResolvedUrl, IOdspResponse } from "./odspUtils";
|
|
22
34
|
import {
|
|
23
35
|
IOdspCache,
|
|
@@ -27,6 +39,7 @@ import {
|
|
|
27
39
|
import { IVersionedValueWithEpoch, persistedCacheValueVersion } from "./contracts";
|
|
28
40
|
import { ClpCompliantAppHeader } from "./contractsPublic";
|
|
29
41
|
import { pkgVersion as driverVersion } from "./packageVersion";
|
|
42
|
+
import { patchOdspResolvedUrl } from "./odspLocationRedirection";
|
|
30
43
|
|
|
31
44
|
export type FetchType = "blob" | "createBlob" | "createFile" | "joinSession" | "ops" | "test" | "snapshotTree" |
|
|
32
45
|
"treesLatest" | "uploadSummary" | "push" | "versions";
|
|
@@ -47,6 +60,7 @@ export const defaultCacheExpiryTimeoutMs: number = 2 * 24 * 60 * 60 * 1000;
|
|
|
47
60
|
export class EpochTracker implements IPersistedFileCache {
|
|
48
61
|
private _fluidEpoch: string | undefined;
|
|
49
62
|
|
|
63
|
+
private readonly snapshotCacheExpiryTimeoutMs: number;
|
|
50
64
|
public readonly rateLimiter: RateLimiter;
|
|
51
65
|
private readonly driverId = uuid();
|
|
52
66
|
// This tracks the request number made by the driver instance.
|
|
@@ -59,6 +73,12 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
59
73
|
) {
|
|
60
74
|
// Limits the max number of concurrent requests to 24.
|
|
61
75
|
this.rateLimiter = new RateLimiter(24);
|
|
76
|
+
|
|
77
|
+
// We need this for GC testing until we properly plumb through the snapshot expiration policy (see PR #11168)
|
|
78
|
+
this.snapshotCacheExpiryTimeoutMs =
|
|
79
|
+
loggerToMonitoringContext(logger).config.getBoolean("Fluid.Driver.Odsp.TestOverride.DisableSnapshotCache")
|
|
80
|
+
? 0
|
|
81
|
+
: defaultCacheExpiryTimeoutMs;
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
// public for UT purposes only!
|
|
@@ -95,17 +115,17 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
95
115
|
} else if (this._fluidEpoch !== value.fluidEpoch) {
|
|
96
116
|
return undefined;
|
|
97
117
|
}
|
|
98
|
-
// Expire the cached snapshot if it's older than
|
|
118
|
+
// Expire the cached snapshot if it's older than snapshotCacheExpiryTimeoutMs and immediately
|
|
99
119
|
// expire all old caches that do not have cacheEntryTime
|
|
100
120
|
if (entry.type === snapshotKey) {
|
|
101
121
|
const cacheTime = value.value?.cacheEntryTime;
|
|
102
122
|
const currentTime = Date.now();
|
|
103
|
-
if (cacheTime === undefined || currentTime - cacheTime >=
|
|
123
|
+
if (cacheTime === undefined || currentTime - cacheTime >= this.snapshotCacheExpiryTimeoutMs) {
|
|
104
124
|
this.logger.sendTelemetryEvent(
|
|
105
125
|
{
|
|
106
126
|
eventName: "odspVersionsCacheExpired",
|
|
107
127
|
duration: currentTime - cacheTime,
|
|
108
|
-
maxCacheAgeMs:
|
|
128
|
+
maxCacheAgeMs: this.snapshotCacheExpiryTimeoutMs,
|
|
109
129
|
});
|
|
110
130
|
await this.removeEntries();
|
|
111
131
|
return undefined;
|
|
@@ -121,8 +141,8 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
121
141
|
|
|
122
142
|
public async put(entry: IEntry, value: any) {
|
|
123
143
|
assert(this._fluidEpoch !== undefined, 0x1dd /* "no epoch" */);
|
|
124
|
-
// For snapshots, the value should have the cacheEntryTime.
|
|
125
|
-
// than
|
|
144
|
+
// For snapshots, the value should have the cacheEntryTime.
|
|
145
|
+
// This will be used to expire snapshots older than snapshotCacheExpiryTimeoutMs.
|
|
126
146
|
if (entry.type === snapshotKey) {
|
|
127
147
|
value.cacheEntryTime = value.cacheEntryTime ?? Date.now();
|
|
128
148
|
}
|
|
@@ -224,6 +244,26 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
224
244
|
}
|
|
225
245
|
await this.checkForEpochError(error, epochFromResponse, fetchType);
|
|
226
246
|
throw error;
|
|
247
|
+
}).catch((error) => {
|
|
248
|
+
// If the error is about location redirection, then we need to generate new resolved url with correct
|
|
249
|
+
// location info.
|
|
250
|
+
if (isFluidError(error) && error.errorType === DriverErrorType.fileNotFoundOrAccessDeniedError) {
|
|
251
|
+
const redirectLocation = (error as IOdspErrorAugmentations).redirectLocation;
|
|
252
|
+
if (redirectLocation !== undefined) {
|
|
253
|
+
const redirectUrl: IOdspResolvedUrl = patchOdspResolvedUrl(
|
|
254
|
+
this.fileEntry.resolvedUrl,
|
|
255
|
+
redirectLocation,
|
|
256
|
+
);
|
|
257
|
+
const locationRedirectionError = new LocationRedirectionError(
|
|
258
|
+
error.message,
|
|
259
|
+
redirectUrl,
|
|
260
|
+
{ driverVersion, redirectLocation },
|
|
261
|
+
);
|
|
262
|
+
locationRedirectionError.addTelemetryProperties(error.getTelemetryProperties());
|
|
263
|
+
throw locationRedirectionError;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
227
267
|
}).catch((error) => {
|
|
228
268
|
const fluidError = normalizeError(error, { props: { XRequestStatsHeader: clientCorrelationId } });
|
|
229
269
|
throw fluidError;
|
package/src/fetchSnapshot.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
InstrumentedStorageTokenFetcher,
|
|
17
17
|
} from "@fluidframework/odsp-driver-definitions";
|
|
18
18
|
import { ISnapshotTree } from "@fluidframework/protocol-definitions";
|
|
19
|
-
import { isRuntimeMessage, NonRetryableError } from "@fluidframework/driver-utils";
|
|
19
|
+
import { DriverErrorTelemetryProps, isRuntimeMessage, NonRetryableError } from "@fluidframework/driver-utils";
|
|
20
20
|
import { IOdspSnapshot, ISnapshotCachedEntry, IVersionedValueWithEpoch, persistedCacheValueVersion } from "./contracts";
|
|
21
21
|
import { getQueryString } from "./getQueryString";
|
|
22
22
|
import { getUrlAndHeadersWithAuth } from "./getUrlAndHeadersWithAuth";
|
|
@@ -234,19 +234,30 @@ async function fetchLatestSnapshotCore(
|
|
|
234
234
|
snapshotOptions,
|
|
235
235
|
controller,
|
|
236
236
|
);
|
|
237
|
+
|
|
237
238
|
const odspResponse = response.odspResponse;
|
|
238
239
|
const contentType = odspResponse.headers.get("content-type");
|
|
239
|
-
|
|
240
|
+
|
|
241
|
+
const propsToLog: DriverErrorTelemetryProps = {
|
|
240
242
|
...odspResponse.propsToLog,
|
|
241
243
|
contentType,
|
|
242
244
|
accept: response.requestHeaders.accept,
|
|
245
|
+
driverVersion: pkgVersion,
|
|
243
246
|
};
|
|
247
|
+
|
|
248
|
+
// Measure how much time we spend processing payload
|
|
249
|
+
const snapshotParseEvent = PerformanceEvent.start(logger, {
|
|
250
|
+
eventName: "SnapshotParse",
|
|
251
|
+
...propsToLog,
|
|
252
|
+
});
|
|
253
|
+
|
|
244
254
|
let parsedSnapshotContents: IOdspResponse<ISnapshotContents> | undefined;
|
|
255
|
+
|
|
245
256
|
try {
|
|
246
257
|
switch (contentType) {
|
|
247
258
|
case "application/json": {
|
|
248
259
|
const text = await odspResponse.content.text();
|
|
249
|
-
|
|
260
|
+
propsToLog.bodySize = text.length;
|
|
250
261
|
const content: IOdspSnapshot = JSON.parse(text);
|
|
251
262
|
validateBlobsAndTrees(content);
|
|
252
263
|
const snapshotContents: ISnapshotContents =
|
|
@@ -256,7 +267,7 @@ async function fetchLatestSnapshotCore(
|
|
|
256
267
|
}
|
|
257
268
|
case "application/ms-fluid": {
|
|
258
269
|
const content = await odspResponse.content.arrayBuffer();
|
|
259
|
-
|
|
270
|
+
propsToLog.bodySize = content.byteLength;
|
|
260
271
|
const snapshotContents: ISnapshotContents = parseCompactSnapshotResponse(
|
|
261
272
|
new ReadBuffer(new Uint8Array(content)));
|
|
262
273
|
if (snapshotContents.snapshotTree.trees === undefined ||
|
|
@@ -264,7 +275,7 @@ async function fetchLatestSnapshotCore(
|
|
|
264
275
|
throw new NonRetryableError(
|
|
265
276
|
"Returned odsp snapshot is malformed. No trees or blobs!",
|
|
266
277
|
DriverErrorType.incorrectServerResponse,
|
|
267
|
-
|
|
278
|
+
propsToLog,
|
|
268
279
|
);
|
|
269
280
|
}
|
|
270
281
|
parsedSnapshotContents = { ...odspResponse, content: snapshotContents };
|
|
@@ -274,12 +285,12 @@ async function fetchLatestSnapshotCore(
|
|
|
274
285
|
throw new NonRetryableError(
|
|
275
286
|
"Unknown snapshot content type",
|
|
276
287
|
DriverErrorType.incorrectServerResponse,
|
|
277
|
-
|
|
288
|
+
propsToLog,
|
|
278
289
|
);
|
|
279
290
|
}
|
|
280
291
|
} catch (error) {
|
|
281
292
|
if (isFluidError(error)) {
|
|
282
|
-
error.addTelemetryProperties(
|
|
293
|
+
error.addTelemetryProperties(propsToLog);
|
|
283
294
|
throw error;
|
|
284
295
|
}
|
|
285
296
|
const enhancedError = wrapError(
|
|
@@ -287,11 +298,14 @@ async function fetchLatestSnapshotCore(
|
|
|
287
298
|
(errorMessage) => new NonRetryableError(
|
|
288
299
|
`Error parsing snapshot response: ${errorMessage}`,
|
|
289
300
|
DriverErrorType.genericError,
|
|
290
|
-
|
|
301
|
+
propsToLog));
|
|
291
302
|
throw enhancedError;
|
|
292
303
|
}
|
|
304
|
+
|
|
293
305
|
assert(parsedSnapshotContents !== undefined, 0x312 /* snapshot should be parsed */);
|
|
294
306
|
const snapshot = parsedSnapshotContents.content;
|
|
307
|
+
const { trees, numBlobs, encodedBlobsSize } = evalBlobsAndTrees(snapshot);
|
|
308
|
+
|
|
295
309
|
// From: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming
|
|
296
310
|
// fetchStart: immediately before the browser starts to fetch the resource.
|
|
297
311
|
// requestStart: immediately before the browser starts requesting the resource from the server
|
|
@@ -344,9 +358,6 @@ async function fetchLatestSnapshotCore(
|
|
|
344
358
|
}
|
|
345
359
|
}
|
|
346
360
|
|
|
347
|
-
const { numTrees, numBlobs, encodedBlobsSize } =
|
|
348
|
-
evalBlobsAndTrees(parsedSnapshotContents.content);
|
|
349
|
-
|
|
350
361
|
// There are some scenarios in ODSP where we cannot cache, trees/latest will explicitly tell us when we
|
|
351
362
|
// cannot cache using an HTTP response header.
|
|
352
363
|
const canCache =
|
|
@@ -375,8 +386,11 @@ async function fetchLatestSnapshotCore(
|
|
|
375
386
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
376
387
|
putInCache(valueWithEpoch);
|
|
377
388
|
}
|
|
389
|
+
|
|
390
|
+
snapshotParseEvent.end();
|
|
391
|
+
|
|
378
392
|
event.end({
|
|
379
|
-
trees
|
|
393
|
+
trees,
|
|
380
394
|
blobs: snapshot.blobs?.size ?? 0,
|
|
381
395
|
leafNodes: numBlobs,
|
|
382
396
|
encodedBlobsSize,
|
|
@@ -406,7 +420,7 @@ async function fetchLatestSnapshotCore(
|
|
|
406
420
|
// Azure Fluid Relay service is the redeem status (S means success), and FRP is a flag to indicate
|
|
407
421
|
// if the permission has changed.
|
|
408
422
|
sltelemetry: odspResponse.headers.get("x-fluid-sltelemetry"),
|
|
409
|
-
...
|
|
423
|
+
...propsToLog,
|
|
410
424
|
});
|
|
411
425
|
return snapshot;
|
|
412
426
|
},
|
|
@@ -466,13 +480,13 @@ function getFormBodyAndHeaders(
|
|
|
466
480
|
}
|
|
467
481
|
|
|
468
482
|
function evalBlobsAndTrees(snapshot: ISnapshotContents) {
|
|
469
|
-
const
|
|
483
|
+
const trees = countTreesInSnapshotTree(snapshot.snapshotTree);
|
|
470
484
|
const numBlobs = snapshot.blobs.size;
|
|
471
485
|
let encodedBlobsSize = 0;
|
|
472
486
|
for (const [_, blobContent] of snapshot.blobs) {
|
|
473
487
|
encodedBlobsSize += blobContent.byteLength;
|
|
474
488
|
}
|
|
475
|
-
return {
|
|
489
|
+
return { trees, numBlobs, encodedBlobsSize };
|
|
476
490
|
}
|
|
477
491
|
|
|
478
492
|
export function validateBlobsAndTrees(snapshot: IOdspSnapshot) {
|
package/src/getFileLink.ts
CHANGED
|
@@ -4,19 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
|
-
import { assert
|
|
8
|
-
import { canRetryOnError,
|
|
7
|
+
import { assert } from "@fluidframework/common-utils";
|
|
8
|
+
import { canRetryOnError, NonRetryableError } from "@fluidframework/driver-utils";
|
|
9
9
|
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
10
10
|
import { DriverErrorType } from "@fluidframework/driver-definitions";
|
|
11
|
-
import {
|
|
12
|
-
IOdspUrlParts,
|
|
13
|
-
OdspResourceTokenFetchOptions,
|
|
14
|
-
IdentityType,
|
|
15
|
-
TokenFetcher,
|
|
16
|
-
} from "@fluidframework/odsp-driver-definitions";
|
|
11
|
+
import { IOdspUrlParts, OdspResourceTokenFetchOptions, TokenFetcher } from "@fluidframework/odsp-driver-definitions";
|
|
17
12
|
import { getUrlAndHeadersWithAuth } from "./getUrlAndHeadersWithAuth";
|
|
18
13
|
import { fetchHelper, getWithRetryForTokenRefresh, toInstrumentedOdspTokenFetcher } from "./odspUtils";
|
|
19
14
|
import { pkgVersion as driverVersion } from "./packageVersion";
|
|
15
|
+
import { runWithRetry } from "./retryUtils";
|
|
20
16
|
|
|
21
17
|
// Store cached responses for the lifetime of web session as file link remains the same for given file item
|
|
22
18
|
const fileLinkCache = new Map<string, Promise<string>>();
|
|
@@ -28,17 +24,13 @@ const fileLinkCache = new Map<string, Promise<string>>();
|
|
|
28
24
|
* throttling error. In future, we are thinking of app allowing to pass some cancel token, with which
|
|
29
25
|
* we would be able to stop retrying.
|
|
30
26
|
* @param getToken - used to fetch access tokens needed to execute operation
|
|
31
|
-
* @param
|
|
32
|
-
* @param driveId - drive where file is stored
|
|
33
|
-
* @param itemId - file id
|
|
34
|
-
* @param identityType - type of client account
|
|
27
|
+
* @param odspUrlParts - object describing file storage identity
|
|
35
28
|
* @param logger - used to log results of operation, including any error
|
|
36
29
|
* @returns Promise which resolves to file link url when successful; otherwise, undefined.
|
|
37
30
|
*/
|
|
38
31
|
export async function getFileLink(
|
|
39
32
|
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
|
|
40
33
|
odspUrlParts: IOdspUrlParts,
|
|
41
|
-
identityType: IdentityType,
|
|
42
34
|
logger: ITelemetryLogger,
|
|
43
35
|
): Promise<string> {
|
|
44
36
|
const cacheKey = `${odspUrlParts.siteUrl}_${odspUrlParts.driveId}_${odspUrlParts.itemId}`;
|
|
@@ -47,33 +39,29 @@ export async function getFileLink(
|
|
|
47
39
|
return maybeFileLinkCacheEntry;
|
|
48
40
|
}
|
|
49
41
|
|
|
50
|
-
const
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
// If the error is throttling error, then wait for the specified time before retrying.
|
|
66
|
-
// If the waitTime is not specified, then we start with retrying immediately to max of 8s.
|
|
67
|
-
retryAfterMs = getRetryDelayFromError(err) ?? Math.min(retryAfterMs * 2, 8000);
|
|
68
|
-
await delay(retryAfterMs);
|
|
42
|
+
const fileLinkGenerator = async function() {
|
|
43
|
+
let fileLinkCore: string;
|
|
44
|
+
try {
|
|
45
|
+
fileLinkCore = await runWithRetry(
|
|
46
|
+
async () => getFileLinkCore(getToken, odspUrlParts, logger),
|
|
47
|
+
"getFileLinkCore",
|
|
48
|
+
logger,
|
|
49
|
+
);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// runWithRetry throws a non retriable error after it hits the max # of attempts
|
|
52
|
+
// or encounters an unexpected error type
|
|
53
|
+
if (!canRetryOnError(err)) {
|
|
54
|
+
// Delete from the cache to permit retrying later.
|
|
55
|
+
fileLinkCache.delete(cacheKey);
|
|
69
56
|
}
|
|
70
|
-
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
71
59
|
|
|
72
60
|
// We are guaranteed to run the getFileLinkCore at least once with successful result (which must be a string)
|
|
73
|
-
assert(
|
|
74
|
-
return
|
|
61
|
+
assert(fileLinkCore !== undefined, 0x292 /* "Unexpected undefined result from getFileLinkCore" */);
|
|
62
|
+
return fileLinkCore;
|
|
75
63
|
};
|
|
76
|
-
const fileLink =
|
|
64
|
+
const fileLink = fileLinkGenerator();
|
|
77
65
|
fileLinkCache.set(cacheKey, fileLink);
|
|
78
66
|
return fileLink;
|
|
79
67
|
}
|
|
@@ -81,15 +69,14 @@ export async function getFileLink(
|
|
|
81
69
|
async function getFileLinkCore(
|
|
82
70
|
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
|
|
83
71
|
odspUrlParts: IOdspUrlParts,
|
|
84
|
-
identityType: IdentityType,
|
|
85
72
|
logger: ITelemetryLogger,
|
|
86
73
|
): Promise<string> {
|
|
87
|
-
const fileItem = await getFileItemLite(getToken, odspUrlParts, logger,
|
|
74
|
+
const fileItem = await getFileItemLite(getToken, odspUrlParts, logger, true);
|
|
88
75
|
|
|
89
76
|
// ODSP link requires extra call to return link that is resistant to file being renamed or moved to different folder
|
|
90
77
|
return PerformanceEvent.timedExecAsync(
|
|
91
78
|
logger,
|
|
92
|
-
{ eventName: "odspFileLink", requestName: "
|
|
79
|
+
{ eventName: "odspFileLink", requestName: "getSharingInformation" },
|
|
93
80
|
async (event) => {
|
|
94
81
|
let attempts = 0;
|
|
95
82
|
let additionalProps;
|
|
@@ -106,11 +93,11 @@ async function getFileLinkCore(
|
|
|
106
93
|
0x2bb /* "Instrumented token fetcher with throwOnNullToken = true should never return null" */);
|
|
107
94
|
|
|
108
95
|
const { url, headers } = getUrlAndHeadersWithAuth(
|
|
109
|
-
`${odspUrlParts.siteUrl}/_api/web/
|
|
110
|
-
encodeURIComponent(`'${
|
|
96
|
+
`${odspUrlParts.siteUrl}/_api/web/GetFileByUrl(@a1)/ListItemAllFields/GetSharingInformation?@a1=${
|
|
97
|
+
encodeURIComponent(`'${fileItem.webDavUrl}'`)
|
|
111
98
|
}`,
|
|
112
99
|
storageToken,
|
|
113
|
-
|
|
100
|
+
true,
|
|
114
101
|
);
|
|
115
102
|
const requestInit = {
|
|
116
103
|
method: "POST",
|
|
@@ -124,15 +111,15 @@ async function getFileLinkCore(
|
|
|
124
111
|
additionalProps = response.propsToLog;
|
|
125
112
|
|
|
126
113
|
const sharingInfo = await response.content.json();
|
|
127
|
-
const
|
|
128
|
-
if (typeof
|
|
114
|
+
const directUrl = sharingInfo?.d?.directUrl;
|
|
115
|
+
if (typeof directUrl !== "string") {
|
|
129
116
|
// This will retry once in getWithRetryForTokenRefresh
|
|
130
117
|
throw new NonRetryableError(
|
|
131
|
-
"Malformed
|
|
118
|
+
"Malformed GetSharingInformation response",
|
|
132
119
|
DriverErrorType.incorrectServerResponse,
|
|
133
120
|
{ driverVersion });
|
|
134
121
|
}
|
|
135
|
-
return
|
|
122
|
+
return directUrl;
|
|
136
123
|
});
|
|
137
124
|
event.end({ ...additionalProps, attempts });
|
|
138
125
|
return fileLink;
|
|
@@ -35,13 +35,14 @@ export class OdspDeltaStorageService {
|
|
|
35
35
|
* @param from - inclusive
|
|
36
36
|
* @param to - exclusive
|
|
37
37
|
* @param telemetryProps - properties to add when issuing telemetry events
|
|
38
|
+
* @param scenarioName - reason for fetching ops
|
|
38
39
|
* @returns ops retrieved & info if result was partial (i.e. more is available)
|
|
39
40
|
*/
|
|
40
41
|
public async get(
|
|
41
42
|
from: number,
|
|
42
43
|
to: number,
|
|
43
44
|
telemetryProps: ITelemetryProperties,
|
|
44
|
-
|
|
45
|
+
scenarioName?: string,
|
|
45
46
|
): Promise<IDeltasFetchResult> {
|
|
46
47
|
return getWithRetryForTokenRefresh(async (options) => {
|
|
47
48
|
// Note - this call ends up in getSocketStorageDiscovery() and can refresh token
|
|
@@ -78,7 +79,7 @@ export class OdspDeltaStorageService {
|
|
|
78
79
|
},
|
|
79
80
|
"ops",
|
|
80
81
|
true,
|
|
81
|
-
|
|
82
|
+
scenarioName,
|
|
82
83
|
);
|
|
83
84
|
clearTimeout(timer);
|
|
84
85
|
const deltaStorageResponse = response.content;
|
|
@@ -99,6 +100,7 @@ export class OdspDeltaStorageService {
|
|
|
99
100
|
from,
|
|
100
101
|
to,
|
|
101
102
|
...telemetryProps,
|
|
103
|
+
reason: scenarioName,
|
|
102
104
|
});
|
|
103
105
|
|
|
104
106
|
// It is assumed that server always returns all the ops that it has in the range that was requested.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
|
-
import { performance } from "@fluidframework/common-utils";
|
|
7
|
+
import { assert, performance } from "@fluidframework/common-utils";
|
|
8
8
|
import {
|
|
9
9
|
ChildLogger,
|
|
10
10
|
IFluidErrorBase,
|
|
@@ -21,12 +21,7 @@ import {
|
|
|
21
21
|
IDocumentServicePolicies,
|
|
22
22
|
DriverErrorType,
|
|
23
23
|
} from "@fluidframework/driver-definitions";
|
|
24
|
-
import {
|
|
25
|
-
canRetryOnError,
|
|
26
|
-
DeltaStreamConnectionForbiddenError,
|
|
27
|
-
NonRetryableError,
|
|
28
|
-
} from "@fluidframework/driver-utils";
|
|
29
|
-
import { IFacetCodes } from "@fluidframework/odsp-doclib-utils";
|
|
24
|
+
import { canRetryOnError, DeltaStreamConnectionForbiddenError, NonRetryableError } from "@fluidframework/driver-utils";
|
|
30
25
|
import {
|
|
31
26
|
IClient,
|
|
32
27
|
ISequencedDocumentMessage,
|
|
@@ -39,6 +34,7 @@ import {
|
|
|
39
34
|
InstrumentedStorageTokenFetcher,
|
|
40
35
|
OdspErrorType,
|
|
41
36
|
} from "@fluidframework/odsp-driver-definitions";
|
|
37
|
+
import { hasFacetCodes } from "@fluidframework/odsp-doclib-utils";
|
|
42
38
|
import type { io as SocketIOClientStatic } from "socket.io-client";
|
|
43
39
|
import { HostStoragePolicyInternal, ISocketStorageDiscovery } from "./contracts";
|
|
44
40
|
import { IOdspCache } from "./odspCache";
|
|
@@ -113,6 +109,8 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
113
109
|
|
|
114
110
|
private currentConnection?: OdspDocumentDeltaConnection;
|
|
115
111
|
|
|
112
|
+
private relayServiceTenantAndSessionId: string | undefined;
|
|
113
|
+
|
|
116
114
|
/**
|
|
117
115
|
* @param odspResolvedUrl - resolved url identifying document that will be managed by this service instance.
|
|
118
116
|
* @param getStorageToken - function that can provide the storage token. This is is also referred to as
|
|
@@ -189,6 +187,11 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
189
187
|
}
|
|
190
188
|
throw new Error("Disconnected while uploading summary (attempt to perform flush())");
|
|
191
189
|
},
|
|
190
|
+
() => {
|
|
191
|
+
assert(this.relayServiceTenantAndSessionId !== undefined,
|
|
192
|
+
0x37b /* relayServiceTenantAndSessionId should be present */);
|
|
193
|
+
return this.relayServiceTenantAndSessionId;
|
|
194
|
+
},
|
|
192
195
|
this.mc.config.getNumber("Fluid.Driver.Odsp.snapshotFormatFetchType"),
|
|
193
196
|
);
|
|
194
197
|
}
|
|
@@ -271,7 +274,7 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
271
274
|
this.socketIoClientFactory().catch(annotateAndRethrowConnectionError("socketIoClientFactory")),
|
|
272
275
|
]);
|
|
273
276
|
|
|
274
|
-
const finalWebsocketToken = websocketToken ?? (websocketEndpoint.socketToken
|
|
277
|
+
const finalWebsocketToken = websocketToken ?? (websocketEndpoint.socketToken ?? null);
|
|
275
278
|
if (finalWebsocketToken === null) {
|
|
276
279
|
throw this.annotateConnectionError(
|
|
277
280
|
new NonRetryableError(
|
|
@@ -344,10 +347,9 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
344
347
|
requestSocketToken: boolean,
|
|
345
348
|
options: TokenFetchOptionsEx,
|
|
346
349
|
) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
for (const code of likelyFacetCodes.facetCodes) {
|
|
350
|
+
const response = await this.joinSessionCore(requestSocketToken, options).catch((e) => {
|
|
351
|
+
if (hasFacetCodes(e) && e.facetCodes !== undefined) {
|
|
352
|
+
for (const code of e.facetCodes) {
|
|
351
353
|
switch (code) {
|
|
352
354
|
case "sessionForbiddenOnPreservedFiles":
|
|
353
355
|
case "sessionForbiddenOnModerationEnabledLibrary":
|
|
@@ -364,6 +366,8 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
364
366
|
}
|
|
365
367
|
throw e;
|
|
366
368
|
});
|
|
369
|
+
this.relayServiceTenantAndSessionId = `${response.tenantId}/${response.id}`;
|
|
370
|
+
return response;
|
|
367
371
|
}
|
|
368
372
|
|
|
369
373
|
private async joinSessionCore(
|