@equinor/fusion-framework-module-http 6.2.5 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +182 -17
- package/dist/esm/errors.js +22 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/lib/client/client.js +35 -0
- package/dist/esm/lib/client/client.js.map +1 -1
- package/dist/esm/lib/operators/sse.operator.js +33 -0
- package/dist/esm/lib/operators/sse.operator.js.map +1 -0
- package/dist/esm/lib/selectors/sse-selector.js +164 -0
- package/dist/esm/lib/selectors/sse-selector.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/errors.d.ts +21 -0
- package/dist/types/lib/client/client.d.ts +24 -0
- package/dist/types/lib/operators/fetch-request.schema.d.ts +4 -4
- package/dist/types/lib/operators/sse.operator.d.ts +32 -0
- package/dist/types/lib/selectors/sse-selector.d.ts +63 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +3 -3
- package/src/errors.ts +27 -0
- package/src/lib/client/client.ts +52 -0
- package/src/lib/operators/sse.operator.ts +44 -0
- package/src/lib/selectors/sse-selector.ts +262 -0
- package/src/version.ts +1 -1
- package/tests/sse.selector.test.ts +150 -0
package/dist/types/errors.d.ts
CHANGED
|
@@ -28,3 +28,24 @@ export declare class HttpJsonResponseError<TType = unknown, TResponse = Response
|
|
|
28
28
|
data?: TType;
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Represents an error that occurs when handling a server-sent event (SSE) HTTP response.
|
|
33
|
+
*
|
|
34
|
+
* @template TType - The type of additional data associated with the error.
|
|
35
|
+
* @template TResponse - The type of the HTTP response object.
|
|
36
|
+
*
|
|
37
|
+
* @extends HttpResponseError<TResponse>
|
|
38
|
+
*/
|
|
39
|
+
export declare class ServerSentEventResponseError<TType = unknown, TResponse = Response> extends HttpResponseError<TResponse> {
|
|
40
|
+
static Name: string;
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new instance of the error.
|
|
43
|
+
*
|
|
44
|
+
* @param message - The error message describing the cause of the error.
|
|
45
|
+
* @param response - The HTTP response associated with the error.
|
|
46
|
+
* @param options - Optional error options, which may include additional data of type `TType`.
|
|
47
|
+
*/
|
|
48
|
+
constructor(message: string, response: TResponse, options?: ErrorOptions & {
|
|
49
|
+
data?: TType;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Subject } from 'rxjs';
|
|
|
2
2
|
import type { Observable, ObservableInput } from 'rxjs';
|
|
3
3
|
import type { IHttpRequestHandler, IHttpResponseHandler } from '../operators';
|
|
4
4
|
import type { BlobResult, FetchRequest, FetchRequestInit, FetchResponse, IHttpClient, JsonRequest, StreamResponse } from './types';
|
|
5
|
+
import { type ServerSentEvent, type SseSelectorOptions } from '../selectors/sse-selector';
|
|
5
6
|
/**
|
|
6
7
|
* Configuration options for creating an `HttpClient` instance.
|
|
7
8
|
*
|
|
@@ -114,6 +115,29 @@ export declare class HttpClient<TRequest extends FetchRequest = FetchRequest, TR
|
|
|
114
115
|
* @returns A Promise that resolves to the blob result.
|
|
115
116
|
*/
|
|
116
117
|
blob<T = BlobResult>(path: string, args?: FetchRequestInit<T, TRequest, TResponse>): Promise<T>;
|
|
118
|
+
/**
|
|
119
|
+
* Initiates a Server-Sent Events (SSE) stream request to the specified path and returns a stream of events.
|
|
120
|
+
*
|
|
121
|
+
* @template T - The type of the event data expected from the SSE stream.
|
|
122
|
+
* @param path - The endpoint path to connect to for the SSE stream.
|
|
123
|
+
* @param args - Optional fetch request initialization options, including headers and abort signal.
|
|
124
|
+
* @param options - Optional selector options for customizing the SSE stream, excluding the abort signal.
|
|
125
|
+
*
|
|
126
|
+
* @returns A `StreamResponse` that emits `ServerSentEvent<T>` objects as they are received from the server.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* const sse$ = httpClient.sse(
|
|
130
|
+
* '/events',
|
|
131
|
+
* { method: 'POST', body: JSON.stringify({ prompt: 'tell me a joke' }) },
|
|
132
|
+
* { eventFilter: ['message'] }
|
|
133
|
+
* );
|
|
134
|
+
* sse$.subscribe({
|
|
135
|
+
* next: (event) => console.log(event),
|
|
136
|
+
* error: (err) => console.error(err),
|
|
137
|
+
* complete: () => console.log('Completed'),
|
|
138
|
+
* });
|
|
139
|
+
*/
|
|
140
|
+
sse$<T = unknown>(path: string, args?: FetchRequestInit<ServerSentEvent<T>, TRequest, TResponse> | null, options?: Omit<SseSelectorOptions<T>, 'abortSignal'>): StreamResponse<ServerSentEvent<T>>;
|
|
117
141
|
/** @deprecated */
|
|
118
142
|
jsonAsync<T = unknown>(path: string, args?: FetchRequestInit<T, JsonRequest<TRequest>, TResponse>): Promise<T>;
|
|
119
143
|
/**
|
|
@@ -53,7 +53,7 @@ export declare const requestInitSchema: z.ZodObject<{
|
|
|
53
53
|
signal: z.ZodOptional<z.ZodType<AbortSignal, z.ZodTypeDef, AbortSignal>>;
|
|
54
54
|
}, "strip", z.ZodTypeAny, {
|
|
55
55
|
body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
|
|
56
|
-
cache?: "default" | "
|
|
56
|
+
cache?: "default" | "no-cache" | "force-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
|
|
57
57
|
credentials?: "include" | "omit" | "same-origin" | undefined;
|
|
58
58
|
headers?: Headers | Record<string, string> | undefined;
|
|
59
59
|
integrity?: string | undefined;
|
|
@@ -72,7 +72,7 @@ export declare const requestInitSchema: z.ZodObject<{
|
|
|
72
72
|
browsingTopics?: boolean | undefined;
|
|
73
73
|
}, {
|
|
74
74
|
body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
|
|
75
|
-
cache?: "default" | "
|
|
75
|
+
cache?: "default" | "no-cache" | "force-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
|
|
76
76
|
credentials?: "include" | "omit" | "same-origin" | undefined;
|
|
77
77
|
headers?: Headers | Record<string, string> | undefined;
|
|
78
78
|
integrity?: string | undefined;
|
|
@@ -129,7 +129,7 @@ export declare const fetchRequestSchema: z.ZodObject<{
|
|
|
129
129
|
uri: string;
|
|
130
130
|
body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
|
|
131
131
|
path?: string | undefined;
|
|
132
|
-
cache?: "default" | "
|
|
132
|
+
cache?: "default" | "no-cache" | "force-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
|
|
133
133
|
credentials?: "include" | "omit" | "same-origin" | undefined;
|
|
134
134
|
headers?: Headers | Record<string, string> | undefined;
|
|
135
135
|
integrity?: string | undefined;
|
|
@@ -150,7 +150,7 @@ export declare const fetchRequestSchema: z.ZodObject<{
|
|
|
150
150
|
uri: string;
|
|
151
151
|
body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
|
|
152
152
|
path?: string | undefined;
|
|
153
|
-
cache?: "default" | "
|
|
153
|
+
cache?: "default" | "no-cache" | "force-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
|
|
154
154
|
credentials?: "include" | "omit" | "same-origin" | undefined;
|
|
155
155
|
headers?: Headers | Record<string, string> | undefined;
|
|
156
156
|
integrity?: string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { OperatorFunction } from 'rxjs';
|
|
2
|
+
import { type ServerSentEvent, type SseSelectorOptions } from '../selectors/sse-selector';
|
|
3
|
+
/**
|
|
4
|
+
* An operator function for handling Server-Sent Events (SSE) in an RxJS pipeline.
|
|
5
|
+
*
|
|
6
|
+
* @template R - The type of the parsed data from the SSE.
|
|
7
|
+
* @template T - The type of the input `Response` object, defaults to `Response`.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Configuration options for the SSE selector.
|
|
10
|
+
*
|
|
11
|
+
* @returns An `OperatorFunction` that transforms a stream of `Response` objects
|
|
12
|
+
* into a stream of `ServerSentEvent` objects containing parsed data of type `R`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { sseMap } from '@equinor/fusion-framework-module-http/operators';
|
|
17
|
+
* import { fromFetch } from 'rxjs/fetch';
|
|
18
|
+
*
|
|
19
|
+
* const response$ = fromFetch('https://example.com/sse', {
|
|
20
|
+
* method: 'GET',
|
|
21
|
+
* headers: {
|
|
22
|
+
* 'Accept': 'text/event-stream',
|
|
23
|
+
* },
|
|
24
|
+
* }).pipe(
|
|
25
|
+
* sseMap( { /* SSE selector options *\/ })
|
|
26
|
+
* ).subscribe((event) => {
|
|
27
|
+
* console.log(event.data); // Process the parsed SSE data
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const sseMap: <R = unknown, T extends Response = Response>(options?: SseSelectorOptions<R>) => OperatorFunction<T, ServerSentEvent<R>>;
|
|
32
|
+
export default sseMap;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ResponseSelector } from '../client/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A type representing a function that parses a string into a specific data type.
|
|
4
|
+
*
|
|
5
|
+
* @typeParam TData - The type of the parsed data. Defaults to `unknown` if not specified.
|
|
6
|
+
* @param data - The string input to be parsed.
|
|
7
|
+
* @returns The parsed data of type `TData`.
|
|
8
|
+
*/
|
|
9
|
+
export type DataParser<TData = unknown> = (data: string) => TData;
|
|
10
|
+
/**
|
|
11
|
+
* Represents a Server-Sent Event (SSE) with optional fields.
|
|
12
|
+
*/
|
|
13
|
+
export type ServerSentEvent<TData = unknown> = {
|
|
14
|
+
id?: string;
|
|
15
|
+
event?: string;
|
|
16
|
+
data?: TData;
|
|
17
|
+
retry?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Options for configuring the SSE (Server-Sent Events) selector.
|
|
21
|
+
*/
|
|
22
|
+
export type SseSelectorOptions<TData = unknown> = {
|
|
23
|
+
dataParser?: DataParser<TData>;
|
|
24
|
+
/**
|
|
25
|
+
* A string or an array of strings specifying the events to filter.
|
|
26
|
+
* Only events matching the filter will be processed. If not provided,
|
|
27
|
+
* all events will be processed.
|
|
28
|
+
*/
|
|
29
|
+
eventFilter?: string | string[];
|
|
30
|
+
/**
|
|
31
|
+
* A boolean indicating whether to skip processing heartbeat events.
|
|
32
|
+
* Defaults to `false` if not specified.
|
|
33
|
+
*/
|
|
34
|
+
skipHeartbeats?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* An `AbortSignal` that can be used to abort the SSE operation.
|
|
37
|
+
* Useful for managing the lifecycle of the SSE connection.
|
|
38
|
+
*/
|
|
39
|
+
abortSignal?: AbortSignal | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A type alias for selecting and transforming Server-Sent Events (SSE) from an HTTP response.
|
|
43
|
+
*
|
|
44
|
+
* @template TData - The type of the data contained within the Server-Sent Event. Defaults to `unknown`.
|
|
45
|
+
* @template TResponse - The type of the HTTP response object. Defaults to `Response`.
|
|
46
|
+
*
|
|
47
|
+
* This selector is used to process and extract `ServerSentEvent<TData>` objects
|
|
48
|
+
* from an HTTP `Response` object, enabling custom handling of SSE data streams.
|
|
49
|
+
*/
|
|
50
|
+
export type SseSelector<TData = unknown, TResponse extends Response = Response> = ResponseSelector<ServerSentEvent<TData>, TResponse>;
|
|
51
|
+
/**
|
|
52
|
+
* Transforms an SSE response into an Observable stream of parsed events.
|
|
53
|
+
* @param response - The HTTP response with Content-Type: text/event-stream.
|
|
54
|
+
* @param options - Optional configuration for event filtering and heartbeat handling.
|
|
55
|
+
* @param options.eventFilter - Filter events by type (single string or array).
|
|
56
|
+
* @param options.skipHeartbeats - Skip empty/heartbeat events if true.
|
|
57
|
+
* @param options.dataParser - Custom parser for the data field.
|
|
58
|
+
* @param options.abortSignal - Abort signal to cancel the stream.
|
|
59
|
+
* @returns An Observable emitting parsed ServerSentEvent objects.
|
|
60
|
+
* @throws ServerSentEventResponseError if response is invalid or stream fails.
|
|
61
|
+
*/
|
|
62
|
+
export declare const createSseSelector: <TData = unknown, TResponse extends Response = Response>(options?: SseSelectorOptions<TData>) => SseSelector<TData, TResponse>;
|
|
63
|
+
export default createSseSelector;
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "6.
|
|
1
|
+
export declare const version = "6.3.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-http",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"rxjs": "^7.8.1",
|
|
42
42
|
"zod": "^3.23.8",
|
|
43
|
-
"@equinor/fusion-framework-module": "^4.
|
|
44
|
-
"@equinor/fusion-framework-module-msal": "^4.0.
|
|
43
|
+
"@equinor/fusion-framework-module": "^4.4.0",
|
|
44
|
+
"@equinor/fusion-framework-module-msal": "^4.0.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"typescript": "^5.8.2",
|
package/src/errors.ts
CHANGED
|
@@ -40,3 +40,30 @@ export class HttpJsonResponseError<
|
|
|
40
40
|
this.data = options?.data;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Represents an error that occurs when handling a server-sent event (SSE) HTTP response.
|
|
46
|
+
*
|
|
47
|
+
* @template TType - The type of additional data associated with the error.
|
|
48
|
+
* @template TResponse - The type of the HTTP response object.
|
|
49
|
+
*
|
|
50
|
+
* @extends HttpResponseError<TResponse>
|
|
51
|
+
*/
|
|
52
|
+
export class ServerSentEventResponseError<
|
|
53
|
+
TType = unknown,
|
|
54
|
+
TResponse = Response,
|
|
55
|
+
> extends HttpResponseError<TResponse> {
|
|
56
|
+
static Name = 'ServerSentEventResponseError';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a new instance of the error.
|
|
60
|
+
*
|
|
61
|
+
* @param message - The error message describing the cause of the error.
|
|
62
|
+
* @param response - The HTTP response associated with the error.
|
|
63
|
+
* @param options - Optional error options, which may include additional data of type `TType`.
|
|
64
|
+
*/
|
|
65
|
+
constructor(message: string, response: TResponse, options?: ErrorOptions & { data?: TType }) {
|
|
66
|
+
super(message, response, options);
|
|
67
|
+
this.name = ServerSentEventResponseError.Name;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/lib/client/client.ts
CHANGED
|
@@ -14,10 +14,17 @@ import type {
|
|
|
14
14
|
FetchResponse,
|
|
15
15
|
IHttpClient,
|
|
16
16
|
JsonRequest,
|
|
17
|
+
ResponseSelector,
|
|
17
18
|
StreamResponse,
|
|
18
19
|
} from './types';
|
|
19
20
|
|
|
20
21
|
import { HttpResponseError } from '../../errors';
|
|
22
|
+
import {
|
|
23
|
+
createSseSelector,
|
|
24
|
+
SseSelector,
|
|
25
|
+
type ServerSentEvent,
|
|
26
|
+
type SseSelectorOptions,
|
|
27
|
+
} from '../selectors/sse-selector';
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Configuration options for creating an `HttpClient` instance.
|
|
@@ -222,6 +229,51 @@ export class HttpClient<
|
|
|
222
229
|
return firstValueFrom(this.blob$(path, args));
|
|
223
230
|
}
|
|
224
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Initiates a Server-Sent Events (SSE) stream request to the specified path and returns a stream of events.
|
|
234
|
+
*
|
|
235
|
+
* @template T - The type of the event data expected from the SSE stream.
|
|
236
|
+
* @param path - The endpoint path to connect to for the SSE stream.
|
|
237
|
+
* @param args - Optional fetch request initialization options, including headers and abort signal.
|
|
238
|
+
* @param options - Optional selector options for customizing the SSE stream, excluding the abort signal.
|
|
239
|
+
*
|
|
240
|
+
* @returns A `StreamResponse` that emits `ServerSentEvent<T>` objects as they are received from the server.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* const sse$ = httpClient.sse(
|
|
244
|
+
* '/events',
|
|
245
|
+
* { method: 'POST', body: JSON.stringify({ prompt: 'tell me a joke' }) },
|
|
246
|
+
* { eventFilter: ['message'] }
|
|
247
|
+
* );
|
|
248
|
+
* sse$.subscribe({
|
|
249
|
+
* next: (event) => console.log(event),
|
|
250
|
+
* error: (err) => console.error(err),
|
|
251
|
+
* complete: () => console.log('Completed'),
|
|
252
|
+
* });
|
|
253
|
+
*/
|
|
254
|
+
public sse$<T = unknown>(
|
|
255
|
+
path: string,
|
|
256
|
+
args?: FetchRequestInit<ServerSentEvent<T>, TRequest, TResponse> | null,
|
|
257
|
+
options?: Omit<SseSelectorOptions<T>, 'abortSignal'>,
|
|
258
|
+
): StreamResponse<ServerSentEvent<T>> {
|
|
259
|
+
// Setup default common headers for SSE
|
|
260
|
+
const headers = new Headers(args?.headers);
|
|
261
|
+
headers.append('Accept', 'text/event-stream');
|
|
262
|
+
headers.append('Content-Type', 'text/event-stream');
|
|
263
|
+
headers.append('Cache-Control', 'no-cache');
|
|
264
|
+
headers.append('Connection', 'keep-alive');
|
|
265
|
+
|
|
266
|
+
// Create the selector using the provided options and the abort signal from args
|
|
267
|
+
const selector = createSseSelector<T>({ ...options, abortSignal: args?.signal });
|
|
268
|
+
|
|
269
|
+
// Call the fetch$ method with the provided path and the constructed init object
|
|
270
|
+
return this._fetch$(path, { selector, ...args, headers } as FetchRequestInit<
|
|
271
|
+
ServerSentEvent<T>,
|
|
272
|
+
TRequest,
|
|
273
|
+
TResponse
|
|
274
|
+
>);
|
|
275
|
+
}
|
|
276
|
+
|
|
225
277
|
/** @deprecated */
|
|
226
278
|
public jsonAsync<T = unknown>(
|
|
227
279
|
path: string,
|
|
@@ -0,0 +1,44 @@
|
|
|
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/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
|
+
source.pipe(switchMap(createSseSelector<R, T>(options)));
|
|
43
|
+
|
|
44
|
+
export default sseMap;
|
|
@@ -0,0 +1,262 @@
|
|
|
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.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
|
+
while (true) {
|
|
143
|
+
const { done, value } = await reader.read();
|
|
144
|
+
if (done) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const text = decoder.decode(value, { stream: true });
|
|
149
|
+
const events = parseEvents<TData>(text, { dataParser: options?.dataParser });
|
|
150
|
+
for (const event of events) {
|
|
151
|
+
if (event.retry) {
|
|
152
|
+
await new Promise((resolve) =>
|
|
153
|
+
setTimeout(resolve, Number.parseInt(event.retry ?? '300', 10)),
|
|
154
|
+
);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (skipHeartbeats) {
|
|
158
|
+
// Skip comment-based heartbeats (no event, data, or id)
|
|
159
|
+
if (!event.event && !event.data && !event.id) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Skip named heartbeat events (e.g., event: heartbeat or event: ping)
|
|
163
|
+
if (event.event && ['heartbeat', 'ping'].includes(event.event)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!eventFilter || (event.event && eventFilter.includes(event.event))) {
|
|
168
|
+
yield event as ServerSentEvent<TData>;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Options for configuring the SSE (Server-Sent Events) selector.
|
|
176
|
+
*/
|
|
177
|
+
export type SseSelectorOptions<TData = unknown> = {
|
|
178
|
+
dataParser?: DataParser<TData>;
|
|
179
|
+
/**
|
|
180
|
+
* A string or an array of strings specifying the events to filter.
|
|
181
|
+
* Only events matching the filter will be processed. If not provided,
|
|
182
|
+
* all events will be processed.
|
|
183
|
+
*/
|
|
184
|
+
eventFilter?: string | string[];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A boolean indicating whether to skip processing heartbeat events.
|
|
188
|
+
* Defaults to `false` if not specified.
|
|
189
|
+
*/
|
|
190
|
+
skipHeartbeats?: boolean;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* An `AbortSignal` that can be used to abort the SSE operation.
|
|
194
|
+
* Useful for managing the lifecycle of the SSE connection.
|
|
195
|
+
*/
|
|
196
|
+
abortSignal?: AbortSignal | null;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A type alias for selecting and transforming Server-Sent Events (SSE) from an HTTP response.
|
|
201
|
+
*
|
|
202
|
+
* @template TData - The type of the data contained within the Server-Sent Event. Defaults to `unknown`.
|
|
203
|
+
* @template TResponse - The type of the HTTP response object. Defaults to `Response`.
|
|
204
|
+
*
|
|
205
|
+
* This selector is used to process and extract `ServerSentEvent<TData>` objects
|
|
206
|
+
* from an HTTP `Response` object, enabling custom handling of SSE data streams.
|
|
207
|
+
*/
|
|
208
|
+
export type SseSelector<TData = unknown, TResponse extends Response = Response> = ResponseSelector<
|
|
209
|
+
ServerSentEvent<TData>,
|
|
210
|
+
TResponse
|
|
211
|
+
>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Transforms an SSE response into an Observable stream of parsed events.
|
|
215
|
+
* @param response - The HTTP response with Content-Type: text/event-stream.
|
|
216
|
+
* @param options - Optional configuration for event filtering and heartbeat handling.
|
|
217
|
+
* @param options.eventFilter - Filter events by type (single string or array).
|
|
218
|
+
* @param options.skipHeartbeats - Skip empty/heartbeat events if true.
|
|
219
|
+
* @param options.dataParser - Custom parser for the data field.
|
|
220
|
+
* @param options.abortSignal - Abort signal to cancel the stream.
|
|
221
|
+
* @returns An Observable emitting parsed ServerSentEvent objects.
|
|
222
|
+
* @throws ServerSentEventResponseError if response is invalid or stream fails.
|
|
223
|
+
*/
|
|
224
|
+
export const createSseSelector = <TData = unknown, TResponse extends Response = Response>(
|
|
225
|
+
options?: SseSelectorOptions<TData>,
|
|
226
|
+
): SseSelector<TData, TResponse> => {
|
|
227
|
+
return (response: TResponse): Observable<ServerSentEvent<TData>> => {
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
throw new ServerSentEventResponseError(`HTTP error! Status: ${response.status}`, response);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!response.body) {
|
|
233
|
+
throw new ServerSentEventResponseError('Response body is not readable', response);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!response.headers.get('Content-Type')?.includes('text/event-stream')) {
|
|
237
|
+
throw new ServerSentEventResponseError('Response is not a text/event-stream', response);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const reader = response.body.getReader();
|
|
241
|
+
|
|
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
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export default createSseSelector;
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '6.
|
|
2
|
+
export const version = '6.3.0';
|