@equinor/fusion-framework-module-http 8.0.3 → 8.0.4
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/CHANGELOG.md +19 -0
- package/dist/esm/configurator.js +7 -1
- package/dist/esm/configurator.js.map +1 -1
- package/dist/esm/errors/ClientNotFoundException.js +10 -0
- package/dist/esm/errors/ClientNotFoundException.js.map +1 -0
- package/dist/esm/errors/HttpJsonResponseError.js +26 -0
- package/dist/esm/errors/HttpJsonResponseError.js.map +1 -0
- package/dist/esm/errors/HttpResponseError.js +19 -0
- package/dist/esm/errors/HttpResponseError.js.map +1 -0
- package/dist/esm/errors/ServerSentEventResponseError.js +25 -0
- package/dist/esm/errors/ServerSentEventResponseError.js.map +1 -0
- package/dist/esm/errors/index.js +5 -0
- package/dist/esm/errors/index.js.map +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/client/client-msal.js +1 -0
- package/dist/esm/lib/client/client-msal.js.map +1 -1
- package/dist/esm/lib/client/client.js +40 -4
- package/dist/esm/lib/client/client.js.map +1 -1
- package/dist/esm/lib/operators/{http-request-handler.js → HttpRequestHandler.js} +3 -3
- package/dist/esm/lib/operators/HttpRequestHandler.js.map +1 -0
- package/dist/esm/lib/operators/{http-response-handler.js → HttpResponseHandler.js} +2 -2
- package/dist/esm/lib/operators/HttpResponseHandler.js.map +1 -0
- package/dist/esm/lib/operators/{process-operators.js → ProcessOperators.js} +4 -1
- package/dist/esm/lib/operators/ProcessOperators.js.map +1 -0
- package/dist/esm/lib/operators/{capitalize-request-method.operator.js → capitalize-request-method-operator.js} +4 -2
- package/dist/esm/lib/operators/capitalize-request-method-operator.js.map +1 -0
- package/dist/esm/lib/operators/{fetch-request.schema.js → fetch-request.schemas.js} +1 -1
- package/dist/esm/lib/operators/fetch-request.schemas.js.map +1 -0
- package/dist/esm/lib/operators/index.js +6 -6
- package/dist/esm/lib/operators/index.js.map +1 -1
- package/dist/esm/lib/operators/{request-validation.operator.js → request-validation-operator.js} +3 -2
- package/dist/esm/lib/operators/{request-validation.operator.js.map → request-validation-operator.js.map} +1 -1
- package/dist/esm/lib/operators/{sse.operator.js → sse-map.operator.js} +5 -3
- package/dist/esm/lib/operators/sse-map.operator.js.map +1 -0
- package/dist/esm/lib/selectors/blob-selector.js +3 -2
- package/dist/esm/lib/selectors/blob-selector.js.map +1 -1
- package/dist/esm/lib/selectors/{sse-selector.js → create-sse-selector.js} +20 -5
- package/dist/esm/lib/selectors/create-sse-selector.js.map +1 -0
- package/dist/esm/lib/selectors/index.js +1 -1
- package/dist/esm/lib/selectors/index.js.map +1 -1
- package/dist/esm/lib/selectors/json-selector.js +1 -1
- package/dist/esm/lib/selectors/json-selector.js.map +1 -1
- package/dist/esm/module.js +4 -1
- package/dist/esm/module.js.map +1 -1
- package/dist/esm/provider.js +14 -8
- package/dist/esm/provider.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/configurator.d.ts +5 -1
- package/dist/types/errors/ClientNotFoundException.d.ts +9 -0
- package/dist/types/errors/HttpJsonResponseError.d.ts +23 -0
- package/dist/types/errors/HttpResponseError.d.ts +15 -0
- package/dist/types/errors/ServerSentEventResponseError.d.ts +23 -0
- package/dist/types/errors/index.d.ts +4 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/lib/client/client-msal.d.ts +1 -0
- package/dist/types/lib/client/client.d.ts +33 -3
- package/dist/types/lib/operators/{http-request-handler.d.ts → HttpRequestHandler.d.ts} +1 -1
- package/dist/types/lib/operators/{http-response-handler.d.ts → HttpResponseHandler.d.ts} +1 -1
- package/dist/types/lib/operators/index.d.ts +6 -6
- package/dist/types/lib/operators/{sse.operator.d.ts → sse-map.operator.d.ts} +1 -1
- package/dist/types/lib/selectors/index.d.ts +2 -2
- package/dist/types/provider.d.ts +7 -8
- package/dist/types/version.d.ts +1 -1
- package/package.json +7 -7
- package/src/configurator.ts +7 -1
- package/src/errors/ClientNotFoundException.ts +9 -0
- package/src/errors/HttpJsonResponseError.ts +31 -0
- package/src/errors/HttpResponseError.ts +21 -0
- package/src/errors/ServerSentEventResponseError.ts +30 -0
- package/src/errors/index.ts +4 -0
- package/src/index.ts +2 -1
- package/src/lib/client/client-msal.ts +1 -0
- package/src/lib/client/client.ts +40 -4
- package/src/lib/operators/{http-request-handler.ts → HttpRequestHandler.ts} +2 -2
- package/src/lib/operators/{http-response-handler.ts → HttpResponseHandler.ts} +1 -1
- package/src/lib/operators/{process-operators.ts → ProcessOperators.ts} +3 -0
- package/src/lib/operators/{capitalize-request-method.operator.ts → capitalize-request-method-operator.ts} +3 -1
- package/src/lib/operators/index.ts +6 -6
- package/src/lib/operators/{request-validation.operator.ts → request-validation-operator.ts} +2 -1
- package/src/lib/operators/{sse.operator.ts → sse-map.operator.ts} +2 -1
- package/src/lib/operators/types.ts +1 -0
- package/src/lib/selectors/blob-selector.ts +3 -2
- package/src/lib/selectors/{sse-selector.ts → create-sse-selector.ts} +34 -17
- package/src/lib/selectors/index.ts +2 -2
- package/src/lib/selectors/json-selector.ts +1 -1
- package/src/module.ts +4 -1
- package/src/provider.ts +14 -8
- package/src/version.ts +1 -1
- package/tests/HttpClient.test.ts +2 -2
- package/tests/operators.test.ts +5 -1
- package/tests/sse.selector.test.ts +34 -21
- package/vitest.config.ts +1 -1
- package/dist/esm/errors.js +0 -58
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/lib/operators/capitalize-request-method.operator.js.map +0 -1
- package/dist/esm/lib/operators/fetch-request.schema.js.map +0 -1
- package/dist/esm/lib/operators/http-request-handler.js.map +0 -1
- package/dist/esm/lib/operators/http-response-handler.js.map +0 -1
- package/dist/esm/lib/operators/process-operators.js.map +0 -1
- package/dist/esm/lib/operators/sse.operator.js.map +0 -1
- package/dist/esm/lib/selectors/sse-selector.js.map +0 -1
- package/dist/types/errors.d.ts +0 -51
- package/src/errors.ts +0 -69
- package/dist/types/lib/operators/{process-operators.d.ts → ProcessOperators.d.ts} +0 -0
- package/dist/types/lib/operators/{capitalize-request-method.operator.d.ts → capitalize-request-method-operator.d.ts} +0 -0
- package/dist/types/lib/operators/{fetch-request.schema.d.ts → fetch-request.schemas.d.ts} +26 -26
- /package/dist/types/lib/operators/{request-validation.operator.d.ts → request-validation-operator.d.ts} +0 -0
- /package/dist/types/lib/selectors/{sse-selector.d.ts → create-sse-selector.d.ts} +0 -0
- /package/src/lib/operators/{fetch-request.schema.ts → fetch-request.schemas.ts} +0 -0
package/src/lib/client/client.ts
CHANGED
|
@@ -17,12 +17,12 @@ import type {
|
|
|
17
17
|
StreamResponse,
|
|
18
18
|
} from './types';
|
|
19
19
|
|
|
20
|
-
import { HttpResponseError } from '../../errors';
|
|
20
|
+
import { HttpResponseError } from '../../errors/index.js';
|
|
21
21
|
import {
|
|
22
22
|
createSseSelector,
|
|
23
23
|
type ServerSentEvent,
|
|
24
24
|
type SseSelectorOptions,
|
|
25
|
-
} from '../selectors/sse-selector';
|
|
25
|
+
} from '../selectors/create-sse-selector';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Configuration options for creating an `HttpClient` instance.
|
|
@@ -78,6 +78,7 @@ export class HttpClient<
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* A stream of requests that are about to be executed.
|
|
81
|
+
* @returns An `Observable` that emits each request before it is executed.
|
|
81
82
|
*/
|
|
82
83
|
public get request$(): Observable<TRequest> {
|
|
83
84
|
return this._request$.asObservable();
|
|
@@ -85,11 +86,17 @@ export class HttpClient<
|
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
88
|
* A stream of responses that have been received.
|
|
89
|
+
* @returns An `Observable` that emits each response as it is received.
|
|
88
90
|
*/
|
|
89
91
|
public get response$(): Observable<TResponse> {
|
|
90
92
|
return this._response$.asObservable();
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Creates a new `HttpClient`.
|
|
97
|
+
* @param uri - The base URI used to resolve relative request paths.
|
|
98
|
+
* @param options - Optional request and response handlers.
|
|
99
|
+
*/
|
|
93
100
|
constructor(
|
|
94
101
|
public uri: string,
|
|
95
102
|
options?: Partial<HttpClientCreateOptions<TRequest, TResponse>>,
|
|
@@ -112,6 +119,7 @@ export class HttpClient<
|
|
|
112
119
|
/**
|
|
113
120
|
* Fetches data from the specified path and returns a stream response.
|
|
114
121
|
*
|
|
122
|
+
* @template T - The expected shape of the fetched data.
|
|
115
123
|
* @param path - The path to fetch data from.
|
|
116
124
|
* @param args - Optional request initialization options, including a custom selector function.
|
|
117
125
|
* @returns A stream response containing the fetched data.
|
|
@@ -126,6 +134,7 @@ export class HttpClient<
|
|
|
126
134
|
/**
|
|
127
135
|
* Fetches data from the specified path and returns a Promise containing the fetched data.
|
|
128
136
|
*
|
|
137
|
+
* @template T - The expected shape of the fetched data.
|
|
129
138
|
* @param path - The path to fetch data from.
|
|
130
139
|
* @param args - Optional request initialization options, including a custom selector function.
|
|
131
140
|
* @returns A Promise containing the fetched data.
|
|
@@ -137,7 +146,13 @@ export class HttpClient<
|
|
|
137
146
|
return firstValueFrom(this.fetch$<T>(path, args));
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* @deprecated Use {@link fetch} instead.
|
|
151
|
+
* @template T - The expected shape of the fetched data.
|
|
152
|
+
* @param path - The path to fetch data from.
|
|
153
|
+
* @param args - Optional request initialization options, including a custom selector function.
|
|
154
|
+
* @returns A Promise containing the fetched data.
|
|
155
|
+
*/
|
|
141
156
|
public fetchAsync<T = TResponse>(
|
|
142
157
|
path: string,
|
|
143
158
|
args?: FetchRequestInit<T, TRequest, TResponse>,
|
|
@@ -148,6 +163,7 @@ export class HttpClient<
|
|
|
148
163
|
/**
|
|
149
164
|
* Fetches data from the specified path and returns a stream response containing the data in JSON format.
|
|
150
165
|
*
|
|
166
|
+
* @template T - The expected shape of the parsed JSON data.
|
|
151
167
|
* @param path - The path to fetch the data from.
|
|
152
168
|
* @param args - Optional request initialization options, including a custom selector function and request body.
|
|
153
169
|
* - `body`: The request body, which will be automatically serialized to JSON if it's an object.
|
|
@@ -175,6 +191,7 @@ export class HttpClient<
|
|
|
175
191
|
/**
|
|
176
192
|
* Fetches data from the specified path and returns a Promise containing the fetched data in JSON format.
|
|
177
193
|
*
|
|
194
|
+
* @template T - The expected shape of the parsed JSON data.
|
|
178
195
|
* @param path - The path to fetch the data from.
|
|
179
196
|
* @param args - Optional request initialization options, including a custom selector function and request body.
|
|
180
197
|
* - `body`: The request body, which will be automatically serialized to JSON if it's an object.
|
|
@@ -192,6 +209,7 @@ export class HttpClient<
|
|
|
192
209
|
/**
|
|
193
210
|
* Fetches a blob resource from the specified path and returns a stream response.
|
|
194
211
|
*
|
|
212
|
+
* @template T - The expected shape of the fetched blob data.
|
|
195
213
|
* @param path - The path to the blob resource.
|
|
196
214
|
* @param args - Optional request initialization options, including a custom selector function.
|
|
197
215
|
* @returns A stream response containing the fetched blob data.
|
|
@@ -216,6 +234,7 @@ export class HttpClient<
|
|
|
216
234
|
/**
|
|
217
235
|
* Fetches a blob from the specified path and returns a Promise that resolves to the blob result.
|
|
218
236
|
*
|
|
237
|
+
* @template T - The expected shape of the fetched blob data.
|
|
219
238
|
* @param path - The path to fetch the blob from.
|
|
220
239
|
* @param args - Optional arguments for the fetch request, including request body, headers, and response type.
|
|
221
240
|
* @returns A Promise that resolves to the blob result.
|
|
@@ -272,7 +291,13 @@ export class HttpClient<
|
|
|
272
291
|
>);
|
|
273
292
|
}
|
|
274
293
|
|
|
275
|
-
/**
|
|
294
|
+
/**
|
|
295
|
+
* @deprecated Use {@link json} instead.
|
|
296
|
+
* @template T - The expected shape of the parsed JSON data.
|
|
297
|
+
* @param path - The path to fetch the data from.
|
|
298
|
+
* @param args - Optional request initialization options, including a custom selector function and request body.
|
|
299
|
+
* @returns A Promise containing the fetched data in JSON format.
|
|
300
|
+
*/
|
|
276
301
|
public jsonAsync<T = unknown>(
|
|
277
302
|
path: string,
|
|
278
303
|
args?: FetchRequestInit<T, JsonRequest<TRequest>, TResponse>,
|
|
@@ -283,6 +308,8 @@ export class HttpClient<
|
|
|
283
308
|
/**
|
|
284
309
|
* Executes an HTTP request using the specified method and path.
|
|
285
310
|
*
|
|
311
|
+
* @template T - The expected shape of the result.
|
|
312
|
+
* @template TMethod - The name of the `IHttpClient` method to invoke.
|
|
286
313
|
* @param method - The HTTP method to use for the request, such as 'fetch', 'json', or 'blob'.
|
|
287
314
|
* @param path - The path to the resource to fetch.
|
|
288
315
|
* @param init - Optional request initialization options, including request body, headers, and response type.
|
|
@@ -308,10 +335,13 @@ export class HttpClient<
|
|
|
308
335
|
/**
|
|
309
336
|
* Fetches data from the specified path and returns an Observable that emits the response.
|
|
310
337
|
*
|
|
338
|
+
* @template T - The expected shape of the emitted data.
|
|
311
339
|
* @param path - The path to fetch the data from.
|
|
312
340
|
* @param args - Optional arguments for the fetch request, including a response selector function, request body, headers, and response type.
|
|
313
341
|
* @returns {Observable<T>} An Observable that emits the response data.
|
|
314
342
|
*
|
|
343
|
+
* @throws {HttpResponseError} When the optional `selector` throws while transforming the response.
|
|
344
|
+
*
|
|
315
345
|
* This method handles the following steps:
|
|
316
346
|
* 1. Resolves the full URL by combining the base URI and the provided path.
|
|
317
347
|
* 2. Prepares the request by passing it through the `requestHandler.process()` method.
|
|
@@ -325,6 +355,9 @@ export class HttpClient<
|
|
|
325
355
|
args?: FetchRequestInit<T, TRequest, TResponse>,
|
|
326
356
|
): Observable<T> {
|
|
327
357
|
const { selector, ...options } = args || {};
|
|
358
|
+
// `fromFetch` yields the raw fetch `Response`, but `responseHandler.process()` (called via
|
|
359
|
+
// `_prepareResponse`) expects the pipeline's generic `TResponse` shape — cast through
|
|
360
|
+
// `unknown` since the two are only compatible after that processing step.
|
|
328
361
|
const response$ = of({
|
|
329
362
|
...options,
|
|
330
363
|
path,
|
|
@@ -343,6 +376,7 @@ export class HttpClient<
|
|
|
343
376
|
|
|
344
377
|
/** execute selector */
|
|
345
378
|
switchMap((response) => {
|
|
379
|
+
// only run the selector when one was provided; otherwise pass the response through untouched
|
|
346
380
|
if (selector) {
|
|
347
381
|
try {
|
|
348
382
|
return selector(response);
|
|
@@ -361,6 +395,8 @@ export class HttpClient<
|
|
|
361
395
|
/** cancel request on abort signal */
|
|
362
396
|
takeUntil(this._abort$),
|
|
363
397
|
);
|
|
398
|
+
// The pipe above resolves to the per-call generic `T` (via the optional `selector`), but
|
|
399
|
+
// the observable's static type tracks the class-level `TResponse` — cast to the caller's `T`.
|
|
364
400
|
return response$ as unknown as Observable<T>;
|
|
365
401
|
}
|
|
366
402
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProcessOperators } from './
|
|
1
|
+
import { ProcessOperators } from './ProcessOperators';
|
|
2
2
|
import { requestOperatorHeader } from './request-operator-header';
|
|
3
3
|
|
|
4
4
|
import type { FetchRequest } from '../client';
|
|
@@ -22,7 +22,7 @@ export class HttpRequestHandler<T extends FetchRequest = FetchRequest> extends P
|
|
|
22
22
|
*/
|
|
23
23
|
setHeader(key: string, value: string): HttpRequestHandler<T> {
|
|
24
24
|
const operator = requestOperatorHeader<T>(key, value);
|
|
25
|
-
return this.set(
|
|
25
|
+
return this.set(`header-${key}`, operator) as HttpRequestHandler<T>;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -32,6 +32,7 @@ export class ProcessOperators<T> implements IProcessOperators<T> {
|
|
|
32
32
|
* It can be either an instance of IProcessOperators<T> or a record of string keys and ProcessOperator<T> values.
|
|
33
33
|
*/
|
|
34
34
|
constructor(operators?: IProcessOperators<T> | Record<string, ProcessOperator<T>>) {
|
|
35
|
+
// accept either a raw operators record or another IProcessOperators instance to clone from
|
|
35
36
|
if (operators && 'operators' in operators) {
|
|
36
37
|
this._operators = { ...operators.operators };
|
|
37
38
|
} else {
|
|
@@ -47,6 +48,7 @@ export class ProcessOperators<T> implements IProcessOperators<T> {
|
|
|
47
48
|
* @throws Error if an operator with the same key already exists.
|
|
48
49
|
*/
|
|
49
50
|
add(key: string, operator: ProcessOperator<T>): ProcessOperators<T> {
|
|
51
|
+
// guard against silently overwriting an existing operator under the same key
|
|
50
52
|
if (Object.keys(this._operators).includes(key))
|
|
51
53
|
throw Error(`Operator [${key}] already defined`);
|
|
52
54
|
return this.set(key, operator);
|
|
@@ -94,6 +96,7 @@ export class ProcessOperators<T> implements IProcessOperators<T> {
|
|
|
94
96
|
if (!operators.length) {
|
|
95
97
|
return of(request);
|
|
96
98
|
}
|
|
99
|
+
// feed the request through each operator sequentially, keeping the previous value when one returns void
|
|
97
100
|
return from(Object.values(this._operators)).pipe(
|
|
98
101
|
mergeScan(
|
|
99
102
|
// resolve current operator and return result or previous if void
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { requestMethodCasing } from './fetch-request.
|
|
1
|
+
import { requestMethodCasing } from './fetch-request.schemas';
|
|
2
2
|
import type { ProcessOperator } from './types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -14,7 +14,9 @@ export const capitalizeRequestMethodOperator =
|
|
|
14
14
|
|
|
15
15
|
request.method = success ? data : request.method?.toUpperCase();
|
|
16
16
|
|
|
17
|
+
// surface schema validation issues as warnings when not running silently
|
|
17
18
|
if (error && !options?.silent) {
|
|
19
|
+
// one warning per issue so callers can see exactly what failed to validate
|
|
18
20
|
for (const e of error.issues) {
|
|
19
21
|
console.warn(e.message);
|
|
20
22
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { HttpRequestHandler } from './
|
|
2
|
-
export { HttpResponseHandler } from './
|
|
3
|
-
export { ProcessOperators } from './
|
|
4
|
-
export { capitalizeRequestMethodOperator } from './capitalize-request-method
|
|
5
|
-
export { requestValidationOperator } from './request-validation
|
|
6
|
-
export { sseMap } from './sse.operator';
|
|
1
|
+
export { HttpRequestHandler } from './HttpRequestHandler';
|
|
2
|
+
export { HttpResponseHandler } from './HttpResponseHandler';
|
|
3
|
+
export { ProcessOperators } from './ProcessOperators';
|
|
4
|
+
export { capitalizeRequestMethodOperator } from './capitalize-request-method-operator';
|
|
5
|
+
export { requestValidationOperator } from './request-validation-operator';
|
|
6
|
+
export { sseMap } from './sse-map.operator';
|
|
7
7
|
|
|
8
8
|
export * from './types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
2
|
import type { ProcessOperator } from './types';
|
|
3
3
|
import type { FetchRequest } from '../client/types';
|
|
4
|
-
import { fetchRequestSchema } from './fetch-request.
|
|
4
|
+
import { fetchRequestSchema } from './fetch-request.schemas';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Validates the given request using the `requestInitSchema`.
|
|
@@ -42,6 +42,7 @@ export const requestValidationOperator =
|
|
|
42
42
|
const result = schema.parse(request) as T;
|
|
43
43
|
return parse ? result : void 0;
|
|
44
44
|
} catch (error) {
|
|
45
|
+
// re-throw so callers relying on `parse` can react to validation failures directly
|
|
45
46
|
if (parse) {
|
|
46
47
|
throw error;
|
|
47
48
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
createSseSelector,
|
|
5
5
|
type ServerSentEvent,
|
|
6
6
|
type SseSelectorOptions,
|
|
7
|
-
} from '../selectors/sse-selector';
|
|
7
|
+
} from '../selectors/create-sse-selector';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* An operator function for handling Server-Sent Events (SSE) in an RxJS pipeline.
|
|
@@ -39,6 +39,7 @@ export const sseMap =
|
|
|
39
39
|
options?: SseSelectorOptions<R>,
|
|
40
40
|
): OperatorFunction<T, ServerSentEvent<R>> =>
|
|
41
41
|
(source) =>
|
|
42
|
+
// parse each raw Response into a stream of typed server-sent events
|
|
42
43
|
source.pipe(switchMap(createSseSelector<R, T>(options)));
|
|
43
44
|
|
|
44
45
|
export default sseMap;
|
|
@@ -11,6 +11,7 @@ import type { FetchRequest } from '../client';
|
|
|
11
11
|
* @param request The input request to be processed.
|
|
12
12
|
* @returns The transformed request, `void`, or a Promise that resolves to the transformed request or `void`.
|
|
13
13
|
*/
|
|
14
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: `void` here relies on TypeScript's special-cased "void-returning callback accepts any return value" behavior for process operators \u2014 `undefined` would break assignability of operator functions that return a transformed request
|
|
14
15
|
export type ProcessOperator<T, R = T> = (request: T) => R | void | Promise<R | void>;
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -10,8 +10,8 @@ import type { ResponseSelector, BlobResult } from '../client/types';
|
|
|
10
10
|
export const blobSelector: ResponseSelector = async <TResponse extends Response = Response>(
|
|
11
11
|
response: TResponse,
|
|
12
12
|
): Promise<BlobResult> => {
|
|
13
|
+
// treat any non-2xx response as a failure rather than attempting to read its body
|
|
13
14
|
if (!response.ok) {
|
|
14
|
-
// Throw an error if the network response is not successful
|
|
15
15
|
throw new Error('network response was not OK');
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -21,6 +21,7 @@ export const blobSelector: ResponseSelector = async <TResponse extends Response
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Extract the filename from the 'content-disposition' header
|
|
24
|
+
// locate the segment carrying the filename directive; other directives (e.g. inline/attachment) are ignored
|
|
24
25
|
const filename = response.headers
|
|
25
26
|
.get('content-disposition')
|
|
26
27
|
?.split(';')
|
|
@@ -32,7 +33,7 @@ export const blobSelector: ResponseSelector = async <TResponse extends Response
|
|
|
32
33
|
// Convert the response to a Blob and return the filename and Blob
|
|
33
34
|
const blob = await response.blob();
|
|
34
35
|
return { filename, blob };
|
|
35
|
-
} catch (
|
|
36
|
+
} catch (_err) {
|
|
36
37
|
// Throw an error if there's a problem parsing the response
|
|
37
38
|
throw Error('failed to parse response');
|
|
38
39
|
}
|
|
@@ -2,7 +2,7 @@ import { EMPTY, from, fromEvent, type Observable } from 'rxjs';
|
|
|
2
2
|
import { finalize, takeUntil } from 'rxjs/operators';
|
|
3
3
|
|
|
4
4
|
import type { ResponseSelector } from '../client/types.js';
|
|
5
|
-
import { ServerSentEventResponseError } from '../../errors.js';
|
|
5
|
+
import { ServerSentEventResponseError } from '../../errors/index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A type representing a function that parses a string into a specific data type.
|
|
@@ -139,31 +139,41 @@ async function* readStream<TData>(
|
|
|
139
139
|
|
|
140
140
|
const decoder = new TextDecoder();
|
|
141
141
|
|
|
142
|
+
// keep reading chunks from the stream until the reader signals completion
|
|
142
143
|
while (true) {
|
|
143
144
|
const { done, value } = await reader.read();
|
|
145
|
+
// the underlying stream has ended, nothing more to read
|
|
144
146
|
if (done) {
|
|
147
|
+
// exit the read loop rather than yielding any further events
|
|
145
148
|
break;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
const text = decoder.decode(value, { stream: true });
|
|
149
152
|
const events = parseEvents<TData>(text, { dataParser: options?.dataParser });
|
|
153
|
+
// a chunk may contain multiple complete SSE events, emit each in turn
|
|
150
154
|
for (const event of events) {
|
|
155
|
+
// a retry directive reconfigures the reconnection delay rather than being a data event
|
|
151
156
|
if (event.retry) {
|
|
152
157
|
await new Promise((resolve) =>
|
|
153
158
|
setTimeout(resolve, Number.parseInt(event.retry ?? '300', 10)),
|
|
154
159
|
);
|
|
160
|
+
// nothing to yield for a retry-only event
|
|
155
161
|
continue;
|
|
156
162
|
}
|
|
163
|
+
// heartbeat filtering is opt-in via the skipHeartbeats option
|
|
157
164
|
if (skipHeartbeats) {
|
|
158
165
|
// Skip comment-based heartbeats (no event, data, or id)
|
|
159
166
|
if (!event.event && !event.data && !event.id) {
|
|
167
|
+
// this event carries no payload, so treat it as a heartbeat and skip it
|
|
160
168
|
continue;
|
|
161
169
|
}
|
|
162
170
|
// Skip named heartbeat events (e.g., event: heartbeat or event: ping)
|
|
163
171
|
if (event.event && ['heartbeat', 'ping'].includes(event.event)) {
|
|
172
|
+
// an explicitly named heartbeat/ping event should also be skipped
|
|
164
173
|
continue;
|
|
165
174
|
}
|
|
166
175
|
}
|
|
176
|
+
// only emit events that pass the caller-provided event type filter, if any
|
|
167
177
|
if (!eventFilter || (event.event && eventFilter.includes(event.event))) {
|
|
168
178
|
yield event as ServerSentEvent<TData>;
|
|
169
179
|
}
|
|
@@ -225,36 +235,43 @@ export const createSseSelector = <TData = unknown, TResponse extends Response =
|
|
|
225
235
|
options?: SseSelectorOptions<TData>,
|
|
226
236
|
): SseSelector<TData, TResponse> => {
|
|
227
237
|
return (response: TResponse): Observable<ServerSentEvent<TData>> => {
|
|
238
|
+
// an unsuccessful HTTP status means there is no valid SSE stream to read
|
|
228
239
|
if (!response.ok) {
|
|
229
240
|
throw new ServerSentEventResponseError(`HTTP error! Status: ${response.status}`, response);
|
|
230
241
|
}
|
|
231
242
|
|
|
243
|
+
// a missing body means there is nothing to stream from
|
|
232
244
|
if (!response.body) {
|
|
233
245
|
throw new ServerSentEventResponseError('Response body is not readable', response);
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
// guard against consuming a non-SSE response as if it were an event stream
|
|
236
249
|
if (!response.headers.get('Content-Type')?.includes('text/event-stream')) {
|
|
237
250
|
throw new ServerSentEventResponseError('Response is not a text/event-stream', response);
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
const reader = response.body.getReader();
|
|
241
254
|
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
return (
|
|
256
|
+
from(
|
|
257
|
+
readStream<TData>(reader, {
|
|
258
|
+
dataParser: options?.dataParser,
|
|
259
|
+
skipHeartbeats: options?.skipHeartbeats,
|
|
260
|
+
eventFilter: options?.eventFilter,
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
// stop the stream on abort and always release the underlying reader when done
|
|
264
|
+
.pipe(
|
|
265
|
+
// Stop reading if the abort signal is triggered
|
|
266
|
+
takeUntil(options?.abortSignal ? fromEvent(options.abortSignal, 'abort') : EMPTY),
|
|
267
|
+
finalize(async () => {
|
|
268
|
+
// cancel just in case of a pre-mature exit
|
|
269
|
+
await reader.cancel().catch(() => {
|
|
270
|
+
/** ignore cancellation errors */
|
|
271
|
+
});
|
|
272
|
+
reader.releaseLock();
|
|
273
|
+
}),
|
|
274
|
+
)
|
|
258
275
|
);
|
|
259
276
|
};
|
|
260
277
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { jsonSelector } from './json-selector';
|
|
2
2
|
export { blobSelector } from './blob-selector';
|
|
3
|
-
export { createSseSelector } from './sse-selector';
|
|
3
|
+
export { createSseSelector } from './create-sse-selector';
|
|
4
4
|
|
|
5
5
|
export type { ResponseSelector } from '../client/types';
|
|
6
6
|
export type {
|
|
@@ -8,4 +8,4 @@ export type {
|
|
|
8
8
|
ServerSentEvent,
|
|
9
9
|
SseSelector,
|
|
10
10
|
SseSelectorOptions,
|
|
11
|
-
} from './sse-selector';
|
|
11
|
+
} from './create-sse-selector';
|
package/src/module.ts
CHANGED
|
@@ -77,15 +77,18 @@ export const module: HttpMsalModule = {
|
|
|
77
77
|
requireInstance,
|
|
78
78
|
}): Promise<HttpClientProvider<HttpClientMsal>> => {
|
|
79
79
|
const httpProvider = new HttpClientProvider(config);
|
|
80
|
+
// wire up an MSAL bearer-token handler only when the auth module is registered
|
|
80
81
|
if (hasModule('auth')) {
|
|
81
82
|
const authProvider = await requireInstance('auth');
|
|
82
83
|
httpProvider.defaultHttpRequestHandler.set('MSAL', async (request) => {
|
|
83
84
|
const { scopes = [] } = request;
|
|
85
|
+
// only attempt to acquire a token when the request actually declares scopes
|
|
84
86
|
if (scopes.length) {
|
|
85
|
-
/** TODO should be try catch, check caller for handling */
|
|
87
|
+
/** TODO(#5143): should be try catch, check caller for handling */
|
|
86
88
|
const accessToken = await authProvider.acquireAccessToken({
|
|
87
89
|
request: { scopes },
|
|
88
90
|
});
|
|
91
|
+
// without a token there's nothing to attach, fall through to the default request
|
|
89
92
|
if (accessToken) {
|
|
90
93
|
const headers = new Headers(request.headers);
|
|
91
94
|
headers.set('Authorization', `Bearer ${accessToken}`);
|
package/src/provider.ts
CHANGED
|
@@ -9,14 +9,7 @@ import type { IHttpRequestHandler } from './lib/operators';
|
|
|
9
9
|
import type { IHttpClient } from './lib/client';
|
|
10
10
|
import { BaseModuleProvider } from '@equinor/fusion-framework-module/provider';
|
|
11
11
|
import { version } from './version';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Thrown when `createClient(name)` is called with an unknown client key.
|
|
15
|
-
*
|
|
16
|
-
* This is only used when the provided string is neither a registered client name
|
|
17
|
-
* nor an absolute `http:` or `https:` URL.
|
|
18
|
-
*/
|
|
19
|
-
export class ClientNotFoundException extends Error {}
|
|
12
|
+
import { ClientNotFoundException } from './errors/index.js';
|
|
20
13
|
|
|
21
14
|
/**
|
|
22
15
|
* Creates fresh HTTP client instances from named or ad-hoc configuration.
|
|
@@ -105,6 +98,10 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
105
98
|
return this.config.defaultHttpRequestHandler;
|
|
106
99
|
}
|
|
107
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Creates a new `HttpClientProvider`.
|
|
103
|
+
* @param config - The configurator providing client definitions and defaults.
|
|
104
|
+
*/
|
|
108
105
|
constructor(protected config: IHttpClientConfigurator<TClient>) {
|
|
109
106
|
super({
|
|
110
107
|
version,
|
|
@@ -149,6 +146,7 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
149
146
|
} = config as HttpClientOptions<TClient>;
|
|
150
147
|
const options = { requestHandler, responseHandler };
|
|
151
148
|
const instance = new ctor(baseUri || '', options) as TClient;
|
|
149
|
+
// attach the resolved default scopes onto the instance without overwriting other own properties
|
|
152
150
|
Object.assign(instance, { defaultScopes });
|
|
153
151
|
onCreate?.(instance as TClient);
|
|
154
152
|
return instance as TClient;
|
|
@@ -157,6 +155,7 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
157
155
|
/**
|
|
158
156
|
* Creates a client instance and returns it as the requested custom client type.
|
|
159
157
|
*
|
|
158
|
+
* @template T - The custom `HttpClient` implementation type to cast the created instance to.
|
|
160
159
|
* @param key - The key of the pre-configured HTTP client to create.
|
|
161
160
|
* @returns The created HTTP client instance, cast to the specified type `T`.
|
|
162
161
|
*
|
|
@@ -167,6 +166,8 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
167
166
|
* is configured to use a different implementation.
|
|
168
167
|
*/
|
|
169
168
|
public createCustomClient<T extends HttpClient>(key: string): T {
|
|
169
|
+
// `createClient` always returns the provider's configured `HttpClient` implementation —
|
|
170
|
+
// cast through `unknown` to hand back the caller-requested implementation type `T`.
|
|
170
171
|
return this.createClient(key) as unknown as T;
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -179,15 +180,20 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
179
180
|
*
|
|
180
181
|
* @param keyOrConfig - The key or configuration object for the HTTP client.
|
|
181
182
|
* @returns The resolved HTTP client configuration.
|
|
183
|
+
* @throws {ClientNotFoundException} When `keyOrConfig` is a string that is neither a registered
|
|
184
|
+
* client key nor a URL-like value.
|
|
182
185
|
*/
|
|
183
186
|
protected _resolveConfig(
|
|
184
187
|
keyOrConfig: string | HttpClientOptions<TClient>,
|
|
185
188
|
): HttpClientOptions<TClient> {
|
|
189
|
+
// a string may reference a registered client key or an ad-hoc URL; anything else is used as-is
|
|
186
190
|
if (typeof keyOrConfig === 'string') {
|
|
187
191
|
const config = this.config.clients[keyOrConfig];
|
|
192
|
+
// an absolute http(s) URL can be used directly as an ad-hoc baseUri
|
|
188
193
|
if (!config && isURL(keyOrConfig)) {
|
|
189
194
|
return { baseUri: keyOrConfig };
|
|
190
195
|
} else if (!config && looksLikeURL(keyOrConfig)) {
|
|
196
|
+
// recover from a missing protocol instead of failing outright, but warn so it can be fixed
|
|
191
197
|
console.warn(
|
|
192
198
|
`[HttpClientProvider] "${keyOrConfig}" looks like a URL but is missing the http:// or https:// protocol. ` +
|
|
193
199
|
`Treating it as "https://${keyOrConfig}". ` +
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '8.0.
|
|
2
|
+
export const version = '8.0.4';
|
package/tests/HttpClient.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { lastValueFrom } from 'rxjs';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { Errors, HttpClientProvider } from '../src';
|
|
5
5
|
import { HttpClientConfigurator } from '../src/configurator';
|
|
6
6
|
import { HttpClient, HttpClientMsal } from '../src/lib';
|
|
7
7
|
import { HttpResponseHandler } from '../src/lib/operators';
|
|
@@ -99,7 +99,7 @@ describe('HttpClient', () => {
|
|
|
99
99
|
|
|
100
100
|
it('should throw when a configured client cannot be found', () => {
|
|
101
101
|
expect(() => provider.createClient('missing-client')).toThrowError(
|
|
102
|
-
new ClientNotFoundException('No registered http client for key [missing-client]'),
|
|
102
|
+
new Errors.ClientNotFoundException('No registered http client for key [missing-client]'),
|
|
103
103
|
);
|
|
104
104
|
});
|
|
105
105
|
|
package/tests/operators.test.ts
CHANGED
|
@@ -126,7 +126,11 @@ describe('sseMap', () => {
|
|
|
126
126
|
},
|
|
127
127
|
);
|
|
128
128
|
|
|
129
|
-
const events = await lastValueFrom(
|
|
129
|
+
const events = await lastValueFrom(
|
|
130
|
+
of(response)
|
|
131
|
+
// parse the SSE stream and collect every emitted event into a single array
|
|
132
|
+
.pipe(sseMap<{ key: string }>(), toArray()),
|
|
133
|
+
);
|
|
130
134
|
|
|
131
135
|
expect(events).toEqual([{ data: { key: 'value' } }]);
|
|
132
136
|
});
|