@equinor/fusion-framework-module-http 8.1.0-next.0 → 8.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -1321
- package/docs/client-configuration.md +0 -175
- package/docs/observable-patterns.md +0 -103
- package/docs/selectors-and-handlers.md +0 -112
- package/docs/server-sent-events.md +0 -125
- package/docs/testing.md +0 -78
- package/src/configurator.ts +0 -249
- package/src/errors/ClientNotFoundException.ts +0 -9
- package/src/errors/HttpJsonResponseError.ts +0 -32
- package/src/errors/HttpResponseError.ts +0 -21
- package/src/errors/ServerSentEventResponseError.ts +0 -30
- package/src/errors/index.ts +0 -4
- package/src/index.ts +0 -15
- package/src/lib/client/client-msal.ts +0 -80
- package/src/lib/client/client.ts +0 -496
- package/src/lib/client/index.ts +0 -4
- package/src/lib/client/types.ts +0 -244
- package/src/lib/index.ts +0 -3
- package/src/lib/operators/HttpMiddlewareHandler.ts +0 -58
- package/src/lib/operators/HttpRequestHandler.ts +0 -29
- package/src/lib/operators/HttpResponseHandler.ts +0 -11
- package/src/lib/operators/ProcessOperators.ts +0 -113
- package/src/lib/operators/capitalize-request-method-operator.ts +0 -26
- package/src/lib/operators/fetch-request.schemas.ts +0 -104
- package/src/lib/operators/index.ts +0 -9
- package/src/lib/operators/request-operator-header.ts +0 -19
- package/src/lib/operators/request-validation-operator.ts +0 -51
- package/src/lib/operators/sse-map.operator.ts +0 -45
- package/src/lib/operators/types.ts +0 -174
- package/src/lib/selectors/blob-selector.ts +0 -42
- package/src/lib/selectors/create-sse-selector.ts +0 -279
- package/src/lib/selectors/index.ts +0 -11
- package/src/lib/selectors/json-selector.ts +0 -52
- package/src/mock/create-open-api-mock-middleware.ts +0 -40
- package/src/mock/create-router-middleware.ts +0 -158
- package/src/mock/index.ts +0 -26
- package/src/mock/resolve-open-api-mock-response.ts +0 -36
- package/src/module.ts +0 -149
- package/src/provider.ts +0 -225
- package/src/version.ts +0 -2
- package/tests/HttpClient.test.ts +0 -173
- package/tests/HttpMiddlewareHandler.test.ts +0 -58
- package/tests/mock/adapters.test.ts +0 -62
- package/tests/mock/router-middleware.test.ts +0 -135
- package/tests/operators.test.ts +0 -137
- package/tests/sse.selector.test.ts +0 -162
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -12
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { OperatorFunction } from 'rxjs';
|
|
2
|
-
import { switchMap } from 'rxjs/operators';
|
|
3
|
-
import {
|
|
4
|
-
createSseSelector,
|
|
5
|
-
type ServerSentEvent,
|
|
6
|
-
type SseSelectorOptions,
|
|
7
|
-
} from '../selectors/create-sse-selector';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* An operator function for handling Server-Sent Events (SSE) in an RxJS pipeline.
|
|
11
|
-
*
|
|
12
|
-
* @template R - The type of the parsed data from the SSE.
|
|
13
|
-
* @template T - The type of the input `Response` object, defaults to `Response`.
|
|
14
|
-
*
|
|
15
|
-
* @param options - Configuration options for the SSE selector.
|
|
16
|
-
*
|
|
17
|
-
* @returns An `OperatorFunction` that transforms a stream of `Response` objects
|
|
18
|
-
* into a stream of `ServerSentEvent` objects containing parsed data of type `R`.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* import { sseMap } from '@equinor/fusion-framework-module-http/operators';
|
|
23
|
-
* import { fromFetch } from 'rxjs/fetch';
|
|
24
|
-
*
|
|
25
|
-
* const response$ = fromFetch('https://example.com/sse', {
|
|
26
|
-
* method: 'GET',
|
|
27
|
-
* headers: {
|
|
28
|
-
* 'Accept': 'text/event-stream',
|
|
29
|
-
* },
|
|
30
|
-
* }).pipe(
|
|
31
|
-
* sseMap( { /* SSE selector options *\/ })
|
|
32
|
-
* ).subscribe((event) => {
|
|
33
|
-
* console.log(event.data); // Process the parsed SSE data
|
|
34
|
-
* });
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export const sseMap =
|
|
38
|
-
<R = unknown, T extends Response = Response>(
|
|
39
|
-
options?: SseSelectorOptions<R>,
|
|
40
|
-
): OperatorFunction<T, ServerSentEvent<R>> =>
|
|
41
|
-
(source) =>
|
|
42
|
-
// parse each raw Response into a stream of typed server-sent events
|
|
43
|
-
source.pipe(switchMap(createSseSelector<R, T>(options)));
|
|
44
|
-
|
|
45
|
-
export default sseMap;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import type { Observable, ObservableInput } from 'rxjs';
|
|
2
|
-
import type { FetchRequest } from '../client';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A process operator that takes a request of type `T` and returns a transformed request of type `R`, or `void`, or a Promise that resolves to `R` or `void`.
|
|
6
|
-
*
|
|
7
|
-
* Process operators are used to transform or modify requests in a sequential pipeline before they are processed by an `IHttpRequestHandler`.
|
|
8
|
-
*
|
|
9
|
-
* @template T The type of the input request.
|
|
10
|
-
* @template R The type of the output request. Defaults to `T` if not specified.
|
|
11
|
-
* @param request The input request to be processed.
|
|
12
|
-
* @returns The transformed request, `void`, or a Promise that resolves to the transformed request or `void`.
|
|
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
|
|
15
|
-
export type ProcessOperator<T, R = T> = (request: T) => R | void | Promise<R | void>;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Represents a collection of process operators.
|
|
19
|
-
* @template T The type of the request being processed.
|
|
20
|
-
*/
|
|
21
|
-
export interface IProcessOperators<T> {
|
|
22
|
-
/**
|
|
23
|
-
* Gets the operators registered in the collection.
|
|
24
|
-
*/
|
|
25
|
-
get operators(): Record<string, ProcessOperator<T>>;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Adds a new operator to the collection.
|
|
29
|
-
* @param key The key to identify the operator.
|
|
30
|
-
* @param operator The process operator to add.
|
|
31
|
-
* @returns The updated collection of process operators.
|
|
32
|
-
* @throws An error if the operator is already defined.
|
|
33
|
-
*/
|
|
34
|
-
add(key: string, operator: ProcessOperator<T>): IProcessOperators<T>;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Adds or sets a process operator in the collection.
|
|
38
|
-
* @param key The key to identify the operator.
|
|
39
|
-
* @param operator The process operator to add or set.
|
|
40
|
-
* @returns The updated collection of process operators.
|
|
41
|
-
*/
|
|
42
|
-
set(key: string, operator: ProcessOperator<T>): IProcessOperators<T>;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Removes a process operator from the collection.
|
|
46
|
-
* @param key The key of the operator to remove.
|
|
47
|
-
* @returns The updated collection of process operators.
|
|
48
|
-
*/
|
|
49
|
-
remove(key: string): IProcessOperators<T>;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Gets a process operator from the collection.
|
|
53
|
-
* @param key The key of the operator to retrieve.
|
|
54
|
-
* @returns The process operator associated with the key, or undefined if the key is invalid.
|
|
55
|
-
*/
|
|
56
|
-
get(key: string): ProcessOperator<T>;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Processes the registered process operators.
|
|
60
|
-
* @param request The request to process.
|
|
61
|
-
* @returns An observable that emits the processed request.
|
|
62
|
-
*/
|
|
63
|
-
process(request: T): Observable<T>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Represents an HTTP request handler that extends the `IProcessOperators` interface.
|
|
68
|
-
* This interface provides methods to manage and process HTTP request operators.
|
|
69
|
-
*
|
|
70
|
-
* @template T - The type of the request being processed. Defaults to `FetchRequest`.
|
|
71
|
-
*/
|
|
72
|
-
export interface IHttpRequestHandler<T extends FetchRequest = FetchRequest>
|
|
73
|
-
extends IProcessOperators<T> {
|
|
74
|
-
/**
|
|
75
|
-
* Set header that will apply on all requests done by consumer @see {HttpClient}
|
|
76
|
-
* @param key - name of header
|
|
77
|
-
* @param value - header value
|
|
78
|
-
*/
|
|
79
|
-
setHeader(key: string, value: string): IHttpRequestHandler<T>;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Represents an HTTP response handler that extends the `IProcessOperators` interface.
|
|
84
|
-
* This interface provides methods to manage and process HTTP response operators.
|
|
85
|
-
*
|
|
86
|
-
* @template T - The type of the response being processed. Defaults to `Response`.
|
|
87
|
-
*/
|
|
88
|
-
export interface IHttpResponseHandler<T = Response> extends IProcessOperators<T> {}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Continues an HTTP request by resolving the given (already-processed) request into a response.
|
|
92
|
-
*
|
|
93
|
-
* @remarks
|
|
94
|
-
* The terminal `next` passed to the outermost {@link HttpMiddleware} ultimately resolves to
|
|
95
|
-
* `HttpClient._performFetch` — the same overridable seam the mock system replaces — so
|
|
96
|
-
* middleware wraps around either the real network call or a mocked one transparently.
|
|
97
|
-
*
|
|
98
|
-
* @param uri - The fully resolved URL for the request.
|
|
99
|
-
* @param init - The prepared `fetch` request options.
|
|
100
|
-
* @returns The resulting `Response`, or an observable input of it.
|
|
101
|
-
*/
|
|
102
|
-
export type HttpMiddlewareNext = (
|
|
103
|
-
uri: string,
|
|
104
|
-
init: RequestInit,
|
|
105
|
-
) => Response | ObservableInput<Response>;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Continues to the next registered {@link HttpMiddleware}, or the network call itself,
|
|
109
|
-
* always resolving to a `Response` regardless of how that next step actually produced it —
|
|
110
|
-
* a short-circuited `Response`, a `Promise`, or an `Observable`.
|
|
111
|
-
*
|
|
112
|
-
* @param uri - The fully resolved URL for the request.
|
|
113
|
-
* @param init - The prepared `fetch` request options.
|
|
114
|
-
* @returns A promise of the resulting `Response`.
|
|
115
|
-
*/
|
|
116
|
-
export type HttpMiddlewareContinuation = (uri: string, init: RequestInit) => Promise<Response>;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Wraps request execution to add cross-cutting behavior — retries, caching, telemetry,
|
|
120
|
-
* circuit breaking — around the network call itself, rather than transforming the
|
|
121
|
-
* request or response payload.
|
|
122
|
-
*
|
|
123
|
-
* @remarks
|
|
124
|
-
* Unlike {@link ProcessOperator}, which transforms a value in a linear pipeline, a middleware
|
|
125
|
-
* controls whether and how many times `next` runs: it can short-circuit by never calling
|
|
126
|
-
* `next`, retry by calling it more than once, or recover from a rejection it throws.
|
|
127
|
-
* Registered middleware compose in an "onion" — the first one registered is outermost, so it
|
|
128
|
-
* sees the request first and the response last.
|
|
129
|
-
*
|
|
130
|
-
* @param uri - The fully resolved URL for the request.
|
|
131
|
-
* @param init - The prepared `fetch` request options.
|
|
132
|
-
* @param next - Continues to the next registered middleware, or the network call itself.
|
|
133
|
-
* @returns The resulting `Response`, or an observable input of it.
|
|
134
|
-
*
|
|
135
|
-
* @example Retry once on a failed response
|
|
136
|
-
* ```typescript
|
|
137
|
-
* const retryOnce: HttpMiddleware = async (uri, init, next) => {
|
|
138
|
-
* const response = await next(uri, init);
|
|
139
|
-
* return response.ok ? response : next(uri, init);
|
|
140
|
-
* };
|
|
141
|
-
* ```
|
|
142
|
-
*/
|
|
143
|
-
export type HttpMiddleware = (
|
|
144
|
-
uri: string,
|
|
145
|
-
init: RequestInit,
|
|
146
|
-
next: HttpMiddlewareContinuation,
|
|
147
|
-
) => Response | ObservableInput<Response>;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Registers and composes {@link HttpMiddleware} into a single execution pipeline wrapping
|
|
151
|
-
* the network call.
|
|
152
|
-
*/
|
|
153
|
-
export interface IHttpMiddlewareHandler {
|
|
154
|
-
/**
|
|
155
|
-
* Gets the registered middleware, in registration order.
|
|
156
|
-
*/
|
|
157
|
-
get middleware(): readonly HttpMiddleware[];
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Registers a middleware, wrapping every middleware registered before it.
|
|
161
|
-
* @param middleware - The middleware to register.
|
|
162
|
-
* @returns The updated handler, for chaining.
|
|
163
|
-
*/
|
|
164
|
-
use(middleware: HttpMiddleware): IHttpMiddlewareHandler;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Runs the registered middleware chain around a request, ending with `terminal`.
|
|
168
|
-
* @param uri - The fully resolved URL for the request.
|
|
169
|
-
* @param init - The prepared `fetch` request options.
|
|
170
|
-
* @param terminal - The innermost step the chain wraps, called when every middleware defers to `next`.
|
|
171
|
-
* @returns An observable of the resulting `Response`.
|
|
172
|
-
*/
|
|
173
|
-
process(uri: string, init: RequestInit, terminal: HttpMiddlewareNext): Observable<Response>;
|
|
174
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { ResponseSelector, BlobResult } from '../client/types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extracts a blob and filename from a successful HTTP response.
|
|
5
|
-
*
|
|
6
|
-
* @param response - The HTTP response to extract the blob and filename from.
|
|
7
|
-
* @returns A promise that resolves to an object containing the extracted blob and filename.
|
|
8
|
-
* @throws {Error} If the response is not successful or if there is an error parsing the response.
|
|
9
|
-
*/
|
|
10
|
-
export const blobSelector: ResponseSelector = async <TResponse extends Response = Response>(
|
|
11
|
-
response: TResponse,
|
|
12
|
-
): Promise<BlobResult> => {
|
|
13
|
-
// treat any non-2xx response as a failure rather than attempting to read its body
|
|
14
|
-
if (!response.ok) {
|
|
15
|
-
throw new Error('network response was not OK');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Status code 204 indicates no content, so throw an error
|
|
19
|
-
if (response.status === 204) {
|
|
20
|
-
throw new Error('no content');
|
|
21
|
-
}
|
|
22
|
-
|
|
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
|
|
25
|
-
const filename = response.headers
|
|
26
|
-
.get('content-disposition')
|
|
27
|
-
?.split(';')
|
|
28
|
-
.find((n) => n.includes('filename='))
|
|
29
|
-
?.replace('filename=', '')
|
|
30
|
-
?.trim();
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Convert the response to a Blob and return the filename and Blob
|
|
34
|
-
const blob = await response.blob();
|
|
35
|
-
return { filename, blob };
|
|
36
|
-
} catch (_err) {
|
|
37
|
-
// Throw an error if there's a problem parsing the response
|
|
38
|
-
throw Error('failed to parse response');
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export default blobSelector;
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { EMPTY, from, fromEvent, type Observable } from 'rxjs';
|
|
2
|
-
import { finalize, takeUntil } from 'rxjs/operators';
|
|
3
|
-
|
|
4
|
-
import type { ResponseSelector } from '../client/types.js';
|
|
5
|
-
import { ServerSentEventResponseError } from '../../errors/index.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A type representing a function that parses a string into a specific data type.
|
|
9
|
-
*
|
|
10
|
-
* @typeParam TData - The type of the parsed data. Defaults to `unknown` if not specified.
|
|
11
|
-
* @param data - The string input to be parsed.
|
|
12
|
-
* @returns The parsed data of type `TData`.
|
|
13
|
-
*/
|
|
14
|
-
export type DataParser<TData = unknown> = (data: string) => TData;
|
|
15
|
-
|
|
16
|
-
const defaultDataParser: DataParser = (data: string) => {
|
|
17
|
-
try {
|
|
18
|
-
return JSON.parse(data);
|
|
19
|
-
} catch {
|
|
20
|
-
return data;
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Represents a Server-Sent Event (SSE) with optional fields.
|
|
26
|
-
*/
|
|
27
|
-
export type ServerSentEvent<TData = unknown> = {
|
|
28
|
-
id?: string;
|
|
29
|
-
event?: string;
|
|
30
|
-
data?: TData;
|
|
31
|
-
retry?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Parses a string containing Server-Sent Events (SSE) data into individual event objects.
|
|
36
|
-
*
|
|
37
|
-
* @template TData - The type of the parsed `data` field in the event.
|
|
38
|
-
* @param text - The raw SSE data as a string, where events are separated by double newlines.
|
|
39
|
-
* @param options - Optional configuration for parsing the events.
|
|
40
|
-
* @param options.dataParser - A custom parser function for the `data` field. Defaults to JSON parsing,
|
|
41
|
-
* falling back to returning the raw string if parsing fails.
|
|
42
|
-
*
|
|
43
|
-
* @returns A generator that yields `ServerSentEvent` objects parsed from the input string.
|
|
44
|
-
*
|
|
45
|
-
* @remarks
|
|
46
|
-
* - Empty lines and events with no fields are ignored.
|
|
47
|
-
* - If the `data` field cannot be parsed as JSON, it is returned as a plain string.
|
|
48
|
-
* - Fields other than `data` are stored as strings in the resulting `ServerSentEvent` object.
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```typescript
|
|
52
|
-
* const sseData = `
|
|
53
|
-
* id: 1
|
|
54
|
-
* event: message
|
|
55
|
-
* data: {"key":"value"}
|
|
56
|
-
*
|
|
57
|
-
* id: 2
|
|
58
|
-
* event: update
|
|
59
|
-
* data: plain text
|
|
60
|
-
* `;
|
|
61
|
-
*
|
|
62
|
-
* for (const event of parseEvents(sseData)) {
|
|
63
|
-
* console.log(event);
|
|
64
|
-
* }
|
|
65
|
-
* // Output:
|
|
66
|
-
* // { id: "1", event: "message", data: { key: "value" } }
|
|
67
|
-
* // { id: "2", event: "update", data: "plain text" }
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
function* parseEvents<TData = unknown>(
|
|
71
|
-
text: string,
|
|
72
|
-
options?: { dataParser?: DataParser<TData> },
|
|
73
|
-
): Generator<ServerSentEvent<TData>> {
|
|
74
|
-
// Split the input string into individual event strings using double newline as separator
|
|
75
|
-
const eventStrings = text.split('\n\n');
|
|
76
|
-
|
|
77
|
-
const dataParser = options?.dataParser || (defaultDataParser as DataParser<TData>);
|
|
78
|
-
|
|
79
|
-
// Iterate through each event string
|
|
80
|
-
for (const eventStr of eventStrings) {
|
|
81
|
-
// Skip empty event strings (after trimming whitespace)
|
|
82
|
-
if (!eventStr.trim()) continue;
|
|
83
|
-
|
|
84
|
-
// Split the event string into lines (fields) using single newline
|
|
85
|
-
const lines = eventStr.split('\n');
|
|
86
|
-
|
|
87
|
-
// Use reduce to process each line in the event string
|
|
88
|
-
const event = lines.reduce(
|
|
89
|
-
(event, line) => {
|
|
90
|
-
// Skip empty lines
|
|
91
|
-
if (!line) return event;
|
|
92
|
-
|
|
93
|
-
// Find the index of the first colon, which separates field name and value
|
|
94
|
-
const colonIndex = line.indexOf(':');
|
|
95
|
-
|
|
96
|
-
// Skip lines without a colon (invalid format)
|
|
97
|
-
if (colonIndex === -1) return event;
|
|
98
|
-
|
|
99
|
-
// Extract the field name (before colon) and trim whitespace
|
|
100
|
-
const field = line.slice(0, colonIndex).trim();
|
|
101
|
-
// Extract the field value (after colon) and trim whitespace
|
|
102
|
-
const value = line.slice(colonIndex + 1).trim();
|
|
103
|
-
|
|
104
|
-
// Handle the 'data' field specially, attempting JSON parsing
|
|
105
|
-
if (field === 'data') {
|
|
106
|
-
event.data = dataParser(value);
|
|
107
|
-
} else {
|
|
108
|
-
// For non-data fields, assign the value as a string to the event object
|
|
109
|
-
(event as Record<string, unknown>)[field] = value;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return event;
|
|
113
|
-
},
|
|
114
|
-
{} as ServerSentEvent<TData>,
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Only emit the event to the results if it has at least one field
|
|
118
|
-
if (Object.keys(event).length) {
|
|
119
|
-
yield event;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function* readStream<TData>(
|
|
125
|
-
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
126
|
-
options?: {
|
|
127
|
-
dataParser?: DataParser<TData>;
|
|
128
|
-
skipHeartbeats?: boolean;
|
|
129
|
-
eventFilter?: string | string[];
|
|
130
|
-
},
|
|
131
|
-
): AsyncGenerator<ServerSentEvent<TData>> {
|
|
132
|
-
const skipHeartbeats = !!options?.skipHeartbeats;
|
|
133
|
-
|
|
134
|
-
const eventFilter = options?.eventFilter
|
|
135
|
-
? Array.isArray(options.eventFilter)
|
|
136
|
-
? options.eventFilter
|
|
137
|
-
: [options.eventFilter]
|
|
138
|
-
: null;
|
|
139
|
-
|
|
140
|
-
const decoder = new TextDecoder();
|
|
141
|
-
|
|
142
|
-
// keep reading chunks from the stream until the reader signals completion
|
|
143
|
-
while (true) {
|
|
144
|
-
const { done, value } = await reader.read();
|
|
145
|
-
// the underlying stream has ended, nothing more to read
|
|
146
|
-
if (done) {
|
|
147
|
-
// exit the read loop rather than yielding any further events
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const text = decoder.decode(value, { stream: true });
|
|
152
|
-
const events = parseEvents<TData>(text, { dataParser: options?.dataParser });
|
|
153
|
-
// a chunk may contain multiple complete SSE events, emit each in turn
|
|
154
|
-
for (const event of events) {
|
|
155
|
-
// a retry directive reconfigures the reconnection delay rather than being a data event
|
|
156
|
-
if (event.retry) {
|
|
157
|
-
await new Promise((resolve) =>
|
|
158
|
-
setTimeout(resolve, Number.parseInt(event.retry ?? '300', 10)),
|
|
159
|
-
);
|
|
160
|
-
// nothing to yield for a retry-only event
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
// heartbeat filtering is opt-in via the skipHeartbeats option
|
|
164
|
-
if (skipHeartbeats) {
|
|
165
|
-
// Skip comment-based heartbeats (no event, data, or id)
|
|
166
|
-
if (!event.event && !event.data && !event.id) {
|
|
167
|
-
// this event carries no payload, so treat it as a heartbeat and skip it
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
// Skip named heartbeat events (e.g., event: heartbeat or event: ping)
|
|
171
|
-
if (event.event && ['heartbeat', 'ping'].includes(event.event)) {
|
|
172
|
-
// an explicitly named heartbeat/ping event should also be skipped
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// only emit events that pass the caller-provided event type filter, if any
|
|
177
|
-
if (!eventFilter || (event.event && eventFilter.includes(event.event))) {
|
|
178
|
-
yield event as ServerSentEvent<TData>;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Options for configuring the SSE (Server-Sent Events) selector.
|
|
186
|
-
*/
|
|
187
|
-
export type SseSelectorOptions<TData = unknown> = {
|
|
188
|
-
dataParser?: DataParser<TData>;
|
|
189
|
-
/**
|
|
190
|
-
* A string or an array of strings specifying the events to filter.
|
|
191
|
-
* Only events matching the filter will be processed. If not provided,
|
|
192
|
-
* all events will be processed.
|
|
193
|
-
*/
|
|
194
|
-
eventFilter?: string | string[];
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* A boolean indicating whether to skip processing heartbeat events.
|
|
198
|
-
* Defaults to `false` if not specified.
|
|
199
|
-
*/
|
|
200
|
-
skipHeartbeats?: boolean;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* An `AbortSignal` that can be used to abort the SSE operation.
|
|
204
|
-
* Useful for managing the lifecycle of the SSE connection.
|
|
205
|
-
*/
|
|
206
|
-
abortSignal?: AbortSignal | null;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* A type alias for selecting and transforming Server-Sent Events (SSE) from an HTTP response.
|
|
211
|
-
*
|
|
212
|
-
* @template TData - The type of the data contained within the Server-Sent Event. Defaults to `unknown`.
|
|
213
|
-
* @template TResponse - The type of the HTTP response object. Defaults to `Response`.
|
|
214
|
-
*
|
|
215
|
-
* This selector is used to process and extract `ServerSentEvent<TData>` objects
|
|
216
|
-
* from an HTTP `Response` object, enabling custom handling of SSE data streams.
|
|
217
|
-
*/
|
|
218
|
-
export type SseSelector<TData = unknown, TResponse extends Response = Response> = ResponseSelector<
|
|
219
|
-
ServerSentEvent<TData>,
|
|
220
|
-
TResponse
|
|
221
|
-
>;
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Transforms an SSE response into an Observable stream of parsed events.
|
|
225
|
-
* @param response - The HTTP response with Content-Type: text/event-stream.
|
|
226
|
-
* @param options - Optional configuration for event filtering and heartbeat handling.
|
|
227
|
-
* @param options.eventFilter - Filter events by type (single string or array).
|
|
228
|
-
* @param options.skipHeartbeats - Skip empty/heartbeat events if true.
|
|
229
|
-
* @param options.dataParser - Custom parser for the data field.
|
|
230
|
-
* @param options.abortSignal - Abort signal to cancel the stream.
|
|
231
|
-
* @returns An Observable emitting parsed ServerSentEvent objects.
|
|
232
|
-
* @throws ServerSentEventResponseError if response is invalid or stream fails.
|
|
233
|
-
*/
|
|
234
|
-
export const createSseSelector = <TData = unknown, TResponse extends Response = Response>(
|
|
235
|
-
options?: SseSelectorOptions<TData>,
|
|
236
|
-
): SseSelector<TData, TResponse> => {
|
|
237
|
-
return (response: TResponse): Observable<ServerSentEvent<TData>> => {
|
|
238
|
-
// an unsuccessful HTTP status means there is no valid SSE stream to read
|
|
239
|
-
if (!response.ok) {
|
|
240
|
-
throw new ServerSentEventResponseError(`HTTP error! Status: ${response.status}`, response);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// a missing body means there is nothing to stream from
|
|
244
|
-
if (!response.body) {
|
|
245
|
-
throw new ServerSentEventResponseError('Response body is not readable', response);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// guard against consuming a non-SSE response as if it were an event stream
|
|
249
|
-
if (!response.headers.get('Content-Type')?.includes('text/event-stream')) {
|
|
250
|
-
throw new ServerSentEventResponseError('Response is not a text/event-stream', response);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const reader = response.body.getReader();
|
|
254
|
-
|
|
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
|
-
)
|
|
275
|
-
);
|
|
276
|
-
};
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
export default createSseSelector;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { jsonSelector } from './json-selector';
|
|
2
|
-
export { blobSelector } from './blob-selector';
|
|
3
|
-
export { createSseSelector } from './create-sse-selector';
|
|
4
|
-
|
|
5
|
-
export type { ResponseSelector } from '../client/types';
|
|
6
|
-
export type {
|
|
7
|
-
DataParser,
|
|
8
|
-
ServerSentEvent,
|
|
9
|
-
SseSelector,
|
|
10
|
-
SseSelectorOptions,
|
|
11
|
-
} from './create-sse-selector';
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { ResponseSelector } from '../client/types';
|
|
2
|
-
import { HttpJsonResponseError } from '../../errors/index.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Asynchronously parses the JSON data from a given HTTP response.
|
|
6
|
-
*
|
|
7
|
-
* If the response has a status code of 204 (No Content), this function will resolve with `undefined`.
|
|
8
|
-
*
|
|
9
|
-
* If the response is not successful (i.e. `response.ok` is `false`), this function will throw an `HttpJsonResponseError` with the response details and the parsed data (if any).
|
|
10
|
-
*
|
|
11
|
-
* If there is an error parsing the JSON data, this function will throw an `HttpJsonResponseError` with the parsing error and the original response.
|
|
12
|
-
*
|
|
13
|
-
* @template TType - The expected type of the parsed JSON data.
|
|
14
|
-
* @template TResponse - The type of the HTTP response object.
|
|
15
|
-
* @param response - The HTTP response to parse.
|
|
16
|
-
* @returns A promise that resolves with the parsed JSON data, or rejects with an `HttpJsonResponseError`.
|
|
17
|
-
*/
|
|
18
|
-
export const jsonSelector: ResponseSelector = async <
|
|
19
|
-
TType = unknown,
|
|
20
|
-
TResponse extends Response = Response,
|
|
21
|
-
>(
|
|
22
|
-
response: TResponse,
|
|
23
|
-
): Promise<TType> => {
|
|
24
|
-
/** Status code 204 indicates no content in the response */
|
|
25
|
-
if (response.status === 204) {
|
|
26
|
-
return Promise.resolve() as Promise<TType>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
// Parse the response JSON data
|
|
31
|
-
const data = await response.json();
|
|
32
|
-
|
|
33
|
-
// Check if the response was successful
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
// Throw an error with the response details
|
|
36
|
-
throw new HttpJsonResponseError('network response was not OK', response, { data });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Return the parsed data
|
|
40
|
-
return data;
|
|
41
|
-
} catch (cause) {
|
|
42
|
-
// If the cause is an HttpJsonResponseError, rethrow it
|
|
43
|
-
if (cause instanceof HttpJsonResponseError) {
|
|
44
|
-
throw cause;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Otherwise, throw a new HttpJsonResponseError with the parsing error
|
|
48
|
-
throw new HttpJsonResponseError('failed to parse response', response, { cause });
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export default jsonSelector;
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { HttpMiddleware } from '../lib/operators/types';
|
|
2
|
-
|
|
3
|
-
import { resolveOpenApiMockResponse, type OpenApiMockLike } from './resolve-open-api-mock-response';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Adapts an `OpenApiMock` into an {@link HttpMiddleware}, so
|
|
7
|
-
* `configurator.http.addMiddleware(...)` fakes every matching request
|
|
8
|
-
* straight from an OpenAPI document — no separate mock configurator needed.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* A request that matches no operation in the document falls through to
|
|
12
|
-
* `next`, so this composes with whatever else is registered — including the
|
|
13
|
-
* real network call, or another middleware further down the chain. Because
|
|
14
|
-
* `addMiddleware` wraps `_performFetch` rather than replacing it, the exact
|
|
15
|
-
* same registration also fakes requests through any client this configurator
|
|
16
|
-
* builds, so app config never has to branch on whether it's under test.
|
|
17
|
-
*
|
|
18
|
-
* @param openApiMock - Typically `createOpenApiMock(document)` from `@equinor/fusion-openapi-mock`.
|
|
19
|
-
* @returns A middleware for {@link IHttpClientConfigurator.addMiddleware}.
|
|
20
|
-
*
|
|
21
|
-
* @example Fake every operation in a spec, straight from the document
|
|
22
|
-
* ```typescript
|
|
23
|
-
* import { createOpenApiMock } from '@equinor/fusion-openapi-mock';
|
|
24
|
-
* import openapi from './openapi.json' with { type: 'json' };
|
|
25
|
-
*
|
|
26
|
-
* configurator.http.addMiddleware(createOpenApiMockMiddleware(createOpenApiMock(openapi)));
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export function createOpenApiMockMiddleware(openApiMock: OpenApiMockLike): HttpMiddleware {
|
|
30
|
-
return async (uri, init, next) => {
|
|
31
|
-
const response = await resolveOpenApiMockResponse(
|
|
32
|
-
openApiMock,
|
|
33
|
-
init.method ?? 'GET',
|
|
34
|
-
new URL(uri),
|
|
35
|
-
);
|
|
36
|
-
return response ?? next(uri, init);
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export default createOpenApiMockMiddleware;
|