@fluidframework/odsp-driver 2.21.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/dist/fetch.d.ts +5 -1
  4. package/dist/fetch.d.ts.map +1 -1
  5. package/dist/fetch.js +5 -9
  6. package/dist/fetch.js.map +1 -1
  7. package/dist/fetchSnapshot.d.ts +1 -1
  8. package/dist/fetchSnapshot.d.ts.map +1 -1
  9. package/dist/fetchSnapshot.js +8 -5
  10. package/dist/fetchSnapshot.js.map +1 -1
  11. package/dist/getFileLink.d.ts +1 -1
  12. package/dist/getFileLink.d.ts.map +1 -1
  13. package/dist/getFileLink.js +3 -3
  14. package/dist/getFileLink.js.map +1 -1
  15. package/dist/mockify.d.ts +60 -0
  16. package/dist/mockify.d.ts.map +1 -0
  17. package/dist/mockify.js +61 -0
  18. package/dist/mockify.js.map +1 -0
  19. package/dist/odspUtils.d.ts +3 -0
  20. package/dist/odspUtils.d.ts.map +1 -1
  21. package/dist/odspUtils.js +3 -3
  22. package/dist/odspUtils.js.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/packageVersion.js +1 -1
  25. package/dist/packageVersion.js.map +1 -1
  26. package/dist/socketModule.d.ts +2 -1
  27. package/dist/socketModule.d.ts.map +1 -1
  28. package/dist/socketModule.js +2 -3
  29. package/dist/socketModule.js.map +1 -1
  30. package/dist/vroom.d.ts +1 -1
  31. package/dist/vroom.d.ts.map +1 -1
  32. package/dist/vroom.js +3 -3
  33. package/dist/vroom.js.map +1 -1
  34. package/lib/fetch.d.ts +5 -1
  35. package/lib/fetch.d.ts.map +1 -1
  36. package/lib/fetch.js +5 -5
  37. package/lib/fetch.js.map +1 -1
  38. package/lib/fetchSnapshot.d.ts +1 -1
  39. package/lib/fetchSnapshot.d.ts.map +1 -1
  40. package/lib/fetchSnapshot.js +8 -4
  41. package/lib/fetchSnapshot.js.map +1 -1
  42. package/lib/getFileLink.d.ts +1 -1
  43. package/lib/getFileLink.d.ts.map +1 -1
  44. package/lib/getFileLink.js +3 -2
  45. package/lib/getFileLink.js.map +1 -1
  46. package/lib/mockify.d.ts +60 -0
  47. package/lib/mockify.d.ts.map +1 -0
  48. package/lib/mockify.js +57 -0
  49. package/lib/mockify.js.map +1 -0
  50. package/lib/odspUtils.d.ts +3 -0
  51. package/lib/odspUtils.d.ts.map +1 -1
  52. package/lib/odspUtils.js +2 -2
  53. package/lib/odspUtils.js.map +1 -1
  54. package/lib/packageVersion.d.ts +1 -1
  55. package/lib/packageVersion.js +1 -1
  56. package/lib/packageVersion.js.map +1 -1
  57. package/lib/socketModule.d.ts +2 -1
  58. package/lib/socketModule.d.ts.map +1 -1
  59. package/lib/socketModule.js +2 -3
  60. package/lib/socketModule.js.map +1 -1
  61. package/lib/vroom.d.ts +1 -1
  62. package/lib/vroom.d.ts.map +1 -1
  63. package/lib/vroom.js +3 -2
  64. package/lib/vroom.js.map +1 -1
  65. package/package.json +15 -17
  66. package/src/fetch.ts +5 -11
  67. package/src/fetchSnapshot.ts +79 -73
  68. package/src/getFileLink.ts +56 -53
  69. package/src/mockify.ts +67 -0
  70. package/src/odspUtils.ts +3 -3
  71. package/src/packageVersion.ts +1 -1
  72. package/src/socketModule.ts +3 -3
  73. package/src/vroom.ts +92 -89
@@ -20,6 +20,7 @@ import {
20
20
  } from "@fluidframework/telemetry-utils/internal";
21
21
 
22
22
  import { getHeadersWithAuth } from "./getUrlAndHeadersWithAuth.js";
23
+ import { mockify } from "./mockify.js";
23
24
  import {
24
25
  fetchHelper,
25
26
  getWithRetryForTokenRefresh,
@@ -42,64 +43,66 @@ const fileLinkCache = new Map<string, Promise<string>>();
42
43
  * @param logger - used to log results of operation, including any error
43
44
  * @returns Promise which resolves to file link url when successful; otherwise, undefined.
44
45
  */
45
- export async function getFileLink(
46
- getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
47
- resolvedUrl: IOdspResolvedUrl,
48
- logger: ITelemetryLoggerExt,
49
- ): Promise<string> {
50
- const cacheKey = `${resolvedUrl.siteUrl}_${resolvedUrl.driveId}_${resolvedUrl.itemId}`;
51
- const maybeFileLinkCacheEntry = fileLinkCache.get(cacheKey);
52
- if (maybeFileLinkCacheEntry !== undefined) {
53
- return maybeFileLinkCacheEntry;
54
- }
46
+ export const getFileLink = mockify(
47
+ async (
48
+ getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
49
+ resolvedUrl: IOdspResolvedUrl,
50
+ logger: ITelemetryLoggerExt,
51
+ ): Promise<string> => {
52
+ const cacheKey = `${resolvedUrl.siteUrl}_${resolvedUrl.driveId}_${resolvedUrl.itemId}`;
53
+ const maybeFileLinkCacheEntry = fileLinkCache.get(cacheKey);
54
+ if (maybeFileLinkCacheEntry !== undefined) {
55
+ return maybeFileLinkCacheEntry;
56
+ }
55
57
 
56
- const fileLinkGenerator = async function (): Promise<string> {
57
- let fileLinkCore: string;
58
- try {
59
- let retryCount = 0;
60
- fileLinkCore = await runWithRetry(
61
- async () =>
62
- runWithRetryForCoherencyAndServiceReadOnlyErrors(
63
- async () =>
64
- getFileLinkWithLocationRedirectionHandling(getToken, resolvedUrl, logger),
65
- "getFileLinkCore",
66
- logger,
67
- ),
68
- "getShareLink",
69
- logger,
70
- {
71
- // TODO: use a stronger type
72
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
- onRetry(delayInMs: number, error: any) {
74
- retryCount++;
75
- if (retryCount === 5) {
76
- if (error !== undefined && typeof error === "object") {
77
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
78
- error.canRetry = false;
58
+ const fileLinkGenerator = async function (): Promise<string> {
59
+ let fileLinkCore: string;
60
+ try {
61
+ let retryCount = 0;
62
+ fileLinkCore = await runWithRetry(
63
+ async () =>
64
+ runWithRetryForCoherencyAndServiceReadOnlyErrors(
65
+ async () =>
66
+ getFileLinkWithLocationRedirectionHandling(getToken, resolvedUrl, logger),
67
+ "getFileLinkCore",
68
+ logger,
69
+ ),
70
+ "getShareLink",
71
+ logger,
72
+ {
73
+ // TODO: use a stronger type
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ onRetry(delayInMs: number, error: any) {
76
+ retryCount++;
77
+ if (retryCount === 5) {
78
+ if (error !== undefined && typeof error === "object") {
79
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
80
+ error.canRetry = false;
81
+ throw error;
82
+ }
79
83
  throw error;
80
84
  }
81
- throw error;
82
- }
85
+ },
83
86
  },
84
- },
85
- );
86
- } catch (error) {
87
- // Delete from the cache to permit retrying later.
88
- fileLinkCache.delete(cacheKey);
89
- throw error;
90
- }
87
+ );
88
+ } catch (error) {
89
+ // Delete from the cache to permit retrying later.
90
+ fileLinkCache.delete(cacheKey);
91
+ throw error;
92
+ }
91
93
 
92
- // We are guaranteed to run the getFileLinkCore at least once with successful result (which must be a string)
93
- assert(
94
- fileLinkCore !== undefined,
95
- 0x292 /* "Unexpected undefined result from getFileLinkCore" */,
96
- );
97
- return fileLinkCore;
98
- };
99
- const fileLink = fileLinkGenerator();
100
- fileLinkCache.set(cacheKey, fileLink);
101
- return fileLink;
102
- }
94
+ // We are guaranteed to run the getFileLinkCore at least once with successful result (which must be a string)
95
+ assert(
96
+ fileLinkCore !== undefined,
97
+ 0x292 /* "Unexpected undefined result from getFileLinkCore" */,
98
+ );
99
+ return fileLinkCore;
100
+ };
101
+ const fileLink = fileLinkGenerator();
102
+ fileLinkCache.set(cacheKey, fileLink);
103
+ return fileLink;
104
+ },
105
+ );
103
106
 
104
107
  /**
105
108
  * Handles location redirection while fulfilling the getFileLink call. We don't want browser to handle
package/src/mockify.ts ADDED
@@ -0,0 +1,67 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ /**
7
+ * A special key used to store the original function in a {@link Mockable | mockable} function.
8
+ * @remarks Use {@link mockify | `mockify.key`} as a convenient way to access this key.
9
+ */
10
+ export const mockifyMockKey = Symbol("`mockify` mock function key");
11
+
12
+ /**
13
+ * A function that can be mocked after being decorated by {@link mockify | mockify()}.
14
+ */
15
+ export interface Mockable<T extends (...args: any[]) => unknown> {
16
+ (...args: Parameters<T>): ReturnType<T>;
17
+ [mockifyMockKey]: T;
18
+ }
19
+
20
+ /**
21
+ * Decorates a function to allow it to be mocked.
22
+ * @param fn - The function that will become mockable.
23
+ * @returns A function with a {@link mockifyMockKey | special property } that can be overwritten to mock the original function.
24
+ * By default, this property is set to the original function.
25
+ * If overwritten with a new function, the new function will be called instead of the original.
26
+ * @example
27
+ * ```typescript
28
+ * const original = () => console.log("original");
29
+ * const mockable = mockify(original);
30
+ * mockable(); // logs "original"
31
+ * mockable[mockify.key] = () => console.log("mocked");
32
+ * mockable(); // logs "mocked"
33
+ * mockable[mockify.key] = original;
34
+ * mockable(); // logs "original"
35
+ * ```
36
+ *
37
+ * This pattern is useful for mocking top-level exported functions in a module.
38
+ * For example,
39
+ * ```typescript
40
+ * export function fn() { /* ... * / }
41
+ * ```
42
+ * becomes
43
+ * ```typescript
44
+ * import { mockify } from "./mockify.js";
45
+ * export const fn = mockify(() => { /* ... * / });
46
+ * ```
47
+ * and can now be mocked by another module that imports it.
48
+ * ```typescript
49
+ * import * as sinon from "sinon";
50
+ * import { mockify } from "./mockify.js";
51
+ * import { fn } from "./module.js";
52
+ * sinon.stub(fn, mockify.key).callsFake(() => {
53
+ * // ... mock function implementation ...
54
+ * });
55
+ * // ...
56
+ * sinon.restore();
57
+ * ```
58
+ */
59
+ export function mockify<T extends (...args: any[]) => unknown>(fn: T): Mockable<T> {
60
+ const mockable = (...args: Parameters<T>): ReturnType<T> => {
61
+ return mockable[mockifyMockKey](...args) as ReturnType<T>;
62
+ };
63
+ mockable[mockifyMockKey] = fn;
64
+ return mockable;
65
+ }
66
+
67
+ mockify.key = mockifyMockKey;
package/src/odspUtils.ts CHANGED
@@ -54,7 +54,6 @@ import {
54
54
  wrapError,
55
55
  } from "@fluidframework/telemetry-utils/internal";
56
56
 
57
- import { fetch } from "./fetch.js";
58
57
  import { storeLocatorInOdspUrl } from "./odspFluidFileLink.js";
59
58
  // eslint-disable-next-line import/no-deprecated
60
59
  import { ISnapshotContents } from "./odspPublicUtils.js";
@@ -137,10 +136,9 @@ export async function fetchHelper(
137
136
  ): Promise<IOdspResponse<Response>> {
138
137
  const start = performanceNow();
139
138
 
140
- // Node-fetch and dom have conflicting typing, force them to work by casting for now
141
139
  return fetch(requestInfo, requestInit).then(
142
140
  async (fetchResponse) => {
143
- const response = fetchResponse as unknown as Response;
141
+ const response = fetchResponse;
144
142
  // Let's assume we can retry.
145
143
  if (!response) {
146
144
  throw new NonRetryableError(
@@ -221,6 +219,8 @@ export async function fetchHelper(
221
219
  },
222
220
  );
223
221
  }
222
+ // This allows `fetch` to be mocked (e.g. with sinon `stub()`)
223
+ fetchHelper.fetch = fetch;
224
224
 
225
225
  /**
226
226
  * A utility function to fetch and parse as JSON with support for retries
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/odsp-driver";
9
- export const pkgVersion = "2.21.0";
9
+ export const pkgVersion = "2.22.0";
@@ -5,6 +5,6 @@
5
5
 
6
6
  import { io } from "socket.io-client";
7
7
 
8
- // Import is required for side-effects.
9
- // eslint-disable-next-line unicorn/prefer-export-from
10
- export const SocketIOClientStatic = io;
8
+ import { mockify, type Mockable } from "./mockify.js";
9
+
10
+ export const SocketIOClientStatic: Mockable<typeof io> = mockify(io);
package/src/vroom.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  import { v4 as uuid } from "uuid";
17
17
 
18
18
  import { EpochTracker } from "./epochTracker.js";
19
+ import { mockify } from "./mockify.js";
19
20
  import { getApiRoot } from "./odspUrlHelper.js";
20
21
  import { TokenFetchOptionsEx } from "./odspUtils.js";
21
22
  import { runWithRetry } from "./retryUtils.js";
@@ -42,100 +43,102 @@ interface IJoinSessionBody {
42
43
  * This is optional and used only when collab session is being joined by client acting in app-only mode (i.e. without user context).
43
44
  * If not specified client display name is extracted from the access token that is used to join session.
44
45
  */
45
- export async function fetchJoinSession(
46
- urlParts: IOdspUrlParts,
47
- path: string,
48
- method: "GET" | "POST",
49
- logger: ITelemetryLoggerExt,
50
- getAuthHeader: InstrumentedStorageTokenFetcher,
51
- epochTracker: EpochTracker,
52
- requestSocketToken: boolean,
53
- options: TokenFetchOptionsEx,
54
- disableJoinSessionRefresh: boolean | undefined,
55
- isRefreshingJoinSession: boolean,
56
- displayName: string | undefined,
57
- ): Promise<ISocketStorageDiscovery> {
58
- const apiRoot = getApiRoot(new URL(urlParts.siteUrl));
59
- const url = `${apiRoot}/drives/${urlParts.driveId}/items/${urlParts.itemId}/${path}?ump=1`;
60
- const authHeader = await getAuthHeader(
61
- { ...options, request: { url, method } },
62
- "JoinSession",
63
- );
46
+ export const fetchJoinSession = mockify(
47
+ async (
48
+ urlParts: IOdspUrlParts,
49
+ path: string,
50
+ method: "GET" | "POST",
51
+ logger: ITelemetryLoggerExt,
52
+ getAuthHeader: InstrumentedStorageTokenFetcher,
53
+ epochTracker: EpochTracker,
54
+ requestSocketToken: boolean,
55
+ options: TokenFetchOptionsEx,
56
+ disableJoinSessionRefresh: boolean | undefined,
57
+ isRefreshingJoinSession: boolean,
58
+ displayName: string | undefined,
59
+ ): Promise<ISocketStorageDiscovery> => {
60
+ const apiRoot = getApiRoot(new URL(urlParts.siteUrl));
61
+ const url = `${apiRoot}/drives/${urlParts.driveId}/items/${urlParts.itemId}/${path}?ump=1`;
62
+ const authHeader = await getAuthHeader(
63
+ { ...options, request: { url, method } },
64
+ "JoinSession",
65
+ );
64
66
 
65
- const tokenRefreshProps = options.refresh
66
- ? { hasClaims: !!options.claims, hasTenantId: !!options.tenantId }
67
- : {};
68
- const details: ITelemetryBaseProperties = {
69
- refreshedToken: options.refresh,
70
- requestSocketToken,
71
- ...tokenRefreshProps,
72
- refreshingSession: isRefreshingJoinSession,
73
- };
74
-
75
- return PerformanceEvent.timedExecAsync(
76
- logger,
77
- {
78
- eventName: "JoinSession",
79
- attempts: options.refresh ? 2 : 1,
80
- details: JSON.stringify(details),
67
+ const tokenRefreshProps = options.refresh
68
+ ? { hasClaims: !!options.claims, hasTenantId: !!options.tenantId }
69
+ : {};
70
+ const details: ITelemetryBaseProperties = {
71
+ refreshedToken: options.refresh,
72
+ requestSocketToken,
81
73
  ...tokenRefreshProps,
82
- },
83
- async (event) => {
84
- const formBoundary = uuid();
85
- let postBody = `--${formBoundary}\r\n`;
86
- postBody += `Authorization: ${authHeader}\r\n`;
87
- postBody += `X-HTTP-Method-Override: POST\r\n`;
88
- postBody += `Content-Type: application/json\r\n`;
89
- if (!disableJoinSessionRefresh) {
90
- postBody += `prefer: FluidRemoveCheckAccess\r\n`;
91
- }
92
- postBody += `_post: 1\r\n`;
74
+ refreshingSession: isRefreshingJoinSession,
75
+ };
93
76
 
94
- let requestBody: IJoinSessionBody | undefined;
95
- if (requestSocketToken) {
96
- requestBody = { ...requestBody, requestSocketToken: true };
97
- }
98
- if (displayName) {
99
- requestBody = { ...requestBody, displayName };
100
- }
101
- if (requestBody) {
102
- postBody += `\r\n${JSON.stringify(requestBody)}\r\n`;
103
- }
104
- postBody += `\r\n--${formBoundary}--`;
105
- const headers: { [index: string]: string } = {
106
- "Content-Type": `multipart/form-data;boundary=${formBoundary}`,
107
- };
77
+ return PerformanceEvent.timedExecAsync(
78
+ logger,
79
+ {
80
+ eventName: "JoinSession",
81
+ attempts: options.refresh ? 2 : 1,
82
+ details: JSON.stringify(details),
83
+ ...tokenRefreshProps,
84
+ },
85
+ async (event) => {
86
+ const formBoundary = uuid();
87
+ let postBody = `--${formBoundary}\r\n`;
88
+ postBody += `Authorization: ${authHeader}\r\n`;
89
+ postBody += `X-HTTP-Method-Override: POST\r\n`;
90
+ postBody += `Content-Type: application/json\r\n`;
91
+ if (!disableJoinSessionRefresh) {
92
+ postBody += `prefer: FluidRemoveCheckAccess\r\n`;
93
+ }
94
+ postBody += `_post: 1\r\n`;
108
95
 
109
- const response = await runWithRetry(
110
- async () =>
111
- epochTracker.fetchAndParseAsJSON<ISocketStorageDiscovery>(
112
- url,
113
- { method, headers, body: postBody },
114
- "joinSession",
115
- true,
116
- ),
117
- "joinSession",
118
- logger,
119
- );
96
+ let requestBody: IJoinSessionBody | undefined;
97
+ if (requestSocketToken) {
98
+ requestBody = { ...requestBody, requestSocketToken: true };
99
+ }
100
+ if (displayName) {
101
+ requestBody = { ...requestBody, displayName };
102
+ }
103
+ if (requestBody) {
104
+ postBody += `\r\n${JSON.stringify(requestBody)}\r\n`;
105
+ }
106
+ postBody += `\r\n--${formBoundary}--`;
107
+ const headers: { [index: string]: string } = {
108
+ "Content-Type": `multipart/form-data;boundary=${formBoundary}`,
109
+ };
120
110
 
121
- const socketUrl = response.content.deltaStreamSocketUrl;
122
- // expecting socketUrl to be something like https://{hostName}/...
123
- const webSocketHostName = socketUrl.split("/")[2];
111
+ const response = await runWithRetry(
112
+ async () =>
113
+ epochTracker.fetchAndParseAsJSON<ISocketStorageDiscovery>(
114
+ url,
115
+ { method, headers, body: postBody },
116
+ "joinSession",
117
+ true,
118
+ ),
119
+ "joinSession",
120
+ logger,
121
+ );
124
122
 
125
- // TODO SPO-specific telemetry
126
- event.end({
127
- ...response.propsToLog,
128
- // pushV2 websocket urls will contain pushf
129
- pushv2: socketUrl.includes("pushf"),
130
- webSocketHostName,
131
- refreshSessionDurationSeconds: response.content.refreshSessionDurationSeconds,
132
- });
123
+ const socketUrl = response.content.deltaStreamSocketUrl;
124
+ // expecting socketUrl to be something like https://{hostName}/...
125
+ const webSocketHostName = socketUrl.split("/")[2];
133
126
 
134
- if (response.content.runtimeTenantId && !response.content.tenantId) {
135
- response.content.tenantId = response.content.runtimeTenantId;
136
- }
127
+ // TODO SPO-specific telemetry
128
+ event.end({
129
+ ...response.propsToLog,
130
+ // pushV2 websocket urls will contain pushf
131
+ pushv2: socketUrl.includes("pushf"),
132
+ webSocketHostName,
133
+ refreshSessionDurationSeconds: response.content.refreshSessionDurationSeconds,
134
+ });
137
135
 
138
- return response.content;
139
- },
140
- );
141
- }
136
+ if (response.content.runtimeTenantId && !response.content.tenantId) {
137
+ response.content.tenantId = response.content.runtimeTenantId;
138
+ }
139
+
140
+ return response.content;
141
+ },
142
+ );
143
+ },
144
+ );