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