@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,175 +0,0 @@
|
|
|
1
|
-
# Configure HTTP Clients
|
|
2
|
-
|
|
3
|
-
Use HTTP client configuration when your application talks to the same backend more than once and you want one place to define base URLs, MSAL scopes, headers, and transport behavior.
|
|
4
|
-
|
|
5
|
-
The main configuration APIs are:
|
|
6
|
-
|
|
7
|
-
- `configureHttpClient(name, options)` for one named client
|
|
8
|
-
- `configureHttp(...)` for lower-level module configuration
|
|
9
|
-
- `createClient(name)` to create a fresh client instance from a named configuration
|
|
10
|
-
- `createCustomClient<T>(name)` to create a custom client class with the same configuration
|
|
11
|
-
|
|
12
|
-
## Named Clients vs Ad-Hoc Clients
|
|
13
|
-
|
|
14
|
-
| Use case | API | Why |
|
|
15
|
-
| --- | --- | --- |
|
|
16
|
-
| One backend used throughout the app | `configureHttpClient(name, options)` | Keeps base URL, scopes, and shared behavior in one place |
|
|
17
|
-
| Several clients configured together | `configureHttp(...)` | Useful when a module or setup step owns multiple backends |
|
|
18
|
-
| One-off call to a specific base URL | `createClient({ baseUri })` | Useful for inline or temporary configuration |
|
|
19
|
-
| One-off call to an absolute URL | `createClient('https://api.example.com')` | Useful when the URL itself is the configuration |
|
|
20
|
-
|
|
21
|
-
> **Fusion app developers:** Higher-level frameworks like `@equinor/fusion-framework-app` can auto-register named clients from other sources (e.g. application config endpoints or service discovery) before your code runs. See the [`@equinor/fusion-framework-app` README](../../app/README.md) for details on how clients are registered at the app level and which source takes priority.
|
|
22
|
-
|
|
23
|
-
## Quick Start
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
configurator.configureHttpClient('catalog', {
|
|
27
|
-
baseUri: '/api/catalog',
|
|
28
|
-
defaultScopes: ['api://catalog-api/.default'],
|
|
29
|
-
onCreate: (client) => {
|
|
30
|
-
client.requestHandler.setHeader('X-App-Name', 'portal');
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const client = framework.modules.http.createClient('catalog');
|
|
35
|
-
const items = await client.json('/items');
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
This pattern is the normal entry point for application code.
|
|
39
|
-
|
|
40
|
-
## Configuration Options
|
|
41
|
-
|
|
42
|
-
| Option | What it controls |
|
|
43
|
-
| --- | --- |
|
|
44
|
-
| `baseUri` | Base URL used to resolve request paths. Relative values are resolved against `window.location.origin`. |
|
|
45
|
-
| `defaultScopes` | Default MSAL scopes used by `HttpClientMsal` requests. |
|
|
46
|
-
| `ctor` | Custom client constructor when you want domain-specific client methods. |
|
|
47
|
-
| `onCreate` | Callback that runs for every newly created client instance. |
|
|
48
|
-
| `requestHandler` | Initial request handler pipeline cloned into created clients. |
|
|
49
|
-
| `responseHandler` | Initial response handler pipeline passed into created clients. |
|
|
50
|
-
|
|
51
|
-
## Use `onCreate` For Shared Client Behavior
|
|
52
|
-
|
|
53
|
-
`onCreate` is the main place to attach behavior that every fresh client instance should start with.
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
configurator.configureHttpClient('catalog', {
|
|
57
|
-
baseUri: '/api/catalog',
|
|
58
|
-
onCreate: (client) => {
|
|
59
|
-
client.requestHandler.setHeader('X-Feature', 'catalog');
|
|
60
|
-
client.responseHandler.add('reject-unauthorized', (response) => {
|
|
61
|
-
if (response.status === 401) {
|
|
62
|
-
throw new Error('Unauthorized');
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Use this for headers, logging, validation, transport guards, and response policies.
|
|
70
|
-
|
|
71
|
-
## MSAL Scope Behavior
|
|
72
|
-
|
|
73
|
-
When the auth module is available, the HTTP module can acquire an access token before sending the request.
|
|
74
|
-
|
|
75
|
-
- `defaultScopes` come from the configured client
|
|
76
|
-
- per-request `scopes` are appended to `defaultScopes`
|
|
77
|
-
- token acquisition only happens when the final scope list is non-empty
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
await client.json('/items', {
|
|
81
|
-
scopes: ['api://catalog-admin/.default'],
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
That request uses both the configured `defaultScopes` and the request-specific scopes.
|
|
86
|
-
|
|
87
|
-
## Direct Module Integration
|
|
88
|
-
|
|
89
|
-
If you are configuring modules directly instead of using higher-level framework helpers, use the package exports.
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
import {
|
|
93
|
-
configureHttp,
|
|
94
|
-
configureHttpClient,
|
|
95
|
-
} from '@equinor/fusion-framework-module-http';
|
|
96
|
-
|
|
97
|
-
configurator.addConfig(configureHttpClient('catalog', {
|
|
98
|
-
baseUri: '/api/catalog',
|
|
99
|
-
}));
|
|
100
|
-
|
|
101
|
-
configurator.addConfig(configureHttp((http) => {
|
|
102
|
-
http.configureClient('search', {
|
|
103
|
-
baseUri: '/api/search',
|
|
104
|
-
});
|
|
105
|
-
}));
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Use `configureHttpClient(...)` when you only need one named client. Use `configureHttp(...)` when a module setup step owns several client registrations.
|
|
109
|
-
|
|
110
|
-
## Ad-Hoc Clients
|
|
111
|
-
|
|
112
|
-
The provider also supports one-off client creation without a named configuration.
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
const inlineClient = framework.modules.http.createClient({
|
|
116
|
-
baseUri: '/api/search',
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const urlClient = framework.modules.http.createClient('https://api.example.com');
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
If the string passed to `createClient()` is not a configured key but is an absolute `http:` or `https:` URL, the provider treats it as a `baseUri`.
|
|
123
|
-
|
|
124
|
-
## Custom Client Classes
|
|
125
|
-
|
|
126
|
-
Use `ctor` when you want to wrap the shared transport behavior in domain-specific methods.
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
import { HttpClient } from '@equinor/fusion-framework-module-http/client';
|
|
130
|
-
|
|
131
|
-
class ApiClient extends HttpClient {
|
|
132
|
-
getHealth(): Promise<{ status: string }> {
|
|
133
|
-
return this.json('/health');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
configurator.configureHttpClient('api', {
|
|
138
|
-
baseUri: '/api',
|
|
139
|
-
ctor: ApiClient,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const client = framework.modules.http.createCustomClient<ApiClient>('api');
|
|
143
|
-
const health = await client.getHealth();
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## What `createClient()` Does
|
|
147
|
-
|
|
148
|
-
Each call to `createClient()` creates a fresh client instance.
|
|
149
|
-
|
|
150
|
-
- the client starts with its configured `baseUri`
|
|
151
|
-
- `defaultScopes` are assigned to the client instance
|
|
152
|
-
- `requestHandler` and `responseHandler` configuration is applied to that instance
|
|
153
|
-
- `onCreate` runs for that specific instance
|
|
154
|
-
|
|
155
|
-
This matters because headers and handlers added to one created client do not leak into the next created client from the same named configuration.
|
|
156
|
-
|
|
157
|
-
## Missing Clients And Safe Lookup
|
|
158
|
-
|
|
159
|
-
Use `hasClient(name)` when a client configuration may be optional.
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
if (framework.modules.http.hasClient('catalog')) {
|
|
163
|
-
const client = framework.modules.http.createClient('catalog');
|
|
164
|
-
await client.json('/items');
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
If you call `createClient(name)` with an unknown key, the provider throws `ClientNotFoundException`.
|
|
169
|
-
|
|
170
|
-
## Rule Of Thumb
|
|
171
|
-
|
|
172
|
-
- use named clients for stable backends
|
|
173
|
-
- use `onCreate` for shared transport behavior
|
|
174
|
-
- use `ctor` when you want domain-specific methods
|
|
175
|
-
- use ad-hoc clients only when the configuration is truly local
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# Observable Patterns
|
|
2
|
-
|
|
3
|
-
The observable API exposes the same HTTP transport pipeline as the promise API, but in a form that composes naturally with RxJS.
|
|
4
|
-
|
|
5
|
-
Use `fetch$()`, `json$()`, `blob$()`, and `sse$()` when your request depends on other streams, needs cancellation by unsubscribe, or should participate in a larger RxJS workflow.
|
|
6
|
-
|
|
7
|
-
## Promise Methods vs Observable Methods
|
|
8
|
-
|
|
9
|
-
| Need | Recommended API |
|
|
10
|
-
| --- | --- |
|
|
11
|
-
| One request and one result | `fetch()` or `json()` |
|
|
12
|
-
| RxJS composition | `fetch$()` or `json$()` |
|
|
13
|
-
| Automatic cancellation when input changes | `fetch$()` or `json$()` with `switchMap` |
|
|
14
|
-
| Observing transport activity | `request$`, `response$`, and the observable request methods |
|
|
15
|
-
| Streaming server events | `sse$()` |
|
|
16
|
-
|
|
17
|
-
Promise methods are implemented with `firstValueFrom(...)` on the corresponding observable method. That means both forms share the same underlying request pipeline.
|
|
18
|
-
|
|
19
|
-
## Core Observable Behavior
|
|
20
|
-
|
|
21
|
-
- observable request methods are cold
|
|
22
|
-
- nothing is sent until you subscribe
|
|
23
|
-
- unsubscribing aborts the underlying fetch
|
|
24
|
-
- `client.abort()` cancels every in-flight request started by that client instance
|
|
25
|
-
- `request$` and `response$` are per-client streams, not global streams
|
|
26
|
-
|
|
27
|
-
## Pattern: Request From Another Stream
|
|
28
|
-
|
|
29
|
-
Use `switchMap` when a new upstream value should replace the previous in-flight request.
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import { switchMap } from 'rxjs/operators';
|
|
33
|
-
|
|
34
|
-
const item$ = selectedId$.pipe(
|
|
35
|
-
switchMap((id) => client.json$(`/items/${id}`)),
|
|
36
|
-
);
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
This pattern works well for route parameters, selected rows, debounced search terms, and refresh triggers.
|
|
40
|
-
|
|
41
|
-
## Pattern: Observe Transport Activity
|
|
42
|
-
|
|
43
|
-
Each client instance exposes `request$` and `response$` so you can observe transport behavior alongside your result stream.
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
const requestLog = client.request$.subscribe((request) => {
|
|
47
|
-
console.debug('request', request.method, request.uri);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const responseLog = client.response$.subscribe((response) => {
|
|
51
|
-
console.debug('response', response.status, response.url);
|
|
52
|
-
});
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Use these streams for diagnostics, telemetry, debugging operators, or building test helpers.
|
|
56
|
-
|
|
57
|
-
## Pattern: Cancel By Unsubscribing
|
|
58
|
-
|
|
59
|
-
Observable requests are a good fit for component and service lifecycles because cancellation is built in.
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
const subscription = client.json$('/items').subscribe({
|
|
63
|
-
next: console.log,
|
|
64
|
-
error: console.error,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
subscription.unsubscribe();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
After unsubscribe, the client aborts the underlying fetch.
|
|
71
|
-
|
|
72
|
-
## Pattern: Reuse Parsing With Selectors
|
|
73
|
-
|
|
74
|
-
Selectors work well with observable request methods because they separate transport from parsing.
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const user$ = client.fetch$('/users/42', {
|
|
78
|
-
selector: userSelector,
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
This lets you keep parsing logic reusable while still composing the request with RxJS operators.
|
|
83
|
-
|
|
84
|
-
## Pattern: Use The Right Layer For The Job
|
|
85
|
-
|
|
86
|
-
For most advanced flows, the HTTP module works best when each layer has one responsibility.
|
|
87
|
-
|
|
88
|
-
- use `fetch$()` or `json$()` for transport
|
|
89
|
-
- use selectors for response parsing
|
|
90
|
-
- use request and response handlers for cross-cutting transport behavior
|
|
91
|
-
- use RxJS operators for orchestration and composition
|
|
92
|
-
|
|
93
|
-
## Common Observable Workflows
|
|
94
|
-
|
|
95
|
-
- a route parameter stream drives `json$()` with `switchMap`
|
|
96
|
-
- a user action stream triggers `fetch$()` and `catchError`
|
|
97
|
-
- a request is observed through `request$` for telemetry
|
|
98
|
-
- a response is observed through `response$` for diagnostics
|
|
99
|
-
- an SSE endpoint uses `sse$()` for incremental updates
|
|
100
|
-
|
|
101
|
-
## Rule Of Thumb
|
|
102
|
-
|
|
103
|
-
Start with `json()` if you just need the result of one request. Move to `json$()` or `fetch$()` when the request is part of a reactive workflow.
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# Selectors And Handlers
|
|
2
|
-
|
|
3
|
-
Selectors and handlers are related, but they solve different problems.
|
|
4
|
-
|
|
5
|
-
- selectors transform a `Response` into the value your application consumes
|
|
6
|
-
- request handlers shape or validate the outgoing request
|
|
7
|
-
- response handlers apply transport-level response rules before selector parsing runs
|
|
8
|
-
|
|
9
|
-
Use this split to keep parsing logic separate from transport policy.
|
|
10
|
-
|
|
11
|
-
## Choose The Right Tool
|
|
12
|
-
|
|
13
|
-
| Need | Use |
|
|
14
|
-
| --- | --- |
|
|
15
|
-
| Parse JSON, blobs, or SSE events | selector |
|
|
16
|
-
| Add headers, normalize methods, validate request shape | request handler |
|
|
17
|
-
| Reject certain responses before parsing | response handler |
|
|
18
|
-
| Reuse parsing rules across multiple calls | selector |
|
|
19
|
-
| Reuse transport behavior across a client configuration | handler |
|
|
20
|
-
|
|
21
|
-
## Built-In Selectors
|
|
22
|
-
|
|
23
|
-
| Selector | What it returns | Notes |
|
|
24
|
-
| --- | --- | --- |
|
|
25
|
-
| `jsonSelector` | parsed JSON or `undefined` for `204` | Throws `HttpJsonResponseError` on non-OK responses and includes parsed error payloads when possible |
|
|
26
|
-
| `blobSelector` | `{ filename?, blob }` | Extracts `filename` from the `content-disposition` header when present |
|
|
27
|
-
| `createSseSelector` | `Observable<ServerSentEvent<T>>` | Lower-level SSE selector used by `client.sse$()` |
|
|
28
|
-
|
|
29
|
-
## Custom Selector Example
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import {
|
|
33
|
-
jsonSelector,
|
|
34
|
-
type ResponseSelector,
|
|
35
|
-
} from '@equinor/fusion-framework-module-http/selectors';
|
|
36
|
-
|
|
37
|
-
type User = {
|
|
38
|
-
id: string;
|
|
39
|
-
name: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const userSelector: ResponseSelector<User> = async (response) => {
|
|
43
|
-
const data = await jsonSelector<User>(response);
|
|
44
|
-
return {
|
|
45
|
-
...data,
|
|
46
|
-
name: data.name.trim(),
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
That selector can then be passed to `client.fetch('/users/42', { selector: userSelector })`.
|
|
52
|
-
|
|
53
|
-
## Request And Response Handlers
|
|
54
|
-
|
|
55
|
-
Handlers are sequential operator pipelines attached to each client instance.
|
|
56
|
-
|
|
57
|
-
- `add(key, operator)` throws if the key already exists
|
|
58
|
-
- `set(key, operator)` adds or replaces an operator
|
|
59
|
-
- `remove(key)` removes an operator
|
|
60
|
-
- returning `void` keeps the previous value flowing through the pipeline
|
|
61
|
-
- operators run in insertion order
|
|
62
|
-
|
|
63
|
-
The public handler types and utilities are exported from `@equinor/fusion-framework-module-http/operators`.
|
|
64
|
-
|
|
65
|
-
## Default Request Pipeline
|
|
66
|
-
|
|
67
|
-
Each created client starts with a default request pipeline containing:
|
|
68
|
-
|
|
69
|
-
1. `capitalizeRequestMethodOperator()`
|
|
70
|
-
2. `requestValidationOperator()`
|
|
71
|
-
|
|
72
|
-
That means lowercase methods are normalized to uppercase and request options are validated in non-throwing mode by default.
|
|
73
|
-
|
|
74
|
-
## Built-In Request Operators
|
|
75
|
-
|
|
76
|
-
- `capitalizeRequestMethodOperator(options?)` uppercases `request.method` and can suppress warnings with `silent: true`
|
|
77
|
-
- `requestValidationOperator(options?)` validates the request against the package schema and can optionally return parsed values
|
|
78
|
-
|
|
79
|
-
## Example: Configure Both Parsing And Transport
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
import {
|
|
83
|
-
capitalizeRequestMethodOperator,
|
|
84
|
-
requestValidationOperator,
|
|
85
|
-
} from '@equinor/fusion-framework-module-http/operators';
|
|
86
|
-
|
|
87
|
-
configurator.configureHttpClient('catalog', {
|
|
88
|
-
baseUri: '/api/catalog',
|
|
89
|
-
onCreate: (client) => {
|
|
90
|
-
client.requestHandler.set('validate-strict', requestValidationOperator({
|
|
91
|
-
parse: true,
|
|
92
|
-
strict: true,
|
|
93
|
-
}));
|
|
94
|
-
|
|
95
|
-
client.requestHandler.add('normalize-method', capitalizeRequestMethodOperator());
|
|
96
|
-
|
|
97
|
-
client.responseHandler.add('reject-server-errors', (response) => {
|
|
98
|
-
if (response.status >= 500) {
|
|
99
|
-
throw new Error(`Unexpected response status: ${response.status}`);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Rule Of Thumb
|
|
107
|
-
|
|
108
|
-
- use handlers for cross-cutting transport concerns such as headers, validation, logging, token behavior, or rejecting bad responses
|
|
109
|
-
- use selectors for parsing a response into a domain shape
|
|
110
|
-
- use observables for orchestration and composition
|
|
111
|
-
|
|
112
|
-
When a piece of logic changes how the request travels, it usually belongs in a handler. When it changes what value the caller receives, it usually belongs in a selector.
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Server-Sent Events
|
|
2
|
-
|
|
3
|
-
Use `client.sse$()` when an endpoint returns `text/event-stream` and you want parsed `ServerSentEvent<T>` objects instead of manually reading the response body.
|
|
4
|
-
|
|
5
|
-
This is the highest-level SSE API in the package. If you need lower-level composition, use `createSseSelector` or `sseMap`.
|
|
6
|
-
|
|
7
|
-
## Quick Start
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
const events$ = client.sse$<{ message: string }>('/events', undefined, {
|
|
11
|
-
eventFilter: 'message',
|
|
12
|
-
skipHeartbeats: true,
|
|
13
|
-
});
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Subscribe to the result the same way you would any other observable HTTP call.
|
|
17
|
-
|
|
18
|
-
## `sse$()` Inputs
|
|
19
|
-
|
|
20
|
-
| Input | Where it goes | Purpose |
|
|
21
|
-
| --- | --- | --- |
|
|
22
|
-
| request init | second argument to `sse$()` | HTTP method, body, headers, and `signal` |
|
|
23
|
-
| SSE selector options | third argument to `sse$()` | `eventFilter`, `skipHeartbeats`, and custom parsing |
|
|
24
|
-
|
|
25
|
-
`client.sse$()` uses the request `signal` as the abort signal for the underlying SSE reader.
|
|
26
|
-
|
|
27
|
-
## SSE Options
|
|
28
|
-
|
|
29
|
-
| Option | Type | What it does |
|
|
30
|
-
| --- | --- | --- |
|
|
31
|
-
| `eventFilter` | `string | string[]` | Emits only matching named SSE events |
|
|
32
|
-
| `skipHeartbeats` | `boolean` | Skips empty heartbeat events and named `heartbeat` or `ping` events |
|
|
33
|
-
| `dataParser` | `(data: string) => T` | Overrides the default parser for the `data:` field |
|
|
34
|
-
|
|
35
|
-
By default, the parser tries `JSON.parse(...)` first and falls back to the raw string when parsing fails.
|
|
36
|
-
|
|
37
|
-
## What `sse$()` Does For You
|
|
38
|
-
|
|
39
|
-
- adds `Accept: text/event-stream`
|
|
40
|
-
- adds `Content-Type: text/event-stream`
|
|
41
|
-
- adds `Cache-Control: no-cache` and `Connection: keep-alive`
|
|
42
|
-
- validates that the response is a readable `text/event-stream`
|
|
43
|
-
- parses `data:` fields into `ServerSentEvent<T>` objects
|
|
44
|
-
- honors `retry:` directives by delaying before continuing to read
|
|
45
|
-
- stops reading when the observable is unsubscribed or the abort signal fires
|
|
46
|
-
|
|
47
|
-
## Abort Behavior
|
|
48
|
-
|
|
49
|
-
Pass an `AbortSignal` through the request init when you need explicit cancellation.
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
const abortController = new AbortController();
|
|
53
|
-
|
|
54
|
-
const events$ = client.sse$<{ message: string }>(
|
|
55
|
-
'/events',
|
|
56
|
-
{ signal: abortController.signal },
|
|
57
|
-
{ skipHeartbeats: true },
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
abortController.abort();
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Unsubscribing from the observable also stops the underlying stream.
|
|
64
|
-
|
|
65
|
-
## Lower-Level SSE APIs
|
|
66
|
-
|
|
67
|
-
### `createSseSelector`
|
|
68
|
-
|
|
69
|
-
Use `createSseSelector` when you want to compose SSE handling with `fetch()` or `fetch$()` yourself.
|
|
70
|
-
|
|
71
|
-
Unlike `client.sse$()`, the lower-level SSE APIs do not add SSE request headers for you. Add the headers your endpoint expects, especially `Accept: text/event-stream`.
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
import { createSseSelector } from '@equinor/fusion-framework-module-http/selectors';
|
|
75
|
-
|
|
76
|
-
const selector = createSseSelector<{ message: string }>({
|
|
77
|
-
eventFilter: ['message', 'update'],
|
|
78
|
-
skipHeartbeats: true,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const events$ = client.fetch$('/events', {
|
|
82
|
-
headers: {
|
|
83
|
-
Accept: 'text/event-stream',
|
|
84
|
-
'Cache-Control': 'no-cache',
|
|
85
|
-
Connection: 'keep-alive',
|
|
86
|
-
},
|
|
87
|
-
selector,
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### `sseMap`
|
|
92
|
-
|
|
93
|
-
Use `sseMap` when you already have a response observable and want to transform that stream into SSE events.
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
import { sseMap } from '@equinor/fusion-framework-module-http/operators';
|
|
97
|
-
|
|
98
|
-
const events$ = client.fetch$('/events', {
|
|
99
|
-
headers: {
|
|
100
|
-
Accept: 'text/event-stream',
|
|
101
|
-
'Cache-Control': 'no-cache',
|
|
102
|
-
Connection: 'keep-alive',
|
|
103
|
-
},
|
|
104
|
-
}).pipe(
|
|
105
|
-
sseMap<{ message: string }>({
|
|
106
|
-
skipHeartbeats: true,
|
|
107
|
-
}),
|
|
108
|
-
);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Error Handling
|
|
112
|
-
|
|
113
|
-
The SSE APIs throw `ServerSentEventResponseError` when:
|
|
114
|
-
|
|
115
|
-
- the response is not OK
|
|
116
|
-
- the response body is not readable
|
|
117
|
-
- the response does not advertise `text/event-stream`
|
|
118
|
-
|
|
119
|
-
Handle SSE errors the same way you handle any other observable request error.
|
|
120
|
-
|
|
121
|
-
## Rule Of Thumb
|
|
122
|
-
|
|
123
|
-
- use `sse$()` for normal application code
|
|
124
|
-
- use `createSseSelector` when you want selector-level control
|
|
125
|
-
- use `sseMap` when you already have a response observable pipeline
|
package/docs/testing.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
There is no separate mock client or configurator — register a short-circuiting middleware
|
|
4
|
-
through `configurator.http.addMiddleware(...)` on the real configurator instead. The real
|
|
5
|
-
configurator API — `configureClient`, `baseUri`, `defaultScopes`, `requestHandler`, `onCreate`
|
|
6
|
-
— all still applies; a middleware only wraps the network call itself.
|
|
7
|
-
|
|
8
|
-
## Quick Start
|
|
9
|
-
|
|
10
|
-
```typescript
|
|
11
|
-
configurator.configureHttpClient('catalog', { baseUri: 'https://api.example.com' });
|
|
12
|
-
configurator.http.addMiddleware(async (uri, init, next) =>
|
|
13
|
-
uri === 'https://api.example.com/items' ? Response.json([{ id: 1 }]) : next(uri, init),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
const items = await fusion.modules.http.createClient('catalog').json('/items');
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## The Middleware Contract
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
type HttpMiddleware = (
|
|
23
|
-
uri: string,
|
|
24
|
-
init: RequestInit,
|
|
25
|
-
next: (uri: string, init: RequestInit) => Promise<Response>,
|
|
26
|
-
) => Response | Promise<Response> | Observable<Response>;
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
- **A middleware decides for itself whether to answer or fall through** — return a `Response` to
|
|
30
|
-
handle the request, or call and return `next(uri, init)` to continue to whichever middleware
|
|
31
|
-
(or the real network call) is registered next. There is no separate "declined" return value —
|
|
32
|
-
unlike a router, this is an onion-style chain, so a middleware can also inspect what `next(...)`
|
|
33
|
-
resolves to and decide based on that (a retry, for example).
|
|
34
|
-
- **Registration order is outermost-first.** The first middleware registered via `addMiddleware`
|
|
35
|
-
wraps every other one, including the real network call — so it sees the request first and the
|
|
36
|
-
response last.
|
|
37
|
-
- **A test middleware composes with real app config unchanged** — `addMiddleware` wraps
|
|
38
|
-
`_performFetch` rather than replacing it, so the exact same client and configuration a real
|
|
39
|
-
app registers is what a test exercises; only the boundary that would reach the network is
|
|
40
|
-
short-circuited.
|
|
41
|
-
|
|
42
|
-
## Faking An Entire OpenAPI Document
|
|
43
|
-
|
|
44
|
-
`createOpenApiMockMiddleware` (`@equinor/fusion-framework-module-http/mock`) adapts an
|
|
45
|
-
[`@equinor/fusion-openapi-mock`](../../utils/openapi-mock) instance into an `HttpMiddleware`,
|
|
46
|
-
so a real `openapi.json`/`openapi.yaml` fakes every response until a specific operation needs
|
|
47
|
-
overriding:
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
import { createOpenApiMock } from '@equinor/fusion-openapi-mock';
|
|
51
|
-
import { createOpenApiMockMiddleware } from '@equinor/fusion-framework-module-http/mock';
|
|
52
|
-
|
|
53
|
-
const openApiMock = createOpenApiMock(openApiDocument, { seed: 42 });
|
|
54
|
-
|
|
55
|
-
configurator.configureHttpClient('catalog', { baseUri: 'https://api.example.com' });
|
|
56
|
-
configurator.http.addMiddleware(createOpenApiMockMiddleware(openApiMock));
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
A request that matches no operation in the document falls through to `next`, so this composes
|
|
60
|
-
with other middleware, or the real network call, registered around it.
|
|
61
|
-
|
|
62
|
-
## Asserting Calls With `vi.fn`
|
|
63
|
-
|
|
64
|
-
A middleware is a plain function, so a `vi.fn` spy works as one directly:
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
const middleware = vi.fn(async () => Response.json({ ok: true }));
|
|
68
|
-
configurator.http.addMiddleware(middleware);
|
|
69
|
-
|
|
70
|
-
await client.json('/items');
|
|
71
|
-
|
|
72
|
-
expect(middleware).toHaveBeenCalledOnce();
|
|
73
|
-
const [uri, init] = middleware.mock.calls[0];
|
|
74
|
-
expect(init.method ?? 'GET').toBe('GET');
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
See [`@equinor/fusion-framework/mock`](../../../framework/docs/testing-extending.md) to mock every
|
|
78
|
-
framework boundary at once.
|