@fluidframework/odsp-driver 0.58.0-55561 → 0.58.1001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts.d.ts +4 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/odspCache.d.ts +8 -3
- package/dist/odspCache.d.ts.map +1 -1
- package/dist/odspCache.js +1 -1
- package/dist/odspCache.js.map +1 -1
- package/dist/odspDocumentService.d.ts +5 -0
- package/dist/odspDocumentService.d.ts.map +1 -1
- package/dist/odspDocumentService.js +87 -24
- package/dist/odspDocumentService.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/vroom.d.ts +2 -1
- package/dist/vroom.d.ts.map +1 -1
- package/dist/vroom.js +6 -2
- package/dist/vroom.js.map +1 -1
- package/lib/contracts.d.ts +4 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/contracts.js.map +1 -1
- package/lib/odspCache.d.ts +8 -3
- package/lib/odspCache.d.ts.map +1 -1
- package/lib/odspCache.js +1 -1
- package/lib/odspCache.js.map +1 -1
- package/lib/odspDocumentService.d.ts +5 -0
- package/lib/odspDocumentService.d.ts.map +1 -1
- package/lib/odspDocumentService.js +87 -24
- package/lib/odspDocumentService.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/vroom.d.ts +2 -1
- package/lib/vroom.d.ts.map +1 -1
- package/lib/vroom.js +6 -2
- package/lib/vroom.js.map +1 -1
- package/package.json +12 -12
- package/src/contracts.ts +5 -0
- package/src/odspCache.ts +3 -3
- package/src/odspDocumentService.ts +110 -27
- package/src/packageVersion.ts +1 -1
- package/src/vroom.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/odsp-driver",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.1001",
|
|
4
4
|
"description": "Socket storage implementation for SPO and ODC",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": "https://github.com/microsoft/FluidFramework",
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
"@fluidframework/common-definitions": "^0.20.1",
|
|
59
59
|
"@fluidframework/common-utils": "^0.32.1",
|
|
60
60
|
"@fluidframework/core-interfaces": "^0.42.0",
|
|
61
|
-
"@fluidframework/driver-base": "0.58.
|
|
62
|
-
"@fluidframework/driver-definitions": "^0.45.
|
|
63
|
-
"@fluidframework/driver-utils": "0.58.
|
|
64
|
-
"@fluidframework/gitresources": "^0.
|
|
65
|
-
"@fluidframework/odsp-doclib-utils": "0.58.
|
|
66
|
-
"@fluidframework/odsp-driver-definitions": "0.58.
|
|
67
|
-
"@fluidframework/protocol-base": "^0.
|
|
68
|
-
"@fluidframework/protocol-definitions": "^0.
|
|
69
|
-
"@fluidframework/telemetry-utils": "0.58.
|
|
61
|
+
"@fluidframework/driver-base": "^0.58.1001",
|
|
62
|
+
"@fluidframework/driver-definitions": "^0.45.1000",
|
|
63
|
+
"@fluidframework/driver-utils": "^0.58.1001",
|
|
64
|
+
"@fluidframework/gitresources": "^0.1035.1000",
|
|
65
|
+
"@fluidframework/odsp-doclib-utils": "^0.58.1001",
|
|
66
|
+
"@fluidframework/odsp-driver-definitions": "^0.58.1001",
|
|
67
|
+
"@fluidframework/protocol-base": "^0.1035.1000",
|
|
68
|
+
"@fluidframework/protocol-definitions": "^0.1027.1000",
|
|
69
|
+
"@fluidframework/telemetry-utils": "^0.58.1001",
|
|
70
70
|
"abort-controller": "^3.0.0",
|
|
71
71
|
"node-fetch": "^2.6.1",
|
|
72
72
|
"socket.io-client": "^4.4.1",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@fluidframework/build-common": "^0.23.0",
|
|
77
|
-
"@fluidframework/eslint-config-fluid": "^0.26.0
|
|
78
|
-
"@fluidframework/mocha-test-setup": "0.58.
|
|
77
|
+
"@fluidframework/eslint-config-fluid": "^0.26.0",
|
|
78
|
+
"@fluidframework/mocha-test-setup": "^0.58.1001",
|
|
79
79
|
"@microsoft/api-extractor": "^7.16.1",
|
|
80
80
|
"@rushstack/eslint-config": "^2.5.1",
|
|
81
81
|
"@types/mocha": "^8.2.2",
|
package/src/contracts.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface ISocketStorageDiscovery {
|
|
|
34
34
|
* passed as a parameter to `OdspDocumentService.create()` factory.
|
|
35
35
|
*/
|
|
36
36
|
socketToken?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* This is the time within which client has to refresh the session on (ODSP) relay service.
|
|
40
|
+
*/
|
|
41
|
+
refreshSessionDurationSeconds?: number;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
package/src/odspCache.ts
CHANGED
|
@@ -113,9 +113,8 @@ export class PromiseCacheWithOneHourSlidingExpiry<T> extends PromiseCache<string
|
|
|
113
113
|
export interface INonPersistentCache {
|
|
114
114
|
/**
|
|
115
115
|
* Cache of joined/joining session info
|
|
116
|
-
* This cache will use a one hour sliding expiration window.
|
|
117
116
|
*/
|
|
118
|
-
readonly sessionJoinCache:
|
|
117
|
+
readonly sessionJoinCache: PromiseCache<string, {entryTime: number, joinSessionResponse: ISocketStorageDiscovery}>;
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
120
|
* Cache of resolved/resolving file URLs
|
|
@@ -134,7 +133,8 @@ export interface IOdspCache extends INonPersistentCache {
|
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
export class NonPersistentCache implements INonPersistentCache {
|
|
137
|
-
public readonly sessionJoinCache =
|
|
136
|
+
public readonly sessionJoinCache =
|
|
137
|
+
new PromiseCache<string, {entryTime: number, joinSessionResponse: ISocketStorageDiscovery}>();
|
|
138
138
|
|
|
139
139
|
public readonly fileUrlCache = new PromiseCache<string, IOdspResolvedUrl>();
|
|
140
140
|
}
|
|
@@ -52,7 +52,8 @@ import { pkgVersion as driverVersion } from "./packageVersion";
|
|
|
52
52
|
*/
|
|
53
53
|
export class OdspDocumentService implements IDocumentService {
|
|
54
54
|
private _policies: IDocumentServicePolicies;
|
|
55
|
-
|
|
55
|
+
// Timer which runs and executes the join session call after intervals.
|
|
56
|
+
private joinSessionRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
56
57
|
/**
|
|
57
58
|
* @param resolvedUrl - resolved url identifying document that will be managed by returned service instance.
|
|
58
59
|
* @param getStorageToken - function that can provide the storage token. This is is also referred to as
|
|
@@ -237,27 +238,7 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
237
238
|
? Promise.resolve(null)
|
|
238
239
|
: this.getWebsocketToken!(options);
|
|
239
240
|
|
|
240
|
-
const joinSessionPromise = this.joinSession(requestWebsocketTokenFromJoinSession, options)
|
|
241
|
-
const likelyFacetCodes = e as IFacetCodes;
|
|
242
|
-
if (Array.isArray(likelyFacetCodes.facetCodes)) {
|
|
243
|
-
for (const code of likelyFacetCodes.facetCodes) {
|
|
244
|
-
switch (code) {
|
|
245
|
-
case "sessionForbiddenOnPreservedFiles":
|
|
246
|
-
case "sessionForbiddenOnModerationEnabledLibrary":
|
|
247
|
-
case "sessionForbiddenOnRequireCheckout":
|
|
248
|
-
// This document can only be opened in storage-only mode.
|
|
249
|
-
// DeltaManager will recognize this error
|
|
250
|
-
// and load without a delta stream connection.
|
|
251
|
-
this._policies = {...this._policies,storageOnly: true};
|
|
252
|
-
throw new DeltaStreamConnectionForbiddenError(code, { driverVersion });
|
|
253
|
-
default:
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
throw e;
|
|
259
|
-
});
|
|
260
|
-
|
|
241
|
+
const joinSessionPromise = this.joinSession(requestWebsocketTokenFromJoinSession, options);
|
|
261
242
|
const [websocketEndpoint, websocketToken, io] =
|
|
262
243
|
await Promise.all([
|
|
263
244
|
joinSessionPromise,
|
|
@@ -286,6 +267,8 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
286
267
|
// On disconnect with 401/403 error code, we can just clear the joinSession cache as we will again
|
|
287
268
|
// get the auth error on reconnecting and face latency.
|
|
288
269
|
connection.on("disconnect", (error: any) => {
|
|
270
|
+
// Clear the join session refresh timer so that it can be restarted on reconnection.
|
|
271
|
+
this.clearJoinSessionTimer();
|
|
289
272
|
if (typeof error === "object" && error !== null
|
|
290
273
|
&& error.errorType === DriverErrorType.authorizationError) {
|
|
291
274
|
this.cache.sessionJoinCache.remove(this.joinSessionKey);
|
|
@@ -303,12 +286,59 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
303
286
|
});
|
|
304
287
|
}
|
|
305
288
|
|
|
289
|
+
private clearJoinSessionTimer() {
|
|
290
|
+
if (this.joinSessionRefreshTimer !== undefined) {
|
|
291
|
+
clearTimeout(this.joinSessionRefreshTimer);
|
|
292
|
+
this.joinSessionRefreshTimer = undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private async scheduleJoinSessionRefresh(delta: number) {
|
|
297
|
+
await new Promise<void>((resolve, reject) => {
|
|
298
|
+
this.joinSessionRefreshTimer = setTimeout(() => {
|
|
299
|
+
getWithRetryForTokenRefresh(async (options) => {
|
|
300
|
+
await this.joinSession(false, options);
|
|
301
|
+
resolve();
|
|
302
|
+
}).catch((error) => {
|
|
303
|
+
reject(error);
|
|
304
|
+
});
|
|
305
|
+
}, delta);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
306
309
|
private async joinSession(
|
|
307
310
|
requestSocketToken: boolean,
|
|
308
311
|
options: TokenFetchOptionsEx,
|
|
312
|
+
) {
|
|
313
|
+
return this.joinSessionCore(requestSocketToken, options).catch((e) => {
|
|
314
|
+
const likelyFacetCodes = e as IFacetCodes;
|
|
315
|
+
if (Array.isArray(likelyFacetCodes.facetCodes)) {
|
|
316
|
+
for (const code of likelyFacetCodes.facetCodes) {
|
|
317
|
+
switch (code) {
|
|
318
|
+
case "sessionForbiddenOnPreservedFiles":
|
|
319
|
+
case "sessionForbiddenOnModerationEnabledLibrary":
|
|
320
|
+
case "sessionForbiddenOnRequireCheckout":
|
|
321
|
+
// This document can only be opened in storage-only mode.
|
|
322
|
+
// DeltaManager will recognize this error
|
|
323
|
+
// and load without a delta stream connection.
|
|
324
|
+
this._policies = {...this._policies,storageOnly: true};
|
|
325
|
+
throw new DeltaStreamConnectionForbiddenError(code, { driverVersion });
|
|
326
|
+
default:
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw e;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async joinSessionCore(
|
|
336
|
+
requestSocketToken: boolean,
|
|
337
|
+
options: TokenFetchOptionsEx,
|
|
309
338
|
): Promise<ISocketStorageDiscovery> {
|
|
310
|
-
const
|
|
311
|
-
|
|
339
|
+
const disableJoinSessionRefresh = this.mc.config.getBoolean("Fluid.Driver.Odsp.disableJoinSessionRefresh");
|
|
340
|
+
const executeFetch = async () => {
|
|
341
|
+
const joinSessionResponse = await fetchJoinSession(
|
|
312
342
|
this.odspResolvedUrl,
|
|
313
343
|
"opStream/joinSession",
|
|
314
344
|
"POST",
|
|
@@ -317,12 +347,65 @@ export class OdspDocumentService implements IDocumentService {
|
|
|
317
347
|
this.epochTracker,
|
|
318
348
|
requestSocketToken,
|
|
319
349
|
options,
|
|
350
|
+
disableJoinSessionRefresh,
|
|
320
351
|
this.hostPolicy.sessionOptions?.unauthenticatedUserDisplayName,
|
|
321
352
|
);
|
|
353
|
+
return {
|
|
354
|
+
entryTime: Date.now(),
|
|
355
|
+
joinSessionResponse,
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const getResponseAndRefreshAfterDeltaMs = async () => {
|
|
360
|
+
let response = await this.cache.sessionJoinCache.addOrGet(this.joinSessionKey, executeFetch);
|
|
361
|
+
// If the response does not contain refreshSessionDurationSeconds, then treat it as old flow and let the
|
|
362
|
+
// cache entry to be treated as expired after 1 hour.
|
|
363
|
+
response.joinSessionResponse.refreshSessionDurationSeconds =
|
|
364
|
+
response.joinSessionResponse.refreshSessionDurationSeconds ?? 3600;
|
|
365
|
+
return {
|
|
366
|
+
...response,
|
|
367
|
+
refreshAfterDeltaMs: this.calculateJoinSessionRefreshDelta(
|
|
368
|
+
response.entryTime, response.joinSessionResponse.refreshSessionDurationSeconds),
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
let response = await getResponseAndRefreshAfterDeltaMs();
|
|
372
|
+
// This means that the cached entry has expired(This should not be possible if the response is fetched
|
|
373
|
+
// from the network call). In this case we remove the cached entry and fetch the new response.
|
|
374
|
+
if (response.refreshAfterDeltaMs <= 0) {
|
|
375
|
+
this.cache.sessionJoinCache.remove(this.joinSessionKey);
|
|
376
|
+
response = await getResponseAndRefreshAfterDeltaMs();
|
|
377
|
+
}
|
|
378
|
+
if (!disableJoinSessionRefresh) {
|
|
379
|
+
const props = {
|
|
380
|
+
entryTime: response.entryTime,
|
|
381
|
+
refreshSessionDurationSeconds:
|
|
382
|
+
response.joinSessionResponse.refreshSessionDurationSeconds,
|
|
383
|
+
refreshAfterDeltaMs: response.refreshAfterDeltaMs,
|
|
384
|
+
};
|
|
385
|
+
if (response.refreshAfterDeltaMs > 0) {
|
|
386
|
+
this.scheduleJoinSessionRefresh(response.refreshAfterDeltaMs)
|
|
387
|
+
.catch((error) => {
|
|
388
|
+
this.mc.logger.sendErrorEvent({
|
|
389
|
+
eventName: "JoinSessionRefreshError",
|
|
390
|
+
...props,
|
|
391
|
+
},
|
|
392
|
+
error,
|
|
393
|
+
)
|
|
394
|
+
});;
|
|
395
|
+
} else {
|
|
396
|
+
// Logging just for informational purposes to help with debugging as this is a new feature.
|
|
397
|
+
this.mc.logger.sendErrorEvent({
|
|
398
|
+
eventName: "JoinSessionRefreshNotScheduled",
|
|
399
|
+
...props,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return response.joinSessionResponse;
|
|
404
|
+
}
|
|
322
405
|
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
return
|
|
406
|
+
private calculateJoinSessionRefreshDelta(responseFetchTime: number, refreshSessionDurationSeconds: number) {
|
|
407
|
+
// 30 seconds is buffer time to refresh the session.
|
|
408
|
+
return responseFetchTime + ((refreshSessionDurationSeconds * 1000) - 30000) - Date.now();
|
|
326
409
|
}
|
|
327
410
|
|
|
328
411
|
/**
|
package/src/packageVersion.ts
CHANGED
package/src/vroom.ts
CHANGED
|
@@ -29,6 +29,7 @@ interface IJoinSessionBody {
|
|
|
29
29
|
* @param requestSocketToken - flag indicating whether joinSession is expected to return access token
|
|
30
30
|
* which is used when establishing websocket connection with collab session backend service.
|
|
31
31
|
* @param options - Options to fetch the token.
|
|
32
|
+
* @param disableJoinSessionRefresh - Whether the caller wants to disable refreshing join session periodically.
|
|
32
33
|
* @param guestDisplayName - display name used to identify guest user joining a session.
|
|
33
34
|
* This is optional and used only when collab session is being joined via invite.
|
|
34
35
|
*/
|
|
@@ -41,6 +42,7 @@ export async function fetchJoinSession(
|
|
|
41
42
|
epochTracker: EpochTracker,
|
|
42
43
|
requestSocketToken: boolean,
|
|
43
44
|
options: TokenFetchOptionsEx,
|
|
45
|
+
disableJoinSessionRefresh: boolean | undefined,
|
|
44
46
|
guestDisplayName?: string,
|
|
45
47
|
): Promise<ISocketStorageDiscovery> {
|
|
46
48
|
const token = await getStorageToken(options, "JoinSession");
|
|
@@ -61,6 +63,9 @@ export async function fetchJoinSession(
|
|
|
61
63
|
postBody += `Authorization: Bearer ${token}\r\n`;
|
|
62
64
|
postBody += `X-HTTP-Method-Override: POST\r\n`;
|
|
63
65
|
postBody += `Content-Type: application/json\r\n`;
|
|
66
|
+
if (!disableJoinSessionRefresh) {
|
|
67
|
+
postBody += `prefer: FluidRemoveCheckAccess\r\n`;
|
|
68
|
+
}
|
|
64
69
|
postBody += `_post: 1\r\n`;
|
|
65
70
|
// Name should be there when socket token is requested and vice-versa.
|
|
66
71
|
if (requestSocketToken && guestDisplayName !== undefined) {
|
|
@@ -98,6 +103,7 @@ export async function fetchJoinSession(
|
|
|
98
103
|
// pushV2 websocket urls will contain pushf
|
|
99
104
|
pushv2: socketUrl.includes("pushf"),
|
|
100
105
|
webSocketHostName,
|
|
106
|
+
refreshSessionDurationSeconds: response.content.refreshSessionDurationSeconds,
|
|
101
107
|
});
|
|
102
108
|
|
|
103
109
|
if (response.content.runtimeTenantId && !response.content.tenantId) {
|