@equinor/fusion-framework-module-http 8.0.2 → 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.
Files changed (111) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/esm/configurator.js +7 -1
  3. package/dist/esm/configurator.js.map +1 -1
  4. package/dist/esm/errors/ClientNotFoundException.js +10 -0
  5. package/dist/esm/errors/ClientNotFoundException.js.map +1 -0
  6. package/dist/esm/errors/HttpJsonResponseError.js +26 -0
  7. package/dist/esm/errors/HttpJsonResponseError.js.map +1 -0
  8. package/dist/esm/errors/HttpResponseError.js +19 -0
  9. package/dist/esm/errors/HttpResponseError.js.map +1 -0
  10. package/dist/esm/errors/ServerSentEventResponseError.js +25 -0
  11. package/dist/esm/errors/ServerSentEventResponseError.js.map +1 -0
  12. package/dist/esm/errors/index.js +5 -0
  13. package/dist/esm/errors/index.js.map +1 -0
  14. package/dist/esm/index.js +2 -1
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/lib/client/client-msal.js +1 -0
  17. package/dist/esm/lib/client/client-msal.js.map +1 -1
  18. package/dist/esm/lib/client/client.js +40 -4
  19. package/dist/esm/lib/client/client.js.map +1 -1
  20. package/dist/esm/lib/operators/{http-request-handler.js → HttpRequestHandler.js} +3 -3
  21. package/dist/esm/lib/operators/HttpRequestHandler.js.map +1 -0
  22. package/dist/esm/lib/operators/{http-response-handler.js → HttpResponseHandler.js} +2 -2
  23. package/dist/esm/lib/operators/HttpResponseHandler.js.map +1 -0
  24. package/dist/esm/lib/operators/{process-operators.js → ProcessOperators.js} +4 -1
  25. package/dist/esm/lib/operators/ProcessOperators.js.map +1 -0
  26. package/dist/esm/lib/operators/{capitalize-request-method.operator.js → capitalize-request-method-operator.js} +4 -2
  27. package/dist/esm/lib/operators/capitalize-request-method-operator.js.map +1 -0
  28. package/dist/esm/lib/operators/{fetch-request.schema.js → fetch-request.schemas.js} +1 -1
  29. package/dist/esm/lib/operators/fetch-request.schemas.js.map +1 -0
  30. package/dist/esm/lib/operators/index.js +6 -6
  31. package/dist/esm/lib/operators/index.js.map +1 -1
  32. package/dist/esm/lib/operators/{request-validation.operator.js → request-validation-operator.js} +3 -2
  33. package/dist/esm/lib/operators/{request-validation.operator.js.map → request-validation-operator.js.map} +1 -1
  34. package/dist/esm/lib/operators/{sse.operator.js → sse-map.operator.js} +5 -3
  35. package/dist/esm/lib/operators/sse-map.operator.js.map +1 -0
  36. package/dist/esm/lib/selectors/blob-selector.js +3 -2
  37. package/dist/esm/lib/selectors/blob-selector.js.map +1 -1
  38. package/dist/esm/lib/selectors/{sse-selector.js → create-sse-selector.js} +20 -5
  39. package/dist/esm/lib/selectors/create-sse-selector.js.map +1 -0
  40. package/dist/esm/lib/selectors/index.js +1 -1
  41. package/dist/esm/lib/selectors/index.js.map +1 -1
  42. package/dist/esm/lib/selectors/json-selector.js +1 -1
  43. package/dist/esm/lib/selectors/json-selector.js.map +1 -1
  44. package/dist/esm/module.js +4 -1
  45. package/dist/esm/module.js.map +1 -1
  46. package/dist/esm/provider.js +14 -8
  47. package/dist/esm/provider.js.map +1 -1
  48. package/dist/esm/version.js +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/dist/types/configurator.d.ts +5 -1
  51. package/dist/types/errors/ClientNotFoundException.d.ts +9 -0
  52. package/dist/types/errors/HttpJsonResponseError.d.ts +23 -0
  53. package/dist/types/errors/HttpResponseError.d.ts +15 -0
  54. package/dist/types/errors/ServerSentEventResponseError.d.ts +23 -0
  55. package/dist/types/errors/index.d.ts +4 -0
  56. package/dist/types/index.d.ts +2 -1
  57. package/dist/types/lib/client/client-msal.d.ts +1 -0
  58. package/dist/types/lib/client/client.d.ts +33 -3
  59. package/dist/types/lib/operators/{http-request-handler.d.ts → HttpRequestHandler.d.ts} +1 -1
  60. package/dist/types/lib/operators/{http-response-handler.d.ts → HttpResponseHandler.d.ts} +1 -1
  61. package/dist/types/lib/operators/index.d.ts +6 -6
  62. package/dist/types/lib/operators/{sse.operator.d.ts → sse-map.operator.d.ts} +1 -1
  63. package/dist/types/lib/selectors/index.d.ts +2 -2
  64. package/dist/types/provider.d.ts +7 -8
  65. package/dist/types/version.d.ts +1 -1
  66. package/package.json +7 -7
  67. package/src/configurator.ts +7 -1
  68. package/src/errors/ClientNotFoundException.ts +9 -0
  69. package/src/errors/HttpJsonResponseError.ts +31 -0
  70. package/src/errors/HttpResponseError.ts +21 -0
  71. package/src/errors/ServerSentEventResponseError.ts +30 -0
  72. package/src/errors/index.ts +4 -0
  73. package/src/index.ts +2 -1
  74. package/src/lib/client/client-msal.ts +1 -0
  75. package/src/lib/client/client.ts +40 -4
  76. package/src/lib/operators/{http-request-handler.ts → HttpRequestHandler.ts} +2 -2
  77. package/src/lib/operators/{http-response-handler.ts → HttpResponseHandler.ts} +1 -1
  78. package/src/lib/operators/{process-operators.ts → ProcessOperators.ts} +3 -0
  79. package/src/lib/operators/{capitalize-request-method.operator.ts → capitalize-request-method-operator.ts} +3 -1
  80. package/src/lib/operators/index.ts +6 -6
  81. package/src/lib/operators/{request-validation.operator.ts → request-validation-operator.ts} +2 -1
  82. package/src/lib/operators/{sse.operator.ts → sse-map.operator.ts} +2 -1
  83. package/src/lib/operators/types.ts +1 -0
  84. package/src/lib/selectors/blob-selector.ts +3 -2
  85. package/src/lib/selectors/{sse-selector.ts → create-sse-selector.ts} +34 -17
  86. package/src/lib/selectors/index.ts +2 -2
  87. package/src/lib/selectors/json-selector.ts +1 -1
  88. package/src/module.ts +4 -1
  89. package/src/provider.ts +14 -8
  90. package/src/version.ts +1 -1
  91. package/tests/HttpClient.test.ts +2 -2
  92. package/tests/operators.test.ts +5 -1
  93. package/tests/sse.selector.test.ts +34 -21
  94. package/vitest.config.ts +1 -1
  95. package/dist/esm/errors.js +0 -58
  96. package/dist/esm/errors.js.map +0 -1
  97. package/dist/esm/lib/operators/capitalize-request-method.operator.js.map +0 -1
  98. package/dist/esm/lib/operators/fetch-request.schema.js.map +0 -1
  99. package/dist/esm/lib/operators/http-request-handler.js.map +0 -1
  100. package/dist/esm/lib/operators/http-response-handler.js.map +0 -1
  101. package/dist/esm/lib/operators/process-operators.js.map +0 -1
  102. package/dist/esm/lib/operators/sse.operator.js.map +0 -1
  103. package/dist/esm/lib/selectors/sse-selector.js.map +0 -1
  104. package/dist/types/errors.d.ts +0 -51
  105. package/src/errors.ts +0 -69
  106. package/dist/types/lib/operators/{process-operators.d.ts → ProcessOperators.d.ts} +0 -0
  107. package/dist/types/lib/operators/{capitalize-request-method.operator.d.ts → capitalize-request-method-operator.d.ts} +0 -0
  108. package/dist/types/lib/operators/{fetch-request.schema.d.ts → fetch-request.schemas.d.ts} +26 -26
  109. /package/dist/types/lib/operators/{request-validation.operator.d.ts → request-validation-operator.d.ts} +0 -0
  110. /package/dist/types/lib/selectors/{sse-selector.d.ts → create-sse-selector.d.ts} +0 -0
  111. /package/src/lib/operators/{fetch-request.schema.ts → fetch-request.schemas.ts} +0 -0
@@ -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
- /** @deprecated */
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
- /** @deprecated */
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 './process-operators';
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('header-' + key, operator) as HttpRequestHandler<T>;
25
+ return this.set(`header-${key}`, operator) as HttpRequestHandler<T>;
26
26
  }
27
27
  }
28
28
 
@@ -1,4 +1,4 @@
1
- import { ProcessOperators } from './process-operators';
1
+ import { ProcessOperators } from './ProcessOperators';
2
2
 
3
3
  /**
4
4
  * The `HttpResponseHandler` class extends the `ProcessOperators` class and is responsible for handling HTTP responses.
@@ -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.schema';
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 './http-request-handler';
2
- export { HttpResponseHandler } from './http-response-handler';
3
- export { ProcessOperators } from './process-operators';
4
- export { capitalizeRequestMethodOperator } from './capitalize-request-method.operator';
5
- export { requestValidationOperator } from './request-validation.operator';
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.schema';
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 (err) {
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 from(
243
- readStream<TData>(reader, {
244
- dataParser: options?.dataParser,
245
- skipHeartbeats: options?.skipHeartbeats,
246
- eventFilter: options?.eventFilter,
247
- }),
248
- ).pipe(
249
- // Stop reading if the abort signal is triggered
250
- takeUntil(options?.abortSignal ? fromEvent(options.abortSignal, 'abort') : EMPTY),
251
- finalize(async () => {
252
- // cancel just in case of a pre-mature exit
253
- await reader.cancel().catch(() => {
254
- /** ignore cancellation errors */
255
- });
256
- reader.releaseLock();
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';
@@ -1,5 +1,5 @@
1
1
  import type { ResponseSelector } from '../client/types';
2
- import { HttpJsonResponseError } from '../../errors';
2
+ import { HttpJsonResponseError } from '../../errors/index.js';
3
3
 
4
4
  /**
5
5
  * Asynchronously parses the JSON data from a given HTTP response.
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';
2
+ export const version = '8.0.4';
@@ -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 { ClientNotFoundException, HttpClientProvider } from '../src';
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
 
@@ -126,7 +126,11 @@ describe('sseMap', () => {
126
126
  },
127
127
  );
128
128
 
129
- const events = await lastValueFrom(of(response).pipe(sseMap<{ key: string }>(), toArray()));
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
  });