opinionated-machine 10.2.0 → 10.4.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 +13 -0
- package/README.md +92 -2
- package/dist/lib/sse/SSESessionSpy.d.ts +5 -1
- package/dist/lib/sse/SSESessionSpy.js.map +1 -1
- package/dist/lib/testing/index.d.ts +2 -2
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/sseHttpClient.d.ts +72 -3
- package/dist/lib/testing/sseHttpClient.js +112 -22
- package/dist/lib/testing/sseHttpClient.js.map +1 -1
- package/dist/lib/testing/sseInjectClient.d.ts +17 -0
- package/dist/lib/testing/sseInjectClient.js +31 -0
- package/dist/lib/testing/sseInjectClient.js.map +1 -1
- package/dist/lib/testing/sseTestTypes.d.ts +40 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# opinionated-machine
|
|
2
2
|
|
|
3
|
+
## 10.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8a388e7: Expose the response body on `SSEInjectConnection`: `getBody()` returns the raw body string and `json<T>()` parses it as JSON, mirroring Fastify's inject response. This lets tests using the untyped `SSEInjectClient` assert on JSON error bodies that an SSE route sends before streaming starts (auth failures, validation errors, unavailable integrations), which previously were unreachable.
|
|
8
|
+
|
|
9
|
+
## 10.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 197d746: Support non-GET requests in `SSEHttpClient.connect()` via new `method` and `body` connect options, so POST/PUT/PATCH SSE endpoints can be tested over real HTTP instead of only through `SSEInjectClient`. `method` is accepted in either case, matching the lowercase spelling used by route contracts. Bodies `fetch()` sends natively (strings, `URLSearchParams`, `FormData`, `Blob`, `ArrayBuffer`, typed arrays, `ReadableStream`) are passed through untouched, everything else is JSON-stringified, and `content-type: application/json` is only defaulted when it does not overwrite an encoding `fetch()` describes itself. The response body is now locked lazily, so `client.response.json()` still works for endpoints that answered with a regular HTTP response (e.g. an error raised before `sse.start()`), and a bodiless response is reported when events are consumed rather than from `connect()`. `awaitServerConnection` now matches the request method as well as the URL, so a path served by both a GET and a POST route resolves the right session; `SpiedSSESession` accordingly requires `request.method`, which every Fastify-backed session already carries.
|
|
14
|
+
- 197d746: Type `SSEConnectOptions.method` as the new exported `SSEInjectMethod`, derived from Fastify's own inject options instead of a hand-listed `'GET' | 'POST' | 'PUT' | 'PATCH'` union. `SSEInjectClient.connectWithBody()` now accepts every method `inject()` accepts (including `DELETE`, `HEAD`, `OPTIONS` and the lowercase spellings), and consumers can import the union instead of redeclaring it.
|
|
15
|
+
|
|
3
16
|
## 10.2.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1579,6 +1579,8 @@ it('streams chat completions', async () => {
|
|
|
1579
1579
|
})
|
|
1580
1580
|
```
|
|
1581
1581
|
|
|
1582
|
+
If the route answers with an error status before streaming starts, the response carries a JSON body instead of events - read it with `conn.getBody()` or `conn.json()` (see [SSEInjectClient](#sseinjectclient)).
|
|
1583
|
+
|
|
1582
1584
|
#### Asserting documented error responses with `bodyForStatus`
|
|
1583
1585
|
|
|
1584
1586
|
When a contract declares `responseBodySchemasByStatusCode` for non-2xx responses (the shape the handler emits via `sse.respond(status, body)` before streaming starts), `injectSSE` / `injectPayloadSSE` expose a typed `bodyForStatus(status)` accessor:
|
|
@@ -2251,12 +2253,14 @@ The library provides utilities for testing SSE endpoints.
|
|
|
2251
2253
|
| **Event delivery** | All events returned at once (after handler closes) | Events arrive incrementally |
|
|
2252
2254
|
| **Connection lifecycle** | Handler must close for request to complete | Can stay open indefinitely |
|
|
2253
2255
|
| **Server requirement** | No `listen()` needed | Requires a listening server (`SSETestServer.start(app)` or manual `app.listen()`) |
|
|
2254
|
-
| **
|
|
2256
|
+
| **Request body** | `injectPayloadSSE` / `connectWithBody` | `method` + `body` connect options |
|
|
2257
|
+
| **Assertions before the handler finishes** | Not possible (response is buffered) | `client.response` is available as soon as headers arrive |
|
|
2258
|
+
| **Best for** | `autoClose` SSE (OpenAI-style, batch exports) | `keepAlive` SSE (notifications, live feeds, rooms), streams whose headers must be asserted mid-handler |
|
|
2255
2259
|
| **Dual-mode sync** | Use `app.inject()` with `accept: 'application/json'` | Same |
|
|
2256
2260
|
|
|
2257
2261
|
#### SSEHttpClient
|
|
2258
2262
|
|
|
2259
|
-
For testing `keepAlive` SSE connections using real HTTP. Requires a listening server — use `SSETestServer.start(app)` to start your app on a random port:
|
|
2263
|
+
For testing `keepAlive` SSE connections using real HTTP, and for any assertion that has to happen on the wire while the handler is still running. Supports `GET`, `POST`, `PUT` and `PATCH`. Requires a listening server — use `SSETestServer.start(app)` to start your app on a random port:
|
|
2260
2264
|
|
|
2261
2265
|
```ts
|
|
2262
2266
|
import { SSEHttpClient } from 'opinionated-machine'
|
|
@@ -2368,6 +2372,65 @@ expect(client.response.headers.get('content-type')).toContain('text/event-stream
|
|
|
2368
2372
|
client.close()
|
|
2369
2373
|
```
|
|
2370
2374
|
|
|
2375
|
+
**POST/PUT/PATCH endpoints**
|
|
2376
|
+
|
|
2377
|
+
`connect()` also issues non-GET requests, so SSE endpoints that take a request body can be tested over real HTTP. Pass `method` and `body`. The method is accepted in either case, so the lowercase spelling your contracts already use (`method: 'post'`) works as-is:
|
|
2378
|
+
|
|
2379
|
+
```ts
|
|
2380
|
+
const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
|
|
2381
|
+
method: 'POST',
|
|
2382
|
+
body: { message: 'Hello', stream: true },
|
|
2383
|
+
})
|
|
2384
|
+
|
|
2385
|
+
const events = await client.collectEvents((event) => event.event === 'done')
|
|
2386
|
+
```
|
|
2387
|
+
|
|
2388
|
+
Bodies `fetch()` can send natively — strings, `URLSearchParams`, `FormData`, `Blob`/`File`, `ArrayBuffer`, typed arrays (`Buffer`, `Uint8Array`, …) and `ReadableStream` — are passed through untouched; anything else is JSON-stringified. `content-type: application/json` is defaulted for JSON-stringified and string bodies (so a raw string stays verbatim, which is handy for asserting on malformed payloads), unless you provide your own content type. Payloads that describe their own encoding — `URLSearchParams`, `FormData`, `Blob` — keep the content type `fetch()` gives them:
|
|
2389
|
+
|
|
2390
|
+
```ts
|
|
2391
|
+
// Sent as application/x-www-form-urlencoded, not JSON-stringified into `{}`
|
|
2392
|
+
const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
|
|
2393
|
+
method: 'post',
|
|
2394
|
+
body: new URLSearchParams({ message: 'Hello' }),
|
|
2395
|
+
})
|
|
2396
|
+
```
|
|
2397
|
+
|
|
2398
|
+
A body without a non-GET `method` throws — `fetch()` cannot attach one to a GET request.
|
|
2399
|
+
|
|
2400
|
+
**Asserting on the wire before the handler finishes**
|
|
2401
|
+
|
|
2402
|
+
`connect()` resolves as soon as HTTP headers arrive, and `client.response` is populated at that point. That is what makes the "open the stream before the slow work starts" behaviour testable: assert on status and headers while the handler is still awaiting its slow call, then let it proceed.
|
|
2403
|
+
|
|
2404
|
+
```ts
|
|
2405
|
+
// Handler calls sse.start() and only then makes its slow LLM call
|
|
2406
|
+
const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
|
|
2407
|
+
method: 'POST',
|
|
2408
|
+
body: { message: 'Hello', stream: true },
|
|
2409
|
+
})
|
|
2410
|
+
|
|
2411
|
+
// Already on the wire while the LLM call is still in flight
|
|
2412
|
+
expect(client.response.status).toBe(200)
|
|
2413
|
+
expect(client.response.headers.get('content-type')).toContain('text/event-stream')
|
|
2414
|
+
|
|
2415
|
+
releaseSlowCall()
|
|
2416
|
+
const events = await client.collectEvents((event) => event.event === 'done')
|
|
2417
|
+
```
|
|
2418
|
+
|
|
2419
|
+
The mirror case works too: a failure raised *before* `sse.start()` reaches the client as the JSON status the contract declares, not as a terminal `error` event. The response body is only locked once you start consuming events, so it can still be read as JSON:
|
|
2420
|
+
|
|
2421
|
+
```ts
|
|
2422
|
+
const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
|
|
2423
|
+
method: 'POST',
|
|
2424
|
+
body: { message: 'Hello', stream: true },
|
|
2425
|
+
})
|
|
2426
|
+
|
|
2427
|
+
expect(client.response.status).toBe(503)
|
|
2428
|
+
expect(client.response.headers.get('content-type')).not.toContain('text/event-stream')
|
|
2429
|
+
expect(await client.response.json()).toEqual({ message: 'Upstream unavailable' })
|
|
2430
|
+
```
|
|
2431
|
+
|
|
2432
|
+
Read that body *before* `close()`: closing aborts the request, so a body read after it (or from a `finally { client.close() }` block that runs first) rejects with an `AbortError`.
|
|
2433
|
+
|
|
2371
2434
|
#### SSEInjectClient
|
|
2372
2435
|
|
|
2373
2436
|
For testing `autoClose` SSE streams (like OpenAI completions). Uses Fastify's `inject()` - no `app.listen()` needed:
|
|
@@ -2388,12 +2451,39 @@ const conn = await client.connectWithBody(
|
|
|
2388
2451
|
{ model: 'gpt-4', messages: [...], stream: true },
|
|
2389
2452
|
)
|
|
2390
2453
|
|
|
2454
|
+
// Any other method inject() accepts works too - the option is typed as
|
|
2455
|
+
// `SSEInjectMethod`, exported so you never have to redeclare that union
|
|
2456
|
+
const conn = await client.connectWithBody(
|
|
2457
|
+
'/api/exports/42',
|
|
2458
|
+
{ reason: 'cleanup' },
|
|
2459
|
+
{ method: 'DELETE' },
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2391
2462
|
// All events are available immediately (inject waits for handler to complete)
|
|
2392
2463
|
expect(conn.getStatusCode()).toBe(200)
|
|
2393
2464
|
const events = conn.getReceivedEvents()
|
|
2394
2465
|
const chunks = events.filter(e => e.event === 'chunk')
|
|
2395
2466
|
```
|
|
2396
2467
|
|
|
2468
|
+
When the route answers with a status code *before* streaming starts (auth failure,
|
|
2469
|
+
validation error, integration unavailable), it sends a JSON body rather than events.
|
|
2470
|
+
`getBody()` returns that body raw and `json()` parses it, mirroring Fastify's own
|
|
2471
|
+
inject response:
|
|
2472
|
+
|
|
2473
|
+
```ts
|
|
2474
|
+
const conn = await client.connect('/api/export/progress')
|
|
2475
|
+
|
|
2476
|
+
expect(conn.getStatusCode()).toBe(503)
|
|
2477
|
+
expect(conn.json<{ errorCode: string }>()).toMatchObject({
|
|
2478
|
+
errorCode: 'INTEGRATION_NOT_AVAILABLE',
|
|
2479
|
+
})
|
|
2480
|
+
```
|
|
2481
|
+
|
|
2482
|
+
`json()` throws if the body is empty or isn't valid JSON, so it only makes sense for
|
|
2483
|
+
these pre-stream responses - a `text/event-stream` body is not JSON. For contract-typed
|
|
2484
|
+
tests, `injectSSE`/`injectPayloadSSE`/`injectApiSSE` offer `bodyForStatus(status)`, which
|
|
2485
|
+
also validates the body against the contract's schema for that status.
|
|
2486
|
+
|
|
2397
2487
|
#### Contract-Aware Inject Helpers
|
|
2398
2488
|
|
|
2399
2489
|
For typed testing with SSE contracts:
|
|
@@ -4,12 +4,16 @@ import type { SSESession } from './AbstractSSEController.ts';
|
|
|
4
4
|
*
|
|
5
5
|
* Both the `SSESession` produced by this package's own SSE routes and the one
|
|
6
6
|
* produced by `@lokalise/fastify-api-contracts` (used by `buildApiRoute`)
|
|
7
|
-
* satisfy it, which lets a single spy be attached to either route style.
|
|
7
|
+
* satisfy it, which lets a single spy be attached to either route style. Both
|
|
8
|
+
* carry a Fastify request, so `method` is always there to match a waiter
|
|
9
|
+
* against - a path served by both a GET and a POST route needs it to resolve
|
|
10
|
+
* the right session.
|
|
8
11
|
*/
|
|
9
12
|
export type SpiedSSESession = {
|
|
10
13
|
id: string;
|
|
11
14
|
request: {
|
|
12
15
|
url: string;
|
|
16
|
+
method: string;
|
|
13
17
|
};
|
|
14
18
|
};
|
|
15
19
|
export type SSESessionEvent<TSession extends SpiedSSESession = SSESession> = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSESessionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSESessionSpy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SSESessionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSESessionSpy.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { injectApiSSE } from './apiSseInjectHelpers.js';
|
|
2
2
|
export type { ApiDeclaredResponseBody, ApiDeclaredResponseStatus, ApiSSEEvent, ApiSSEEventReader, InjectApiSSEParams, InjectApiSSEResult, } from './apiSseTestTypes.js';
|
|
3
|
-
export { type HasSessionSpy, SSEHttpClient, type SSEHttpConnectOptions, type SSEHttpConnectResult, type SSEHttpConnectWithSessionSpyOptions, type SSEHttpConnectWithSpyOptions, } from './sseHttpClient.js';
|
|
3
|
+
export { type HasSessionSpy, SSEHttpClient, type SSEHttpConnectOptions, type SSEHttpConnectResult, type SSEHttpConnectWithSessionSpyOptions, type SSEHttpConnectWithSpyOptions, type SSEHttpMethod, } from './sseHttpClient.js';
|
|
4
4
|
export { SSEInjectClient, SSEInjectConnection } from './sseInjectClient.js';
|
|
5
5
|
export { injectPayloadSSE, injectSSE } from './sseInjectHelpers.js';
|
|
6
6
|
export { type CreateSSESessionSpyResult, createSSESessionSpy, type SSESessionSpyHooks, type SSESessionSpyRouteOptions, } from './sseSessionSpyFactory.js';
|
|
7
7
|
export { SSETestServer } from './sseTestServer.js';
|
|
8
|
-
export type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult, SSEConnectOptions, SSEResponse, SSETestConnection, } from './sseTestTypes.js';
|
|
8
|
+
export type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult, SSEConnectOptions, SSEInjectMethod, SSEResponse, SSETestConnection, } from './sseTestTypes.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AASvD,OAAO,EAEL,aAAa,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AASvD,OAAO,EAEL,aAAa,GAMd,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EAEL,mBAAmB,GAGpB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
|
|
@@ -7,6 +7,16 @@ import { type ParsedSSEEvent } from '../sse/sseParser.ts';
|
|
|
7
7
|
export type HasSessionSpy = {
|
|
8
8
|
connectionSpy: SSESessionSpy;
|
|
9
9
|
};
|
|
10
|
+
/** Canonical, on-the-wire spelling of a supported method. */
|
|
11
|
+
type NormalizedSSEHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH';
|
|
12
|
+
/**
|
|
13
|
+
* HTTP methods supported when connecting to an SSE endpoint.
|
|
14
|
+
*
|
|
15
|
+
* Both spellings are accepted and normalized internally, so the lowercase form
|
|
16
|
+
* used by route contracts (`buildContract({ method: 'post' })`) can be handed
|
|
17
|
+
* over as-is.
|
|
18
|
+
*/
|
|
19
|
+
export type SSEHttpMethod = NormalizedSSEHttpMethod | Lowercase<NormalizedSSEHttpMethod>;
|
|
10
20
|
/**
|
|
11
21
|
* Options for connecting to an SSE endpoint via HTTP.
|
|
12
22
|
*/
|
|
@@ -15,6 +25,24 @@ export type SSEHttpConnectOptions = {
|
|
|
15
25
|
query?: Record<string, string | undefined>;
|
|
16
26
|
/** Additional headers to send with the request */
|
|
17
27
|
headers?: Record<string, string>;
|
|
28
|
+
/** HTTP method to use, upper- or lowercase (default: 'GET') */
|
|
29
|
+
method?: SSEHttpMethod;
|
|
30
|
+
/**
|
|
31
|
+
* Request body.
|
|
32
|
+
*
|
|
33
|
+
* Values `fetch()` can send natively - strings, `URLSearchParams`, `FormData`,
|
|
34
|
+
* `Blob`/`File`, `ArrayBuffer`, typed arrays (`Buffer`, `Uint8Array`, ...) and
|
|
35
|
+
* `ReadableStream` - are passed through untouched. Anything else is
|
|
36
|
+
* JSON-stringified.
|
|
37
|
+
*
|
|
38
|
+
* `content-type: application/json` is defaulted for JSON-stringified and string
|
|
39
|
+
* bodies, unless `headers` already provides a content type. Bodies that describe
|
|
40
|
+
* their own encoding (`URLSearchParams`, `FormData`, `Blob`) are left for
|
|
41
|
+
* `fetch()` to label, so their content type is never overwritten with a wrong one.
|
|
42
|
+
*
|
|
43
|
+
* Requires a non-GET `method`.
|
|
44
|
+
*/
|
|
45
|
+
body?: unknown;
|
|
18
46
|
};
|
|
19
47
|
/**
|
|
20
48
|
* Options for connecting with automatic server-side connection waiting,
|
|
@@ -70,6 +98,13 @@ export type SSEHttpConnectResult<TSession extends SpiedSSESession = SSESession>
|
|
|
70
98
|
* - **Long-lived connections** that stay open indefinitely
|
|
71
99
|
* - **Real-time notifications** where events arrive over time
|
|
72
100
|
* - **Push-based streaming** where the client waits for server-initiated events
|
|
101
|
+
* - **Assertions on the wire while the handler is still running** - `connect()`
|
|
102
|
+
* resolves as soon as headers arrive, so `response.status` / `response.headers`
|
|
103
|
+
* can be checked before the handler produces its first event
|
|
104
|
+
*
|
|
105
|
+
* GET, POST, PUT and PATCH are all supported (see `method` / `body` in
|
|
106
|
+
* {@link SSEHttpConnectOptions}), so POST SSE endpoints that take a request
|
|
107
|
+
* body can be tested over real HTTP too.
|
|
73
108
|
*
|
|
74
109
|
* **When to use SSEHttpClient vs SSEInjectClient:**
|
|
75
110
|
*
|
|
@@ -113,14 +148,33 @@ export type SSEHttpConnectResult<TSession extends SpiedSSESession = SSESession>
|
|
|
113
148
|
* ```
|
|
114
149
|
*/
|
|
115
150
|
export declare class SSEHttpClient {
|
|
116
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The fetch Response object. Available immediately after connect() returns,
|
|
153
|
+
* before any event is consumed, so status and headers can be asserted while
|
|
154
|
+
* the handler is still running.
|
|
155
|
+
*
|
|
156
|
+
* The response body is only locked once events are first consumed, so
|
|
157
|
+
* `response.json()` / `response.text()` still work for endpoints that
|
|
158
|
+
* answered with a regular HTTP response instead of a stream (for example an
|
|
159
|
+
* error raised before `sse.start()`). Read that body *before* calling
|
|
160
|
+
* `close()` - `close()` aborts the request, which rejects any pending or
|
|
161
|
+
* subsequent body read with an `AbortError`.
|
|
162
|
+
*/
|
|
117
163
|
readonly response: Response;
|
|
118
164
|
private readonly abortController;
|
|
119
|
-
private readonly
|
|
165
|
+
private readonly responseBody;
|
|
166
|
+
private streamReader;
|
|
120
167
|
private readonly decoder;
|
|
121
168
|
private buffer;
|
|
122
169
|
private closed;
|
|
123
170
|
private constructor();
|
|
171
|
+
/**
|
|
172
|
+
* Lazily acquire the stream reader, locking the response body on first use.
|
|
173
|
+
*
|
|
174
|
+
* A missing body is reported here rather than from the constructor, so a
|
|
175
|
+
* bodiless response (e.g. `204`) can still be inspected via `response`.
|
|
176
|
+
*/
|
|
177
|
+
private get reader();
|
|
124
178
|
/**
|
|
125
179
|
* Connect to an SSE endpoint.
|
|
126
180
|
*
|
|
@@ -130,7 +184,7 @@ export declare class SSEHttpClient {
|
|
|
130
184
|
*
|
|
131
185
|
* @param baseUrl - Base URL of the server (e.g., 'http://localhost:3000')
|
|
132
186
|
* @param path - SSE endpoint path (e.g., '/api/notifications')
|
|
133
|
-
* @param options - Connection options (query params, headers)
|
|
187
|
+
* @param options - Connection options (method, body, query params, headers)
|
|
134
188
|
* @returns Connected SSE client ready to receive events
|
|
135
189
|
*
|
|
136
190
|
* @example
|
|
@@ -142,6 +196,16 @@ export declare class SSEHttpClient {
|
|
|
142
196
|
* { query: { userId: '123' }, headers: { authorization: 'Bearer token' } }
|
|
143
197
|
* )
|
|
144
198
|
*
|
|
199
|
+
* // POST with a JSON body (content-type defaults to application/json)
|
|
200
|
+
* const client = await SSEHttpClient.connect(
|
|
201
|
+
* 'http://localhost:3000',
|
|
202
|
+
* '/api/chat/completions',
|
|
203
|
+
* { method: 'POST', body: { message: 'Hello', stream: true } }
|
|
204
|
+
* )
|
|
205
|
+
* // Headers are on the wire before the handler finished its slow work
|
|
206
|
+
* expect(client.response.status).toBe(200)
|
|
207
|
+
* expect(client.response.headers.get('content-type')).toContain('text/event-stream')
|
|
208
|
+
*
|
|
145
209
|
* // With awaitServerConnection (waits for server-side registration)
|
|
146
210
|
* const { client, serverConnection } = await SSEHttpClient.connect(
|
|
147
211
|
* 'http://localhost:3000',
|
|
@@ -230,6 +294,11 @@ export declare class SSEHttpClient {
|
|
|
230
294
|
*
|
|
231
295
|
* This aborts the underlying fetch request. Call this when done
|
|
232
296
|
* consuming events to clean up resources.
|
|
297
|
+
*
|
|
298
|
+
* Aborting also discards any unread response body, so for a non-stream
|
|
299
|
+
* response (an error raised before `sse.start()`, say) read
|
|
300
|
+
* `response.json()` / `response.text()` before calling this.
|
|
233
301
|
*/
|
|
234
302
|
close(): void;
|
|
235
303
|
}
|
|
304
|
+
export {};
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
import { stringify } from 'fast-querystring';
|
|
2
2
|
import { parseSSEBuffer } from "../sse/sseParser.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether `fetch()` can send this value as-is. Everything else is JSON-stringified,
|
|
5
|
+
* so binary and form payloads are not silently mangled into `{}` or `{"0":1}`.
|
|
6
|
+
*/
|
|
7
|
+
function isFetchBody(body) {
|
|
8
|
+
return (typeof body === 'string' ||
|
|
9
|
+
body instanceof URLSearchParams ||
|
|
10
|
+
body instanceof FormData ||
|
|
11
|
+
body instanceof Blob ||
|
|
12
|
+
body instanceof ArrayBuffer ||
|
|
13
|
+
ArrayBuffer.isView(body) ||
|
|
14
|
+
body instanceof ReadableStream);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether the body carries its own content type, which `fetch()` sets correctly
|
|
18
|
+
* (form encoding, multipart boundary, blob type) and we must not overwrite.
|
|
19
|
+
*/
|
|
20
|
+
function describesOwnContentType(body) {
|
|
21
|
+
return body instanceof URLSearchParams || body instanceof FormData || body instanceof Blob;
|
|
22
|
+
}
|
|
23
|
+
function hasContentTypeHeader(headers) {
|
|
24
|
+
return Object.keys(headers).some((header) => header.toLowerCase() === 'content-type');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the value handed to `fetch()` as the request body, defaulting the
|
|
28
|
+
* content type (into `headers`) whenever we are the ones choosing the encoding.
|
|
29
|
+
*/
|
|
30
|
+
function buildRequestBody(body, method, headers) {
|
|
31
|
+
if (body === undefined) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (method === 'GET') {
|
|
35
|
+
throw new Error("SSEHttpClient.connect(): a request body requires a non-GET method. Pass e.g. { method: 'POST', body }.");
|
|
36
|
+
}
|
|
37
|
+
if (isFetchBody(body)) {
|
|
38
|
+
if (!describesOwnContentType(body) && !hasContentTypeHeader(headers)) {
|
|
39
|
+
headers['content-type'] = 'application/json';
|
|
40
|
+
}
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
const serializedBody = JSON.stringify(body);
|
|
44
|
+
if (serializedBody === undefined) {
|
|
45
|
+
throw new Error(`SSEHttpClient.connect(): a body of type ${typeof body} cannot be serialized to JSON. Pass a JSON-serializable value, a string, or a body fetch() accepts natively.`);
|
|
46
|
+
}
|
|
47
|
+
if (!hasContentTypeHeader(headers)) {
|
|
48
|
+
headers['content-type'] = 'application/json';
|
|
49
|
+
}
|
|
50
|
+
return serializedBody;
|
|
51
|
+
}
|
|
3
52
|
/**
|
|
4
53
|
* SSE client for testing long-lived connections using real HTTP.
|
|
5
54
|
*
|
|
@@ -10,6 +59,13 @@ import { parseSSEBuffer } from "../sse/sseParser.js";
|
|
|
10
59
|
* - **Long-lived connections** that stay open indefinitely
|
|
11
60
|
* - **Real-time notifications** where events arrive over time
|
|
12
61
|
* - **Push-based streaming** where the client waits for server-initiated events
|
|
62
|
+
* - **Assertions on the wire while the handler is still running** - `connect()`
|
|
63
|
+
* resolves as soon as headers arrive, so `response.status` / `response.headers`
|
|
64
|
+
* can be checked before the handler produces its first event
|
|
65
|
+
*
|
|
66
|
+
* GET, POST, PUT and PATCH are all supported (see `method` / `body` in
|
|
67
|
+
* {@link SSEHttpConnectOptions}), so POST SSE endpoints that take a request
|
|
68
|
+
* body can be tested over real HTTP too.
|
|
13
69
|
*
|
|
14
70
|
* **When to use SSEHttpClient vs SSEInjectClient:**
|
|
15
71
|
*
|
|
@@ -53,20 +109,44 @@ import { parseSSEBuffer } from "../sse/sseParser.js";
|
|
|
53
109
|
* ```
|
|
54
110
|
*/
|
|
55
111
|
export class SSEHttpClient {
|
|
56
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The fetch Response object. Available immediately after connect() returns,
|
|
114
|
+
* before any event is consumed, so status and headers can be asserted while
|
|
115
|
+
* the handler is still running.
|
|
116
|
+
*
|
|
117
|
+
* The response body is only locked once events are first consumed, so
|
|
118
|
+
* `response.json()` / `response.text()` still work for endpoints that
|
|
119
|
+
* answered with a regular HTTP response instead of a stream (for example an
|
|
120
|
+
* error raised before `sse.start()`). Read that body *before* calling
|
|
121
|
+
* `close()` - `close()` aborts the request, which rejects any pending or
|
|
122
|
+
* subsequent body read with an `AbortError`.
|
|
123
|
+
*/
|
|
57
124
|
response;
|
|
58
125
|
abortController;
|
|
59
|
-
|
|
126
|
+
responseBody;
|
|
127
|
+
streamReader;
|
|
60
128
|
decoder = new TextDecoder();
|
|
61
129
|
buffer = '';
|
|
62
130
|
closed = false;
|
|
63
131
|
constructor(response, abortController) {
|
|
64
132
|
this.response = response;
|
|
65
133
|
this.abortController = abortController;
|
|
66
|
-
|
|
67
|
-
|
|
134
|
+
this.responseBody = response.body;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Lazily acquire the stream reader, locking the response body on first use.
|
|
138
|
+
*
|
|
139
|
+
* A missing body is reported here rather than from the constructor, so a
|
|
140
|
+
* bodiless response (e.g. `204`) can still be inspected via `response`.
|
|
141
|
+
*/
|
|
142
|
+
get reader() {
|
|
143
|
+
if (!this.streamReader) {
|
|
144
|
+
if (!this.responseBody) {
|
|
145
|
+
throw new Error(`SSE response has no body to stream (status ${this.response.status}). Inspect \`client.response\` instead of consuming events.`);
|
|
146
|
+
}
|
|
147
|
+
this.streamReader = this.responseBody.getReader();
|
|
68
148
|
}
|
|
69
|
-
this.
|
|
149
|
+
return this.streamReader;
|
|
70
150
|
}
|
|
71
151
|
static async connect(baseUrl, path, options) {
|
|
72
152
|
// Build path with query string
|
|
@@ -77,30 +157,34 @@ export class SSEHttpClient {
|
|
|
77
157
|
pathWithQuery = `${path}?${queryString}`;
|
|
78
158
|
}
|
|
79
159
|
}
|
|
160
|
+
// Normalize once - contracts spell methods in lowercase, the wire and Node use uppercase
|
|
161
|
+
const method = (options?.method ?? 'GET').toUpperCase();
|
|
162
|
+
const headers = {
|
|
163
|
+
Accept: 'text/event-stream',
|
|
164
|
+
...options?.headers,
|
|
165
|
+
};
|
|
166
|
+
const body = buildRequestBody(options?.body, method, headers);
|
|
80
167
|
// Connect - fetch() returns when headers are received
|
|
81
168
|
const abortController = new AbortController();
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
169
|
+
const requestInit = {
|
|
170
|
+
method,
|
|
171
|
+
headers,
|
|
172
|
+
body,
|
|
87
173
|
signal: abortController.signal,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
catch (error) {
|
|
94
|
-
// Nothing owns the response yet, so abort it here rather than leaking it.
|
|
95
|
-
abortController.abort();
|
|
96
|
-
throw error;
|
|
174
|
+
};
|
|
175
|
+
if (body instanceof ReadableStream) {
|
|
176
|
+
// A streamed request body has to declare half-duplex explicitly, or fetch() rejects it
|
|
177
|
+
;
|
|
178
|
+
requestInit.duplex = 'half';
|
|
97
179
|
}
|
|
180
|
+
const response = await fetch(`${baseUrl}${pathWithQuery}`, requestInit);
|
|
181
|
+
const client = new SSEHttpClient(response, abortController);
|
|
98
182
|
// If awaitServerConnection is specified, wait for server-side registration
|
|
99
183
|
if (options && 'awaitServerConnection' in options && options.awaitServerConnection) {
|
|
100
184
|
const awaitOptions = options.awaitServerConnection;
|
|
101
185
|
const waitOptions = {
|
|
102
186
|
timeout: awaitOptions.timeout ?? 5000,
|
|
103
|
-
predicate: (conn) => conn.request.url === pathWithQuery,
|
|
187
|
+
predicate: (conn) => conn.request.url === pathWithQuery && conn.request.method.toUpperCase() === method,
|
|
104
188
|
};
|
|
105
189
|
try {
|
|
106
190
|
// Both branches call the same method; the spy is invariant in its session
|
|
@@ -272,11 +356,17 @@ export class SSEHttpClient {
|
|
|
272
356
|
*
|
|
273
357
|
* This aborts the underlying fetch request. Call this when done
|
|
274
358
|
* consuming events to clean up resources.
|
|
359
|
+
*
|
|
360
|
+
* Aborting also discards any unread response body, so for a non-stream
|
|
361
|
+
* response (an error raised before `sse.start()`, say) read
|
|
362
|
+
* `response.json()` / `response.text()` before calling this.
|
|
275
363
|
*/
|
|
276
364
|
close() {
|
|
277
365
|
this.closed = true;
|
|
278
|
-
// Cancel the reader first to prevent unhandled rejections from pending reads
|
|
279
|
-
|
|
366
|
+
// Cancel the reader first to prevent unhandled rejections from pending reads.
|
|
367
|
+
// Only touch it if events were actually consumed - acquiring it here would
|
|
368
|
+
// lock a body the caller may still want to read as JSON/text.
|
|
369
|
+
this.streamReader?.cancel().catch(() => {
|
|
280
370
|
// Expected: may already be closed or errored
|
|
281
371
|
});
|
|
282
372
|
this.abortController.abort();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sseHttpClient.js","sourceRoot":"","sources":["../../../lib/testing/sseHttpClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAG5C,OAAO,EAAuB,cAAc,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"sseHttpClient.js","sourceRoot":"","sources":["../../../lib/testing/sseHttpClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -54,6 +54,23 @@ export declare class SSEInjectConnection implements SSETestConnection {
|
|
|
54
54
|
* Get the response headers.
|
|
55
55
|
*/
|
|
56
56
|
getHeaders(): Record<string, string | string[] | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* Get the raw response body as a string.
|
|
59
|
+
*
|
|
60
|
+
* For a streaming response this is the raw `text/event-stream` payload the
|
|
61
|
+
* events were parsed from. For a route that answered with a status code
|
|
62
|
+
* before streaming started (auth failure, validation error, integration
|
|
63
|
+
* unavailable) it is that non-streaming body - typically JSON.
|
|
64
|
+
*/
|
|
65
|
+
getBody(): string;
|
|
66
|
+
/**
|
|
67
|
+
* Parse the raw response body as JSON, mirroring Fastify's inject `json()`.
|
|
68
|
+
*
|
|
69
|
+
* @throws if the body is empty or not valid JSON. A `text/event-stream` body
|
|
70
|
+
* is not JSON, so this only makes sense for responses emitted before
|
|
71
|
+
* streaming started.
|
|
72
|
+
*/
|
|
73
|
+
json<T = unknown>(): T;
|
|
57
74
|
}
|
|
58
75
|
/**
|
|
59
76
|
* SSE client using Fastify's inject() for testing SSE endpoints.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseSSEEvents } from "../sse/sseParser.js";
|
|
2
|
+
import { truncateBody } from "./sseInjectShared.js";
|
|
2
3
|
/**
|
|
3
4
|
* SSE connection object returned by SSEInjectClient.
|
|
4
5
|
*
|
|
@@ -80,6 +81,36 @@ export class SSEInjectConnection {
|
|
|
80
81
|
getHeaders() {
|
|
81
82
|
return this.response.headers;
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Get the raw response body as a string.
|
|
86
|
+
*
|
|
87
|
+
* For a streaming response this is the raw `text/event-stream` payload the
|
|
88
|
+
* events were parsed from. For a route that answered with a status code
|
|
89
|
+
* before streaming started (auth failure, validation error, integration
|
|
90
|
+
* unavailable) it is that non-streaming body - typically JSON.
|
|
91
|
+
*/
|
|
92
|
+
getBody() {
|
|
93
|
+
return this.response.body;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse the raw response body as JSON, mirroring Fastify's inject `json()`.
|
|
97
|
+
*
|
|
98
|
+
* @throws if the body is empty or not valid JSON. A `text/event-stream` body
|
|
99
|
+
* is not JSON, so this only makes sense for responses emitted before
|
|
100
|
+
* streaming started.
|
|
101
|
+
*/
|
|
102
|
+
json() {
|
|
103
|
+
const { body } = this.response;
|
|
104
|
+
if (!body) {
|
|
105
|
+
throw new Error(`json() — response body is empty (status ${this.response.statusCode})`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(body);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`json() — body is not valid JSON: ${err.message}; body: ${truncateBody(body)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
83
114
|
}
|
|
84
115
|
/**
|
|
85
116
|
* SSE client using Fastify's inject() for testing SSE endpoints.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { AnySSEContractDefinition, HttpStatusCode } from '@lokalise/api-contracts';
|
|
2
|
+
import type { InjectOptions } from 'fastify';
|
|
2
3
|
import type { z } from 'zod';
|
|
3
4
|
import type { ParsedSSEEvent } from '../sse/sseParser.ts';
|
|
5
|
+
/**
|
|
6
|
+
* HTTP methods Fastify's `inject()` accepts, in either case.
|
|
7
|
+
*
|
|
8
|
+
* Derived from Fastify's own inject options instead of being hand-listed, so it
|
|
9
|
+
* tracks whatever the installed Fastify supports and never needs to be redefined
|
|
10
|
+
* downstream. Note that Fastify's `HTTPMethods` is wider than this - it also
|
|
11
|
+
* covers `SEARCH`, `QUERY` and the WebDAV verbs, which `inject()` does not take.
|
|
12
|
+
*/
|
|
13
|
+
export type SSEInjectMethod = NonNullable<InjectOptions['method']>;
|
|
4
14
|
/** Safely infer the output type of an optional Zod schema property. */
|
|
5
15
|
type InferOptionalSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodTypeAny ? z.infer<NonNullable<T>> : Fallback;
|
|
6
16
|
/**
|
|
@@ -50,13 +60,42 @@ export interface SSETestConnection {
|
|
|
50
60
|
* Get response headers.
|
|
51
61
|
*/
|
|
52
62
|
getHeaders(): Record<string, string | string[] | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* Get the raw response body as a string.
|
|
65
|
+
*
|
|
66
|
+
* For a successful SSE response this is the raw `text/event-stream` payload
|
|
67
|
+
* (already parsed into events, available via `getReceivedEvents()`). It is
|
|
68
|
+
* most useful when the route answered with a status code before streaming
|
|
69
|
+
* started - an auth failure, a validation error, an unavailable integration -
|
|
70
|
+
* and responded with a JSON body instead of events.
|
|
71
|
+
*/
|
|
72
|
+
getBody(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Parse the raw response body as JSON.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors Fastify's own inject response `json()`. Intended for non-streaming
|
|
77
|
+
* responses emitted before streaming starts; calling it on an actual SSE
|
|
78
|
+
* stream body throws, since `text/event-stream` is not JSON.
|
|
79
|
+
*
|
|
80
|
+
* @throws if the body is empty or not valid JSON (the message includes a
|
|
81
|
+
* truncated body snippet).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const conn = await client.connect('/api/stream')
|
|
86
|
+
* expect(conn.getStatusCode()).toBe(503)
|
|
87
|
+
* expect(conn.json()).toMatchObject({ errorCode: 'INTEGRATION_NOT_AVAILABLE' })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
json<T = unknown>(): T;
|
|
53
91
|
}
|
|
54
92
|
/**
|
|
55
93
|
* Options for establishing an SSE connection.
|
|
56
94
|
*/
|
|
57
95
|
export type SSEConnectOptions = {
|
|
58
96
|
headers?: Record<string, string>;
|
|
59
|
-
method
|
|
97
|
+
/** Any method `inject()` accepts (default: `'POST'` for `connectWithBody`) */
|
|
98
|
+
method?: SSEInjectMethod;
|
|
60
99
|
body?: unknown;
|
|
61
100
|
};
|
|
62
101
|
/**
|