@fluidframework/odsp-driver 0.52.1 → 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/createFile.d.ts.map +1 -1
- package/dist/createFile.js +5 -4
- package/dist/createFile.js.map +1 -1
- package/dist/epochTracker.d.ts +2 -1
- package/dist/epochTracker.d.ts.map +1 -1
- package/dist/epochTracker.js +50 -21
- package/dist/epochTracker.js.map +1 -1
- package/dist/fetchSnapshot.d.ts.map +1 -1
- package/dist/fetchSnapshot.js +15 -14
- package/dist/fetchSnapshot.js.map +1 -1
- package/dist/getFileLink.js +3 -3
- 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 +29 -26
- package/dist/odspDocumentStorageManager.js.map +1 -1
- package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
- package/dist/odspDriverUrlResolverForShareLink.js +4 -3
- 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/odspUtils.d.ts.map +1 -1
- package/dist/odspUtils.js +31 -22
- 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 -3
- package/dist/zipItDataRepresentationUtils.js.map +1 -1
- package/lib/createFile.d.ts.map +1 -1
- package/lib/createFile.js +6 -5
- package/lib/createFile.js.map +1 -1
- package/lib/epochTracker.d.ts +2 -1
- package/lib/epochTracker.d.ts.map +1 -1
- package/lib/epochTracker.js +50 -21
- package/lib/epochTracker.js.map +1 -1
- package/lib/fetchSnapshot.d.ts.map +1 -1
- package/lib/fetchSnapshot.js +15 -14
- package/lib/fetchSnapshot.js.map +1 -1
- package/lib/getFileLink.js +4 -4
- 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 +31 -28
- package/lib/odspDocumentStorageManager.js.map +1 -1
- package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
- package/lib/odspDriverUrlResolverForShareLink.js +5 -4
- 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/odspUtils.d.ts.map +1 -1
- package/lib/odspUtils.js +33 -24
- 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 -3
- package/lib/zipItDataRepresentationUtils.js.map +1 -1
- package/package.json +8 -8
- package/src/createFile.ts +13 -9
- package/src/epochTracker.ts +50 -20
- package/src/fetchSnapshot.ts +15 -8
- package/src/getFileLink.ts +10 -4
- package/src/odspDeltaStorageService.ts +10 -3
- package/src/odspDocumentDeltaConnection.ts +2 -0
- package/src/odspDocumentService.ts +8 -4
- package/src/odspDocumentStorageManager.ts +48 -28
- package/src/odspDriverUrlResolverForShareLink.ts +8 -3
- package/src/odspError.ts +5 -1
- package/src/odspUtils.ts +43 -34
- package/src/packageVersion.ts +1 -1
- package/src/zipItDataRepresentationUtils.ts +4 -7
package/src/createFile.ts
CHANGED
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { assert, Uint8ArrayToString } from "@fluidframework/common-utils";
|
|
7
|
-
import { getDocAttributesFromProtocolSummary } from "@fluidframework/driver-utils";
|
|
8
|
-
import {
|
|
9
|
-
fetchIncorrectResponse,
|
|
10
|
-
invalidFileNameStatusCode,
|
|
11
|
-
throwOdspNetworkError,
|
|
12
|
-
} from "@fluidframework/odsp-doclib-utils";
|
|
7
|
+
import { getDocAttributesFromProtocolSummary, NonRetryableError } from "@fluidframework/driver-utils";
|
|
13
8
|
import { getGitType } from "@fluidframework/protocol-base";
|
|
14
9
|
import { SummaryType, ISummaryTree, ISummaryBlob } from "@fluidframework/protocol-definitions";
|
|
15
10
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
@@ -18,7 +13,9 @@ import {
|
|
|
18
13
|
IFileEntry,
|
|
19
14
|
InstrumentedStorageTokenFetcher,
|
|
20
15
|
IOdspResolvedUrl,
|
|
16
|
+
OdspErrorType,
|
|
21
17
|
} from "@fluidframework/odsp-driver-definitions";
|
|
18
|
+
import { DriverErrorType } from "@fluidframework/driver-definitions";
|
|
22
19
|
import {
|
|
23
20
|
IOdspSummaryTree,
|
|
24
21
|
OdspSummaryTreeValue,
|
|
@@ -61,7 +58,8 @@ export async function createNewFluidFile(
|
|
|
61
58
|
): Promise<IOdspResolvedUrl> {
|
|
62
59
|
// Check for valid filename before the request to create file is actually made.
|
|
63
60
|
if (isInvalidFileName(newFileInfo.filename)) {
|
|
64
|
-
|
|
61
|
+
throw new NonRetryableError(
|
|
62
|
+
"createNewInvalidFilename", "Invalid filename", OdspErrorType.invalidFileNameError);
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
let itemId: string;
|
|
@@ -144,7 +142,10 @@ export async function createNewEmptyFluidFile(
|
|
|
144
142
|
|
|
145
143
|
const content = fetchResponse.content;
|
|
146
144
|
if (!content || !content.id) {
|
|
147
|
-
|
|
145
|
+
throw new NonRetryableError(
|
|
146
|
+
"createEmptyFileNoItemId",
|
|
147
|
+
"ODSP CreateFile call returned no item ID",
|
|
148
|
+
DriverErrorType.incorrectServerResponse);
|
|
148
149
|
}
|
|
149
150
|
event.end({
|
|
150
151
|
headers: Object.keys(headers).length !== 0 ? true : undefined,
|
|
@@ -199,7 +200,10 @@ export async function createNewFluidFileFromSummary(
|
|
|
199
200
|
|
|
200
201
|
const content = fetchResponse.content;
|
|
201
202
|
if (!content || !content.itemId) {
|
|
202
|
-
|
|
203
|
+
throw new NonRetryableError(
|
|
204
|
+
"createFileNoItemId",
|
|
205
|
+
"ODSP CreateFile call returned no item ID",
|
|
206
|
+
DriverErrorType.incorrectServerResponse);
|
|
203
207
|
}
|
|
204
208
|
event.end({
|
|
205
209
|
headers: Object.keys(headers).length !== 0 ? true : undefined,
|
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,
|
|
@@ -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,7 +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: {
|
|
196
|
+
const fluidError = normalizeError(error, { props: { XRequestStatsHeader: clientCorrelationId }});
|
|
170
197
|
throw fluidError;
|
|
171
198
|
});
|
|
172
199
|
}
|
|
@@ -184,9 +211,9 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
184
211
|
fetchType: FetchType,
|
|
185
212
|
addInBody: boolean = false,
|
|
186
213
|
) {
|
|
187
|
-
const
|
|
214
|
+
const clientCorrelationId = this.formatClientCorrelationId();
|
|
188
215
|
// Add epoch in fetch request.
|
|
189
|
-
this.addEpochInRequest(fetchOptions, addInBody,
|
|
216
|
+
this.addEpochInRequest(fetchOptions, addInBody, clientCorrelationId);
|
|
190
217
|
let epochFromResponse: string | undefined;
|
|
191
218
|
return this.rateLimiter.schedule(
|
|
192
219
|
async () => fetchArray(url, fetchOptions),
|
|
@@ -195,7 +222,7 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
195
222
|
this.validateEpochFromResponse(epochFromResponse, fetchType);
|
|
196
223
|
response.commonSpoHeaders = {
|
|
197
224
|
...response.commonSpoHeaders,
|
|
198
|
-
"X-RequestStats":
|
|
225
|
+
"X-RequestStats": clientCorrelationId,
|
|
199
226
|
};
|
|
200
227
|
return response;
|
|
201
228
|
}).catch(async (error) => {
|
|
@@ -207,7 +234,7 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
207
234
|
await this.checkForEpochError(error, epochFromResponse, fetchType);
|
|
208
235
|
throw error;
|
|
209
236
|
}).catch((error) => {
|
|
210
|
-
const fluidError = normalizeError(error, {props: {
|
|
237
|
+
const fluidError = normalizeError(error, { props: { XRequestStatsHeader: clientCorrelationId }});
|
|
211
238
|
throw fluidError;
|
|
212
239
|
});
|
|
213
240
|
}
|
|
@@ -215,11 +242,11 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
215
242
|
private addEpochInRequest(
|
|
216
243
|
fetchOptions: RequestInit,
|
|
217
244
|
addInBody: boolean,
|
|
218
|
-
|
|
245
|
+
clientCorrelationId: string,
|
|
219
246
|
) {
|
|
220
247
|
if (addInBody) {
|
|
221
248
|
const headers: {[key: string]: string} = {};
|
|
222
|
-
headers["X-RequestStats"] =
|
|
249
|
+
headers["X-RequestStats"] = clientCorrelationId;
|
|
223
250
|
if (this.fluidEpoch !== undefined) {
|
|
224
251
|
headers["x-fluid-epoch"] = this.fluidEpoch;
|
|
225
252
|
}
|
|
@@ -232,7 +259,7 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
232
259
|
assert(fetchOptions.headers !== undefined, 0x282 /* "Headers should be present now" */);
|
|
233
260
|
fetchOptions.headers[key] = val;
|
|
234
261
|
};
|
|
235
|
-
addHeader("X-RequestStats",
|
|
262
|
+
addHeader("X-RequestStats", clientCorrelationId);
|
|
236
263
|
if (this.fluidEpoch !== undefined) {
|
|
237
264
|
addHeader("x-fluid-epoch", this.fluidEpoch);
|
|
238
265
|
}
|
|
@@ -257,7 +284,7 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
257
284
|
fetchOptions.body = formParams.join("\r\n");
|
|
258
285
|
}
|
|
259
286
|
|
|
260
|
-
private
|
|
287
|
+
private formatClientCorrelationId() {
|
|
261
288
|
return `driverId=${this.driverId}, RequestNumber=${this.networkCallNumber++}`;
|
|
262
289
|
}
|
|
263
290
|
|
|
@@ -266,7 +293,10 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
266
293
|
fetchType: FetchTypeInternal,
|
|
267
294
|
fromCache: boolean = false,
|
|
268
295
|
) {
|
|
269
|
-
this.checkForEpochErrorCore(epochFromResponse);
|
|
296
|
+
const error = this.checkForEpochErrorCore(epochFromResponse);
|
|
297
|
+
if (error !== undefined) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
270
300
|
if (epochFromResponse !== undefined) {
|
|
271
301
|
if (this._fluidEpoch === undefined) {
|
|
272
302
|
this.setEpoch(epochFromResponse, fromCache, fetchType);
|
|
@@ -281,10 +311,8 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
281
311
|
fromCache: boolean = false,
|
|
282
312
|
) {
|
|
283
313
|
if (isFluidError(error) && error.errorType === DriverErrorType.fileOverwrittenInStorage) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.checkForEpochErrorCore(epochFromResponse);
|
|
287
|
-
} catch (epochError) {
|
|
314
|
+
const epochError = this.checkForEpochErrorCore(epochFromResponse);
|
|
315
|
+
if (epochError !== undefined) {
|
|
288
316
|
assert(isFluidError(epochError),
|
|
289
317
|
0x21f /* "epochError expected to be thrown by throwOdspNetworkError and of known type" */);
|
|
290
318
|
epochError.addTelemetryProperties({
|
|
@@ -309,7 +337,9 @@ export class EpochTracker implements IPersistedFileCache {
|
|
|
309
337
|
// initializes this value. Sometimes response does not contain epoch as it is still in
|
|
310
338
|
// implementation phase at server side. In that case also, don't compare it with our epoch value.
|
|
311
339
|
if (this.fluidEpoch && epochFromResponse && (this.fluidEpoch !== epochFromResponse)) {
|
|
312
|
-
|
|
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);
|
|
313
343
|
}
|
|
314
344
|
}
|
|
315
345
|
|
package/src/fetchSnapshot.ts
CHANGED
|
@@ -104,7 +104,14 @@ export async function fetchSnapshotWithRedeem(
|
|
|
104
104
|
}, error);
|
|
105
105
|
await redeemSharingLink(odspResolvedUrl, storageTokenFetcher, logger);
|
|
106
106
|
const odspResolvedUrlWithoutShareLink: IOdspResolvedUrl =
|
|
107
|
-
|
|
107
|
+
{ ...odspResolvedUrl, sharingLinkToRedeem: undefined };
|
|
108
|
+
|
|
109
|
+
if(odspResolvedUrlWithoutShareLink.shareLinkInfo) {
|
|
110
|
+
odspResolvedUrlWithoutShareLink.shareLinkInfo = {
|
|
111
|
+
...odspResolvedUrlWithoutShareLink.shareLinkInfo,
|
|
112
|
+
sharingLinkToRedeem: undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
108
115
|
|
|
109
116
|
return fetchLatestSnapshotCore(
|
|
110
117
|
odspResolvedUrlWithoutShareLink,
|
|
@@ -140,10 +147,10 @@ async function redeemSharingLink(
|
|
|
140
147
|
eventName: "RedeemShareLink",
|
|
141
148
|
},
|
|
142
149
|
async () => getWithRetryForTokenRefresh(async (tokenFetchOptions) => {
|
|
143
|
-
assert(!!odspResolvedUrl.
|
|
150
|
+
assert(!!odspResolvedUrl.sharingLinkToRedeem,
|
|
144
151
|
0x1ed /* "Share link should be present" */);
|
|
145
152
|
const storageToken = await storageTokenFetcher(tokenFetchOptions, "RedeemShareLink");
|
|
146
|
-
const encodedShareUrl = getEncodedShareUrl(odspResolvedUrl.
|
|
153
|
+
const encodedShareUrl = getEncodedShareUrl(odspResolvedUrl.sharingLinkToRedeem);
|
|
147
154
|
const redeemUrl = `${odspResolvedUrl.siteUrl}/_api/v2.0/shares/${encodedShareUrl}`;
|
|
148
155
|
const { url, headers } = getUrlAndHeadersWithAuth(redeemUrl, storageToken);
|
|
149
156
|
headers.prefer = "redeemSharingLink";
|
|
@@ -349,8 +356,8 @@ async function fetchSnapshotContentsCoreV1(
|
|
|
349
356
|
}
|
|
350
357
|
});
|
|
351
358
|
}
|
|
352
|
-
if (odspResolvedUrl.
|
|
353
|
-
formParams.push(`sl: ${odspResolvedUrl.
|
|
359
|
+
if (odspResolvedUrl.sharingLinkToRedeem) {
|
|
360
|
+
formParams.push(`sl: ${odspResolvedUrl.sharingLinkToRedeem}`);
|
|
354
361
|
}
|
|
355
362
|
formParams.push(`_post: 1`);
|
|
356
363
|
formParams.push(`\r\n--${formBoundary}--`);
|
|
@@ -396,9 +403,9 @@ async function fetchSnapshotContentsCoreV2(
|
|
|
396
403
|
const fullUrl = `${odspResolvedUrl.siteUrl}/_api/v2.1/drives/${odspResolvedUrl.driveId}/items/${
|
|
397
404
|
odspResolvedUrl.itemId}/opStream/attachments/latest/content`;
|
|
398
405
|
const queryParams = { ...snapshotOptions };
|
|
399
|
-
if (odspResolvedUrl.
|
|
406
|
+
if (odspResolvedUrl.sharingLinkToRedeem) {
|
|
400
407
|
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
401
|
-
queryParams["sl"] = odspResolvedUrl.
|
|
408
|
+
queryParams["sl"] = odspResolvedUrl.sharingLinkToRedeem;
|
|
402
409
|
}
|
|
403
410
|
const queryString = getQueryString(queryParams);
|
|
404
411
|
const { url, headers } = getUrlAndHeadersWithAuth(`${fullUrl}${queryString}`, storageToken);
|
|
@@ -460,7 +467,7 @@ export async function downloadSnapshot(
|
|
|
460
467
|
}
|
|
461
468
|
|
|
462
469
|
function isRedeemSharingLinkError(odspResolvedUrl: IOdspResolvedUrl, error: any) {
|
|
463
|
-
if (odspResolvedUrl.
|
|
470
|
+
if (odspResolvedUrl.sharingLinkToRedeem !== undefined
|
|
464
471
|
&& (typeof error === "object" && error !== null)
|
|
465
472
|
&& (error.errorType === DriverErrorType.authorizationError
|
|
466
473
|
|| error.errorType === DriverErrorType.fileNotFoundOrAccessDeniedError)) {
|
package/src/getFileLink.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, delay } from "@fluidframework/common-utils";
|
|
8
|
-
import { canRetryOnError, getRetryDelayFromError } from "@fluidframework/driver-utils";
|
|
8
|
+
import { canRetryOnError, getRetryDelayFromError, NonRetryableError } from "@fluidframework/driver-utils";
|
|
9
9
|
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
10
|
-
import {
|
|
10
|
+
import { DriverErrorType } from "@fluidframework/driver-definitions";
|
|
11
11
|
import {
|
|
12
12
|
IOdspUrlParts,
|
|
13
13
|
OdspResourceTokenFetchOptions,
|
|
@@ -120,7 +120,10 @@ async function getFileLinkCore(
|
|
|
120
120
|
const directUrl = sharingInfo?.d?.directUrl;
|
|
121
121
|
if (typeof directUrl !== "string") {
|
|
122
122
|
// This will retry once in getWithRetryForTokenRefresh
|
|
123
|
-
|
|
123
|
+
throw new NonRetryableError(
|
|
124
|
+
"getFileLinkCoreMalformedResponse",
|
|
125
|
+
"Malformed GetSharingInformation response",
|
|
126
|
+
DriverErrorType.incorrectServerResponse);
|
|
124
127
|
}
|
|
125
128
|
return directUrl;
|
|
126
129
|
});
|
|
@@ -171,7 +174,10 @@ async function getFileItemLite(
|
|
|
171
174
|
const responseJson = await response.content.json();
|
|
172
175
|
if (!isFileItemLite(responseJson)) {
|
|
173
176
|
// This will retry once in getWithRetryForTokenRefresh
|
|
174
|
-
|
|
177
|
+
throw new NonRetryableError(
|
|
178
|
+
"getFileItemLiteMalformedResponse",
|
|
179
|
+
"Malformed getFileItemLite response",
|
|
180
|
+
DriverErrorType.incorrectServerResponse);
|
|
175
181
|
}
|
|
176
182
|
return responseJson;
|
|
177
183
|
});
|
|
@@ -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(
|
|
@@ -17,9 +17,9 @@ import {
|
|
|
17
17
|
ISummaryContext,
|
|
18
18
|
IDocumentStorageService,
|
|
19
19
|
LoaderCachingPolicy,
|
|
20
|
+
DriverErrorType,
|
|
20
21
|
} from "@fluidframework/driver-definitions";
|
|
21
|
-
import { RateLimiter } from "@fluidframework/driver-utils";
|
|
22
|
-
import { throwOdspNetworkError } from "@fluidframework/odsp-doclib-utils";
|
|
22
|
+
import { RateLimiter, NonRetryableError } from "@fluidframework/driver-utils";
|
|
23
23
|
import {
|
|
24
24
|
IOdspResolvedUrl,
|
|
25
25
|
ISnapshotOptions,
|
|
@@ -313,9 +313,6 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
313
313
|
this.blobCache.setBlob(blobId, blob);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
if (!this.attributesBlobHandles.has(blobId)) {
|
|
317
|
-
return blob;
|
|
318
|
-
}
|
|
319
316
|
return blob;
|
|
320
317
|
}
|
|
321
318
|
|
|
@@ -406,22 +403,31 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
406
403
|
{ eventName: "ObtainSnapshot" },
|
|
407
404
|
async (event: PerformanceEvent) => {
|
|
408
405
|
const props = {};
|
|
409
|
-
let
|
|
406
|
+
let retrievedSnapshot: ISnapshotContents | undefined;
|
|
407
|
+
// Here's the logic to grab the persistent cache snapshot implemented by the host
|
|
408
|
+
// Epoch tracker is responsible for communicating with the persistent cache, handling epochs and cache versions
|
|
410
409
|
const cachedSnapshotP: Promise<ISnapshotContents | undefined> =
|
|
411
410
|
this.epochTracker.get(createCacheSnapshotKey(this.odspResolvedUrl))
|
|
412
|
-
.then((snapshotCachedEntry: ISnapshotCachedEntry) => {
|
|
411
|
+
.then(async (snapshotCachedEntry: ISnapshotCachedEntry) => {
|
|
413
412
|
if (snapshotCachedEntry !== undefined) {
|
|
414
413
|
// If the cached entry does not contain the entry time, then assign it a default of 30 days old.
|
|
415
|
-
|
|
416
|
-
props["cacheEntryAge"] = Date.now() - (snapshotCachedEntry.cacheEntryTime ??
|
|
414
|
+
const age = Date.now() - (snapshotCachedEntry.cacheEntryTime ??
|
|
417
415
|
(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
|
416
|
+
|
|
417
|
+
// Record the cache age
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
419
|
+
props["cacheEntryAge"] = age;
|
|
418
420
|
}
|
|
421
|
+
|
|
419
422
|
return snapshotCachedEntry;
|
|
420
423
|
});
|
|
421
424
|
|
|
425
|
+
// Based on the concurrentSnapshotFetch policy:
|
|
426
|
+
// Either retrieve both the network and cache snapshots concurrently and pick the first to return,
|
|
427
|
+
// or grab the cache value and then the network value if the cache value returns undefined.
|
|
422
428
|
let method: string;
|
|
423
429
|
if (this.hostPolicy.concurrentSnapshotFetch && !this.hostPolicy.summarizerClient) {
|
|
424
|
-
const
|
|
430
|
+
const networkSnapshotP = this.fetchSnapshot(hostSnapshotOptions);
|
|
425
431
|
|
|
426
432
|
// Ensure that failures on both paths are ignored initially.
|
|
427
433
|
// I.e. if cache fails for some reason, we will proceed with network result.
|
|
@@ -429,20 +435,20 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
429
435
|
// do want to attempt to succeed with cached data!
|
|
430
436
|
const promiseRaceWinner = await promiseRaceWithWinner([
|
|
431
437
|
cachedSnapshotP.catch(() => undefined),
|
|
432
|
-
|
|
438
|
+
networkSnapshotP.catch(() => undefined),
|
|
433
439
|
]);
|
|
434
|
-
|
|
440
|
+
retrievedSnapshot = promiseRaceWinner.value;
|
|
435
441
|
method = promiseRaceWinner.index === 0 ? "cache" : "network";
|
|
436
442
|
|
|
437
|
-
if (
|
|
443
|
+
if (retrievedSnapshot === undefined) {
|
|
438
444
|
// if network failed -> wait for cache ( then return network failure)
|
|
439
445
|
// If cache returned empty or failed -> wait for network (success of failure)
|
|
440
446
|
if (promiseRaceWinner.index === 1) {
|
|
441
|
-
|
|
447
|
+
retrievedSnapshot = await cachedSnapshotP;
|
|
442
448
|
method = "cache";
|
|
443
449
|
}
|
|
444
|
-
if (
|
|
445
|
-
|
|
450
|
+
if (retrievedSnapshot === undefined) {
|
|
451
|
+
retrievedSnapshot = await networkSnapshotP;
|
|
446
452
|
method = "network";
|
|
447
453
|
}
|
|
448
454
|
}
|
|
@@ -450,12 +456,12 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
450
456
|
// Note: There's a race condition here - another caller may come past the undefined check
|
|
451
457
|
// while the first caller is awaiting later async code in this block.
|
|
452
458
|
|
|
453
|
-
|
|
459
|
+
retrievedSnapshot = await cachedSnapshotP;
|
|
454
460
|
|
|
455
|
-
method =
|
|
461
|
+
method = retrievedSnapshot !== undefined ? "cache" : "network";
|
|
456
462
|
|
|
457
|
-
if (
|
|
458
|
-
|
|
463
|
+
if (retrievedSnapshot === undefined) {
|
|
464
|
+
retrievedSnapshot = await this.fetchSnapshot(hostSnapshotOptions);
|
|
459
465
|
}
|
|
460
466
|
}
|
|
461
467
|
if (method === "network") {
|
|
@@ -463,12 +469,11 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
463
469
|
props["cacheEntryAge"] = undefined;
|
|
464
470
|
}
|
|
465
471
|
event.end({ ...props, method });
|
|
466
|
-
return
|
|
472
|
+
return retrievedSnapshot;
|
|
467
473
|
},
|
|
468
|
-
{end: true, cancel: "error"},
|
|
469
474
|
);
|
|
470
475
|
|
|
471
|
-
// Successful call,
|
|
476
|
+
// Successful call, make network calls only
|
|
472
477
|
this.firstVersionCall = false;
|
|
473
478
|
|
|
474
479
|
this._snapshotSequenceNumber = odspSnapshotCacheValue.sequenceNumber;
|
|
@@ -503,10 +508,16 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
503
508
|
);
|
|
504
509
|
const versionsResponse = response.content;
|
|
505
510
|
if (!versionsResponse) {
|
|
506
|
-
|
|
511
|
+
throw new NonRetryableError(
|
|
512
|
+
"getVersionsReturnedNoResponse",
|
|
513
|
+
"No response from /versions endpoint",
|
|
514
|
+
DriverErrorType.genericNetworkError);
|
|
507
515
|
}
|
|
508
516
|
if (!Array.isArray(versionsResponse.value)) {
|
|
509
|
-
|
|
517
|
+
throw new NonRetryableError(
|
|
518
|
+
"getVersionsReturnedNonArrayResponse",
|
|
519
|
+
"Incorrect response from /versions endpoint",
|
|
520
|
+
DriverErrorType.genericNetworkError);
|
|
510
521
|
}
|
|
511
522
|
return versionsResponse.value.map((version) => {
|
|
512
523
|
// Parse the date from the message
|
|
@@ -678,19 +689,28 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
|
|
|
678
689
|
|
|
679
690
|
private checkSnapshotUrl() {
|
|
680
691
|
if (!this.snapshotUrl) {
|
|
681
|
-
|
|
692
|
+
throw new NonRetryableError(
|
|
693
|
+
"noSnapshotUrlProvided",
|
|
694
|
+
"Method failed because no snapshot url was available",
|
|
695
|
+
DriverErrorType.genericError);
|
|
682
696
|
}
|
|
683
697
|
}
|
|
684
698
|
|
|
685
699
|
private checkAttachmentPOSTUrl() {
|
|
686
700
|
if (!this.attachmentPOSTUrl) {
|
|
687
|
-
|
|
701
|
+
throw new NonRetryableError(
|
|
702
|
+
"noAttachmentPOSTUrlProvided",
|
|
703
|
+
"Method failed because no attachment POST url was available",
|
|
704
|
+
DriverErrorType.genericError);
|
|
688
705
|
}
|
|
689
706
|
}
|
|
690
707
|
|
|
691
708
|
private checkAttachmentGETUrl() {
|
|
692
709
|
if (!this.attachmentGETUrl) {
|
|
693
|
-
|
|
710
|
+
throw new NonRetryableError(
|
|
711
|
+
"noAttachmentGETUrlWasProvided",
|
|
712
|
+
"Method failed because no attachment GET url was available",
|
|
713
|
+
DriverErrorType.genericError);
|
|
694
714
|
}
|
|
695
715
|
}
|
|
696
716
|
|