@equinor/fusion-framework-module-http 7.0.8 → 8.0.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 +34 -27
- package/README.md +141 -925
- package/dist/esm/configurator.js +5 -5
- package/dist/esm/configurator.js.map +1 -1
- package/dist/esm/lib/client/client-msal.js +3 -2
- package/dist/esm/lib/client/client-msal.js.map +1 -1
- package/dist/esm/lib/client/client.js +8 -7
- package/dist/esm/lib/client/client.js.map +1 -1
- package/dist/esm/lib/operators/index.js +1 -0
- package/dist/esm/lib/operators/index.js.map +1 -1
- package/dist/esm/lib/selectors/index.js +1 -0
- package/dist/esm/lib/selectors/index.js.map +1 -1
- package/dist/esm/module.js +18 -9
- package/dist/esm/module.js.map +1 -1
- package/dist/esm/provider.js +38 -27
- package/dist/esm/provider.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/configurator.d.ts +49 -26
- package/dist/types/lib/client/client.d.ts +7 -7
- package/dist/types/lib/operators/index.d.ts +1 -0
- package/dist/types/lib/selectors/index.d.ts +2 -0
- package/dist/types/module.d.ts +18 -9
- package/dist/types/provider.d.ts +24 -19
- package/dist/types/version.d.ts +1 -1
- package/docs/client-configuration.md +175 -0
- package/docs/observable-patterns.md +103 -0
- package/docs/selectors-and-handlers.md +112 -0
- package/docs/server-sent-events.md +125 -0
- package/package.json +6 -6
- package/src/configurator.ts +54 -26
- package/src/lib/client/client-msal.ts +3 -2
- package/src/lib/client/client.ts +8 -9
- package/src/lib/operators/index.ts +1 -0
- package/src/lib/selectors/index.ts +7 -0
- package/src/module.ts +18 -9
- package/src/provider.ts +56 -34
- package/src/version.ts +1 -1
- package/tests/HttpClient.test.ts +90 -11
- package/tests/operators.test.ts +24 -0
- package/tests/sse.selector.test.ts +6 -7
package/src/provider.ts
CHANGED
|
@@ -11,15 +11,19 @@ import { BaseModuleProvider } from '@equinor/fusion-framework-module/provider';
|
|
|
11
11
|
import { version } from './version';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Thrown when `createClient(name)` is called with an unknown client key.
|
|
15
15
|
*
|
|
16
|
-
* This
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* @extends {Error}
|
|
16
|
+
* This is only used when the provided string is neither a registered client name
|
|
17
|
+
* nor an absolute `http:` or `https:` URL.
|
|
20
18
|
*/
|
|
21
19
|
export class ClientNotFoundException extends Error {}
|
|
22
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Creates fresh HTTP client instances from named or ad-hoc configuration.
|
|
23
|
+
*
|
|
24
|
+
* A provider can create clients from registered names, inline `HttpClientOptions`,
|
|
25
|
+
* or absolute URLs treated as ad-hoc `baseUri` values.
|
|
26
|
+
*/
|
|
23
27
|
export interface IHttpClientProvider<TClient extends IHttpClient = IHttpClient> {
|
|
24
28
|
/**
|
|
25
29
|
* The default HTTP request handler used by the HttpClientProvider.
|
|
@@ -43,35 +47,48 @@ export interface IHttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
43
47
|
createClient(key: HttpClientOptions<TClient>): TClient;
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
50
|
+
* Creates a client instance and casts it to a custom HTTP client type.
|
|
51
|
+
*
|
|
52
|
+
* This is most useful when the named client configuration uses a custom `ctor`
|
|
53
|
+
* that extends `HttpClient` with domain-specific methods.
|
|
47
54
|
* @example
|
|
48
55
|
* ```ts
|
|
49
|
-
* config.http.configureClient('foobar',
|
|
50
|
-
*
|
|
51
|
-
*
|
|
56
|
+
* config.http.configureClient('foobar', {
|
|
57
|
+
* ctor: MyClient,
|
|
58
|
+
* baseUri: 'https://foobar.com',
|
|
52
59
|
* });
|
|
60
|
+
* ```
|
|
53
61
|
*/
|
|
54
62
|
createCustomClient<T extends HttpClient>(key: string): T;
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/** URL protocols accepted as valid ad-hoc base URIs. */
|
|
66
|
+
const SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ws:', 'wss:'] as const;
|
|
67
|
+
|
|
57
68
|
/**
|
|
58
|
-
* Checks if a given string is a valid URL.
|
|
59
|
-
* @param url - The string to check
|
|
60
|
-
* @returns `true`
|
|
69
|
+
* Checks if a given string is a valid absolute URL with a supported protocol.
|
|
70
|
+
* @param url - The string to check.
|
|
71
|
+
* @returns `true` when the string uses one of {@link SUPPORTED_PROTOCOLS}.
|
|
61
72
|
*/
|
|
62
|
-
const isURL = (url: string) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'(\\#[-a-z\\d_]*)?$',
|
|
70
|
-
'i',
|
|
71
|
-
); // fragment locator
|
|
72
|
-
return pattern.test(url);
|
|
73
|
+
const isURL = (url: string): boolean => {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(url);
|
|
76
|
+
return (SUPPORTED_PROTOCOLS as readonly string[]).includes(parsed.protocol);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
73
80
|
};
|
|
74
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Heuristic check for strings that look like a bare hostname or URL without a
|
|
84
|
+
* protocol prefix (e.g. `api.example.com` or `api.example.com/v1`).
|
|
85
|
+
*
|
|
86
|
+
* Used to emit a deprecation warning when callers rely on the old (pre-patch)
|
|
87
|
+
* behaviour that accepted protocol-less URLs.
|
|
88
|
+
*/
|
|
89
|
+
const looksLikeURL = (value: string): boolean =>
|
|
90
|
+
!value.includes(' ') && /^[a-z\d]([a-z\d-]*\.)+[a-z]{2,}/i.test(value);
|
|
91
|
+
|
|
75
92
|
/**
|
|
76
93
|
* The `HttpClientProvider` class is responsible for managing HTTP client instances and their configuration.
|
|
77
94
|
* It provides methods to check if a client is configured, create new client instances, and create custom client instances.
|
|
@@ -105,7 +122,7 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
/**
|
|
108
|
-
* Creates a
|
|
125
|
+
* Creates a fresh HTTP client instance from a named or ad-hoc configuration.
|
|
109
126
|
*
|
|
110
127
|
* @param keyOrConfig - The key or configuration object for the HTTP client.
|
|
111
128
|
* @returns The created HTTP client instance.
|
|
@@ -115,12 +132,8 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
115
132
|
* If a string is provided, it is treated as the key for a pre-configured client in the `HttpClientProvider`.
|
|
116
133
|
* If an `HttpClientOptions` object is provided, it is used as the configuration for the new client instance.
|
|
117
134
|
*
|
|
118
|
-
* The method
|
|
119
|
-
*
|
|
120
|
-
* - `defaultScopes`: The default scopes to be used for authentication.
|
|
121
|
-
* - `onCreate`: An optional callback function that is called when the client instance is created.
|
|
122
|
-
* - `ctor`: The constructor function for the HTTP client, defaulting to the configured `defaultHttpClientCtor`.
|
|
123
|
-
* - `requestHandler`: The HTTP request handler to be used by the client, defaulting to the `defaultHttpRequestHandler`.
|
|
135
|
+
* The method applies `baseUri`, `defaultScopes`, `requestHandler`, `responseHandler`,
|
|
136
|
+
* a custom `ctor` when configured, and finally runs `onCreate` for the newly created instance.
|
|
124
137
|
*
|
|
125
138
|
* The created HTTP client instance is returned.
|
|
126
139
|
*/
|
|
@@ -132,8 +145,9 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
132
145
|
onCreate,
|
|
133
146
|
ctor = this.config.defaultHttpClientCtor,
|
|
134
147
|
requestHandler = this.defaultHttpRequestHandler,
|
|
148
|
+
responseHandler,
|
|
135
149
|
} = config as HttpClientOptions<TClient>;
|
|
136
|
-
const options = { requestHandler };
|
|
150
|
+
const options = { requestHandler, responseHandler };
|
|
137
151
|
const instance = new ctor(baseUri || '', options) as TClient;
|
|
138
152
|
Object.assign(instance, { defaultScopes });
|
|
139
153
|
onCreate?.(instance as TClient);
|
|
@@ -141,7 +155,7 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
/**
|
|
144
|
-
* Creates a
|
|
158
|
+
* Creates a client instance and returns it as the requested custom client type.
|
|
145
159
|
*
|
|
146
160
|
* @param key - The key of the pre-configured HTTP client to create.
|
|
147
161
|
* @returns The created HTTP client instance, cast to the specified type `T`.
|
|
@@ -159,8 +173,9 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
159
173
|
/**
|
|
160
174
|
* Resolves the configuration for an HTTP client based on the provided `keyOrConfig` parameter.
|
|
161
175
|
*
|
|
162
|
-
* If a string is provided, it is treated as
|
|
163
|
-
*
|
|
176
|
+
* If a string is provided, it is treated as either a pre-configured client key or,
|
|
177
|
+
* when it is an absolute `http:` or `https:` URL, as an ad-hoc `baseUri`.
|
|
178
|
+
* If an `HttpClientOptions` object is provided, it is used directly as the configuration for the new client instance.
|
|
164
179
|
*
|
|
165
180
|
* @param keyOrConfig - The key or configuration object for the HTTP client.
|
|
166
181
|
* @returns The resolved HTTP client configuration.
|
|
@@ -172,6 +187,13 @@ export class HttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
172
187
|
const config = this.config.clients[keyOrConfig];
|
|
173
188
|
if (!config && isURL(keyOrConfig)) {
|
|
174
189
|
return { baseUri: keyOrConfig };
|
|
190
|
+
} else if (!config && looksLikeURL(keyOrConfig)) {
|
|
191
|
+
console.warn(
|
|
192
|
+
`[HttpClientProvider] "${keyOrConfig}" looks like a URL but is missing the http:// or https:// protocol. ` +
|
|
193
|
+
`Treating it as "https://${keyOrConfig}". ` +
|
|
194
|
+
`Pass a fully-qualified URL to silence this warning.`,
|
|
195
|
+
);
|
|
196
|
+
return { baseUri: `https://${keyOrConfig}` };
|
|
175
197
|
} else if (!config) {
|
|
176
198
|
throw new ClientNotFoundException(`No registered http client for key [${keyOrConfig}]`);
|
|
177
199
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '
|
|
2
|
+
export const version = '8.0.0';
|
package/tests/HttpClient.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { lastValueFrom } from 'rxjs';
|
|
2
3
|
|
|
4
|
+
import { ClientNotFoundException, HttpClientProvider } from '../src';
|
|
3
5
|
import { HttpClientConfigurator } from '../src/configurator';
|
|
4
|
-
import { HttpClient } from '../src/lib';
|
|
5
|
-
import {
|
|
6
|
-
import { lastValueFrom } from 'rxjs';
|
|
6
|
+
import { HttpClient, HttpClientMsal } from '../src/lib';
|
|
7
|
+
import { HttpResponseHandler } from '../src/lib/operators';
|
|
7
8
|
|
|
8
9
|
let provider: HttpClientProvider;
|
|
9
10
|
|
|
@@ -14,35 +15,113 @@ describe('HttpClient', () => {
|
|
|
14
15
|
provider = new HttpClientProvider(config);
|
|
15
16
|
});
|
|
16
17
|
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
17
22
|
it('should create instance', () => {
|
|
18
23
|
const client = provider.createClient('foo');
|
|
19
24
|
expect(client).toBeDefined();
|
|
20
25
|
expect(client.uri).toBe('http://localhost:3000');
|
|
21
26
|
});
|
|
22
27
|
|
|
23
|
-
it('should allow providing headers in request', async () => {
|
|
28
|
+
it('should allow providing headers in request', async () => {
|
|
29
|
+
const client = provider.createClient('foo');
|
|
30
|
+
client.requestHandler.setHeader('x-foo', 'bar');
|
|
31
|
+
|
|
32
|
+
const request = await lastValueFrom(
|
|
33
|
+
client.requestHandler.process({
|
|
34
|
+
path: '/api',
|
|
35
|
+
uri: client.uri,
|
|
36
|
+
headers: { 'x-bar': 'baz' },
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const headers = request.headers as Headers;
|
|
41
|
+
expect(headers.get('x-foo')).toBe('bar');
|
|
42
|
+
expect(headers.get('x-bar')).toBe('baz');
|
|
43
|
+
});
|
|
24
44
|
|
|
25
45
|
it('should not modify configured headers', async () => {
|
|
26
|
-
// create a new client from configuration
|
|
27
46
|
const fooClient = provider.createClient('foo');
|
|
28
47
|
fooClient.requestHandler.setHeader('x-foo', 'bar');
|
|
29
48
|
|
|
30
|
-
// generate a RequestInit object
|
|
31
49
|
const fooRequest = await lastValueFrom(
|
|
32
50
|
fooClient.requestHandler.process({ path: '/api', uri: fooClient.uri }),
|
|
33
51
|
);
|
|
34
52
|
|
|
35
53
|
expect((fooRequest.headers as Headers)?.get('x-foo')).toBe('bar');
|
|
36
54
|
|
|
37
|
-
// create a new client from the same configuration
|
|
38
55
|
const barClient = provider.createClient('foo');
|
|
39
|
-
|
|
40
|
-
// generate a RequestInit object
|
|
41
56
|
const barRequest = await lastValueFrom(
|
|
42
57
|
barClient.requestHandler.process({ path: '/api', uri: barClient.uri }),
|
|
43
58
|
);
|
|
44
59
|
|
|
45
|
-
// expect the request header to not been modified
|
|
46
60
|
expect((barRequest.headers as Headers)?.get('x-foo')).toBeUndefined();
|
|
47
61
|
});
|
|
62
|
+
|
|
63
|
+
it('should execute configured response handlers from client options', async () => {
|
|
64
|
+
const responseSpy = vi.fn();
|
|
65
|
+
const config = new HttpClientConfigurator(HttpClient);
|
|
66
|
+
|
|
67
|
+
config.configureClient('bar', {
|
|
68
|
+
baseUri: 'http://localhost:3000',
|
|
69
|
+
responseHandler: new HttpResponseHandler({
|
|
70
|
+
'response-spy': responseSpy,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const localProvider = new HttpClientProvider(config);
|
|
75
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
|
|
76
|
+
|
|
77
|
+
await localProvider.createClient('bar').fetch('/api');
|
|
78
|
+
|
|
79
|
+
expect(responseSpy).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should emit the original path on the request stream', async () => {
|
|
83
|
+
const requestSpy = vi.fn();
|
|
84
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
|
|
85
|
+
|
|
86
|
+
const client = provider.createClient('foo');
|
|
87
|
+
const subscription = client.request$.subscribe(requestSpy);
|
|
88
|
+
|
|
89
|
+
await client.fetch('/api');
|
|
90
|
+
subscription.unsubscribe();
|
|
91
|
+
|
|
92
|
+
expect(requestSpy).toHaveBeenCalledWith(
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
path: '/api',
|
|
95
|
+
uri: 'http://localhost:3000/api',
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should throw when a configured client cannot be found', () => {
|
|
101
|
+
expect(() => provider.createClient('missing-client')).toThrowError(
|
|
102
|
+
new ClientNotFoundException('No registered http client for key [missing-client]'),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should treat a bare hostname as https:// URL with a warning', () => {
|
|
107
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
108
|
+
const client = provider.createClient('api.example.com');
|
|
109
|
+
expect(client.uri).toBe('https://api.example.com');
|
|
110
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
111
|
+
expect.stringContaining('missing the http:// or https:// protocol'),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('HttpClientMsal', () => {
|
|
117
|
+
it('should not mutate the provided request init object', () => {
|
|
118
|
+
const client = new HttpClientMsal('http://localhost:3000');
|
|
119
|
+
client.defaultScopes = ['scope.default'];
|
|
120
|
+
|
|
121
|
+
const init = { scopes: ['scope.request'] };
|
|
122
|
+
|
|
123
|
+
client.fetch$('/api', init);
|
|
124
|
+
|
|
125
|
+
expect(init).toStrictEqual({ scopes: ['scope.request'] });
|
|
126
|
+
});
|
|
48
127
|
});
|
package/tests/operators.test.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
2
|
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { lastValueFrom, of } from 'rxjs';
|
|
4
|
+
import { toArray } from 'rxjs/operators';
|
|
3
5
|
import {
|
|
4
6
|
capitalizeRequestMethodOperator,
|
|
5
7
|
type ProcessOperator,
|
|
6
8
|
requestValidationOperator,
|
|
9
|
+
sseMap,
|
|
7
10
|
} from '../src/lib/operators';
|
|
8
11
|
import type { FetchRequest } from '../src/lib';
|
|
9
12
|
|
|
@@ -107,3 +110,24 @@ describe('requestValidationOperator', () => {
|
|
|
107
110
|
await expect(result).rejects.toThrowError('RFC 2615');
|
|
108
111
|
});
|
|
109
112
|
});
|
|
113
|
+
|
|
114
|
+
describe('sseMap', () => {
|
|
115
|
+
it('should map SSE responses through the operators index export', async () => {
|
|
116
|
+
const response = new Response(
|
|
117
|
+
new ReadableStream({
|
|
118
|
+
start(controller) {
|
|
119
|
+
controller.enqueue(new TextEncoder().encode('data: {"key": "value"}\n\n'));
|
|
120
|
+
controller.close();
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
{
|
|
124
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
125
|
+
status: 200,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const events = await lastValueFrom(of(response).pipe(sseMap<{ key: string }>(), toArray()));
|
|
130
|
+
|
|
131
|
+
expect(events).toEqual([{ data: { key: 'value' } }]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { ServerSentEventResponseError } from '../src/errors.js';
|
|
3
|
-
import { createSseSelector } from '../src/lib/selectors
|
|
3
|
+
import { createSseSelector, type ServerSentEvent } from '../src/lib/selectors';
|
|
4
4
|
import { of, lastValueFrom } from 'rxjs';
|
|
5
5
|
import { concatMap, scan } from 'rxjs/operators';
|
|
6
|
-
import { set } from 'zod';
|
|
7
6
|
|
|
8
7
|
// Helper to create a mock Response
|
|
9
8
|
function createMockResponse(
|
|
@@ -34,7 +33,7 @@ describe('createSseSelector', () => {
|
|
|
34
33
|
const events = await lastValueFrom(
|
|
35
34
|
of(response).pipe(
|
|
36
35
|
concatMap(selector),
|
|
37
|
-
scan((acc, event) => [...acc, event], [] as
|
|
36
|
+
scan((acc, event) => [...acc, event], [] as ServerSentEvent[]),
|
|
38
37
|
),
|
|
39
38
|
);
|
|
40
39
|
expect(events).toEqual([{ data: { key: 'value' } }]);
|
|
@@ -79,7 +78,7 @@ describe('createSseSelector', () => {
|
|
|
79
78
|
const events = await lastValueFrom(
|
|
80
79
|
of(response).pipe(
|
|
81
80
|
concatMap(selector),
|
|
82
|
-
scan((acc, event) => [...acc, event], [] as
|
|
81
|
+
scan((acc, event) => [...acc, event], [] as ServerSentEvent[]),
|
|
83
82
|
),
|
|
84
83
|
);
|
|
85
84
|
|
|
@@ -98,7 +97,7 @@ describe('createSseSelector', () => {
|
|
|
98
97
|
const events = await lastValueFrom(
|
|
99
98
|
of(response).pipe(
|
|
100
99
|
concatMap(selector),
|
|
101
|
-
scan((acc, event) => [...acc, event], [] as
|
|
100
|
+
scan((acc, event) => [...acc, event], [] as ServerSentEvent[]),
|
|
102
101
|
),
|
|
103
102
|
);
|
|
104
103
|
|
|
@@ -117,7 +116,7 @@ describe('createSseSelector', () => {
|
|
|
117
116
|
const events = await lastValueFrom(
|
|
118
117
|
of(response).pipe(
|
|
119
118
|
concatMap(selector),
|
|
120
|
-
scan((acc, event) => [...acc, event], [] as
|
|
119
|
+
scan((acc, event) => [...acc, event], [] as ServerSentEvent[]),
|
|
121
120
|
),
|
|
122
121
|
);
|
|
123
122
|
|
|
@@ -136,7 +135,7 @@ describe('createSseSelector', () => {
|
|
|
136
135
|
const eventsPromise = lastValueFrom(
|
|
137
136
|
of(response).pipe(
|
|
138
137
|
concatMap(selector),
|
|
139
|
-
scan((acc, event) => [...acc, event], [] as
|
|
138
|
+
scan((acc, event) => [...acc, event], [] as ServerSentEvent[]),
|
|
140
139
|
),
|
|
141
140
|
);
|
|
142
141
|
|