@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.
Files changed (44) hide show
  1. package/dist/contracts.d.ts +4 -0
  2. package/dist/contracts.d.ts.map +1 -1
  3. package/dist/contracts.js.map +1 -1
  4. package/dist/odspCache.d.ts +8 -3
  5. package/dist/odspCache.d.ts.map +1 -1
  6. package/dist/odspCache.js +1 -1
  7. package/dist/odspCache.js.map +1 -1
  8. package/dist/odspDocumentService.d.ts +5 -0
  9. package/dist/odspDocumentService.d.ts.map +1 -1
  10. package/dist/odspDocumentService.js +87 -24
  11. package/dist/odspDocumentService.js.map +1 -1
  12. package/dist/packageVersion.d.ts +1 -1
  13. package/dist/packageVersion.d.ts.map +1 -1
  14. package/dist/packageVersion.js +1 -1
  15. package/dist/packageVersion.js.map +1 -1
  16. package/dist/vroom.d.ts +2 -1
  17. package/dist/vroom.d.ts.map +1 -1
  18. package/dist/vroom.js +6 -2
  19. package/dist/vroom.js.map +1 -1
  20. package/lib/contracts.d.ts +4 -0
  21. package/lib/contracts.d.ts.map +1 -1
  22. package/lib/contracts.js.map +1 -1
  23. package/lib/odspCache.d.ts +8 -3
  24. package/lib/odspCache.d.ts.map +1 -1
  25. package/lib/odspCache.js +1 -1
  26. package/lib/odspCache.js.map +1 -1
  27. package/lib/odspDocumentService.d.ts +5 -0
  28. package/lib/odspDocumentService.d.ts.map +1 -1
  29. package/lib/odspDocumentService.js +87 -24
  30. package/lib/odspDocumentService.js.map +1 -1
  31. package/lib/packageVersion.d.ts +1 -1
  32. package/lib/packageVersion.d.ts.map +1 -1
  33. package/lib/packageVersion.js +1 -1
  34. package/lib/packageVersion.js.map +1 -1
  35. package/lib/vroom.d.ts +2 -1
  36. package/lib/vroom.d.ts.map +1 -1
  37. package/lib/vroom.js +6 -2
  38. package/lib/vroom.js.map +1 -1
  39. package/package.json +12 -12
  40. package/src/contracts.ts +5 -0
  41. package/src/odspCache.ts +3 -3
  42. package/src/odspDocumentService.ts +110 -27
  43. package/src/packageVersion.ts +1 -1
  44. 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.0-55561",
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.0-55561",
62
- "@fluidframework/driver-definitions": "^0.45.0-0",
63
- "@fluidframework/driver-utils": "0.58.0-55561",
64
- "@fluidframework/gitresources": "^0.1034.0",
65
- "@fluidframework/odsp-doclib-utils": "0.58.0-55561",
66
- "@fluidframework/odsp-driver-definitions": "0.58.0-55561",
67
- "@fluidframework/protocol-base": "^0.1034.0",
68
- "@fluidframework/protocol-definitions": "^0.1026.0",
69
- "@fluidframework/telemetry-utils": "0.58.0-55561",
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-0",
78
- "@fluidframework/mocha-test-setup": "0.58.0-55561",
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: PromiseCacheWithOneHourSlidingExpiry<ISocketStorageDiscovery>;
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 = new PromiseCacheWithOneHourSlidingExpiry<ISocketStorageDiscovery>();
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).catch((e) => {
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 executeFetch = async () =>
311
- fetchJoinSession(
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
- // Note: The sessionCache is configured with a sliding expiry of 1 hour,
324
- // so if we've fetched the join session within the last hour we won't run executeFetch again now.
325
- return this.cache.sessionJoinCache.addOrGet(this.joinSessionKey, executeFetch);
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
  /**
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/odsp-driver";
9
- export const pkgVersion = "0.58.0-55561";
9
+ export const pkgVersion = "0.58.1001";
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) {