opinionated-machine 10.0.0 → 10.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.
@@ -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
- /** The fetch Response object. Available immediately after connect() returns. */
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
- reader;
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
- if (!response.body) {
67
- throw new Error('SSE response has no body');
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.reader = response.body.getReader();
149
+ return this.streamReader;
70
150
  }
71
151
  static async connect(baseUrl, path, options) {
72
152
  // Build path with query string
@@ -77,24 +157,51 @@ 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 response = await fetch(`${baseUrl}${pathWithQuery}`, {
83
- headers: {
84
- Accept: 'text/event-stream',
85
- ...options?.headers,
86
- },
169
+ const requestInit = {
170
+ method,
171
+ headers,
172
+ body,
87
173
  signal: abortController.signal,
88
- });
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';
179
+ }
180
+ const response = await fetch(`${baseUrl}${pathWithQuery}`, requestInit);
89
181
  const client = new SSEHttpClient(response, abortController);
90
182
  // If awaitServerConnection is specified, wait for server-side registration
91
183
  if (options && 'awaitServerConnection' in options && options.awaitServerConnection) {
92
- const { controller, timeout } = options.awaitServerConnection;
93
- const serverConnection = await controller.connectionSpy.waitForConnection({
94
- timeout: timeout ?? 5000,
95
- predicate: (conn) => conn.request.url === pathWithQuery,
96
- });
97
- return { client, serverConnection };
184
+ const awaitOptions = options.awaitServerConnection;
185
+ const waitOptions = {
186
+ timeout: awaitOptions.timeout ?? 5000,
187
+ predicate: (conn) => conn.request.url === pathWithQuery && conn.request.method.toUpperCase() === method,
188
+ };
189
+ try {
190
+ // Both branches call the same method; the spy is invariant in its session
191
+ // type, so they cannot be collapsed into one reference.
192
+ const serverConnection = 'spy' in awaitOptions
193
+ ? await awaitOptions.spy.waitForConnection(waitOptions)
194
+ : await awaitOptions.controller.connectionSpy.waitForConnection(waitOptions);
195
+ return { client, serverConnection };
196
+ }
197
+ catch (error) {
198
+ // The HTTP connection is already established and the caller never gets a
199
+ // handle to it, so close it here. Left open, a keep-alive route keeps the
200
+ // socket streaming and the test's `app.close()` hangs, hiding this error
201
+ // behind a suite-level timeout.
202
+ client.close();
203
+ throw error;
204
+ }
98
205
  }
99
206
  return client;
100
207
  }
@@ -249,11 +356,17 @@ export class SSEHttpClient {
249
356
  *
250
357
  * This aborts the underlying fetch request. Call this when done
251
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.
252
363
  */
253
364
  close() {
254
365
  this.closed = true;
255
- // Cancel the reader first to prevent unhandled rejections from pending reads
256
- this.reader.cancel().catch(() => {
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(() => {
257
370
  // Expected: may already be closed or errored
258
371
  });
259
372
  this.abortController.abort();
@@ -1 +1 @@
1
- {"version":3,"file":"sseHttpClient.js","sourceRoot":"","sources":["../../../lib/testing/sseHttpClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sseHttpClient.js","sourceRoot":"","sources":["../../../lib/testing/sseHttpClient.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,4 @@
1
- /** Truncate a long body string for error messages. */
2
- const BODY_TRUNCATE_LIMIT = 500;
3
- const truncateBody = (body) => {
4
- if (body.length <= BODY_TRUNCATE_LIMIT) {
5
- return body;
6
- }
7
- // Step back one unit if the cut would split a surrogate pair, so the
8
- // snippet never ends in a lone (invalid) surrogate.
9
- const lastCode = body.charCodeAt(BODY_TRUNCATE_LIMIT - 1);
10
- const end = lastCode >= 0xd800 && lastCode <= 0xdbff ? BODY_TRUNCATE_LIMIT - 1 : BODY_TRUNCATE_LIMIT;
11
- return `${body.slice(0, end)}…`;
12
- };
1
+ import { truncateBody } from "./sseInjectShared.js";
13
2
  /**
14
3
  * Build a `bodyForStatus` accessor bound to one inject call. The closure
15
4
  * captures the contract's schemas map so the resulting helper knows which
@@ -1 +1 @@
1
- {"version":3,"file":"sseInjectHelpers.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectHelpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sseInjectHelpers.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectHelpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Helpers shared by the contract-aware SSE inject helpers (legacy
3
+ * `SSEContractDefinition` and `defineApiContract` flavours alike).
4
+ *
5
+ * @internal
6
+ */
7
+ export declare const truncateBody: (body: string) => string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Helpers shared by the contract-aware SSE inject helpers (legacy
3
+ * `SSEContractDefinition` and `defineApiContract` flavours alike).
4
+ *
5
+ * @internal
6
+ */
7
+ /** Truncate a long body string for error messages. */
8
+ const BODY_TRUNCATE_LIMIT = 500;
9
+ export const truncateBody = (body) => {
10
+ if (body.length <= BODY_TRUNCATE_LIMIT) {
11
+ return body;
12
+ }
13
+ // Step back one unit if the cut would split a surrogate pair, so the
14
+ // snippet never ends in a lone (invalid) surrogate.
15
+ const lastCode = body.charCodeAt(BODY_TRUNCATE_LIMIT - 1);
16
+ const end = lastCode >= 0xd800 && lastCode <= 0xdbff ? BODY_TRUNCATE_LIMIT - 1 : BODY_TRUNCATE_LIMIT;
17
+ return `${body.slice(0, end)}…`;
18
+ };
19
+ //# sourceMappingURL=sseInjectShared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseInjectShared.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectShared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,sDAAsD;AACtD,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAE/B,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE;IACnD,IAAI,IAAI,CAAC,MAAM,IAAI,mBAAmB,EAAE,CAAC;QACvC,OAAO,IAAI,CAAA;IACb,CAAC;IACD,qEAAqE;IACrE,oDAAoD;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAA;IACzD,MAAM,GAAG,GACP,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAA;IAC1F,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAA;AACjC,CAAC,CAAA"}
@@ -0,0 +1,109 @@
1
+ import type { SSESession as ApiSSESession, SSECloseInitiator } from '@lokalise/fastify-api-contracts';
2
+ import { type SpiedSSESession, SSESessionSpy } from '../sse/SSESessionSpy.ts';
3
+ /**
4
+ * The SSE lifecycle hooks a route may declare, as far as a spy is concerned.
5
+ *
6
+ * Structurally a subset of the SSE route options of both `buildApiRoute` /
7
+ * `buildFastifyApiRoute` and this package's own `buildFastifyRoute`, so any of
8
+ * their option objects can be handed to {@link CreateSSESessionSpyResult.withSpy}.
9
+ */
10
+ export type SSESessionSpyHooks<TSession extends SpiedSSESession = ApiSSESession> = {
11
+ onConnect?: (connection: TSession) => void | Promise<void>;
12
+ onClose?: (connection: TSession, initiator: SSECloseInitiator) => void | Promise<void>;
13
+ };
14
+ /**
15
+ * The SSE lifecycle hooks that feed a standalone {@link SSESessionSpy}.
16
+ *
17
+ * Shaped to be spread straight into route options that accept SSE lifecycle
18
+ * hooks — `buildApiRoute` / `buildFastifyApiRoute` options, or this package's
19
+ * own `buildFastifyRoute` options.
20
+ *
21
+ * Spreading these over options that declare `onConnect` / `onClose` of their own
22
+ * would silently drop one side or the other; use
23
+ * {@link CreateSSESessionSpyResult.withSpy} for such routes, which chains both.
24
+ */
25
+ export type SSESessionSpyRouteOptions<TSession extends SpiedSSESession = ApiSSESession> = {
26
+ onConnect: (connection: TSession) => void | Promise<void>;
27
+ onClose: (connection: TSession, initiator: SSECloseInitiator) => void | Promise<void>;
28
+ };
29
+ /**
30
+ * Result of {@link createSSESessionSpy}.
31
+ */
32
+ export type CreateSSESessionSpyResult<TSession extends SpiedSSESession = ApiSSESession> = {
33
+ /** The spy, ready to hand to `SSEHttpClient.connect`'s `awaitServerConnection`. */
34
+ spy: SSESessionSpy<TSession>;
35
+ /**
36
+ * `{ onConnect, onClose }` to spread into the options of a route that declares
37
+ * no SSE lifecycle hooks of its own. Equivalent to `withSpy()`.
38
+ */
39
+ routeOptions: SSESessionSpyRouteOptions<TSession>;
40
+ /**
41
+ * Merge the spy's hooks into a route's own options.
42
+ *
43
+ * Any `onConnect` / `onClose` the route already declares is kept and runs
44
+ * first; the spy is notified once that hook settles. Every other option is
45
+ * passed through untouched. Use this instead of spreading
46
+ * {@link CreateSSESessionSpyResult.routeOptions}, which would replace the
47
+ * route's hooks rather than compose with them.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * app.route(
52
+ * buildApiRoute(contract, handler, withSpy({
53
+ * heartbeat: false,
54
+ * onConnect: (connection) => subscriptions.add(connection.id),
55
+ * })),
56
+ * )
57
+ * ```
58
+ */
59
+ withSpy: <TOptions extends object>(options?: TOptions & SSESessionSpyHooks<TSession>) => Omit<TOptions, 'onConnect' | 'onClose'> & SSESessionSpyRouteOptions<TSession>;
60
+ };
61
+ /**
62
+ * Create a standalone connection spy plus the route hooks that drive it.
63
+ *
64
+ * `AbstractSSEController` exposes a spy of its own via `connectionSpy` (gated on
65
+ * `{ enableConnectionSpy: true }`). Routes built with `buildApiRoute` have no
66
+ * such controller, so this factory drives a spy from the route's `onConnect` /
67
+ * `onClose` hooks instead. That removes the race between `connect()` returning
68
+ * (HTTP headers received) and the server-side handler finishing its connection
69
+ * registration.
70
+ *
71
+ * **The hooks have to be passed to the `buildApiRoute()` call that builds the
72
+ * route.** The route builder captures them when it constructs the handler, so
73
+ * attaching them to an already-built `RouteOptions` object has no effect. A
74
+ * route owned by production code therefore has to accept SSE route options (or
75
+ * the hooks themselves) as a parameter for a test to be able to spy on it.
76
+ *
77
+ * **Only useful for sessions that outlive the handler** — `keepAlive` sessions.
78
+ * An `autoClose` route closes its session as the handler returns, before the
79
+ * test can claim it, so `awaitServerConnection` on such a route races and
80
+ * usually times out. Omit `awaitServerConnection` there and assert on the
81
+ * events the client received instead.
82
+ *
83
+ * The spy observes `@lokalise/fastify-api-contracts` sessions by default, which
84
+ * is what `buildApiRoute` hands to its hooks. Pass this package's `SSESession`
85
+ * as the type argument when wiring it to `buildFastifyRoute` instead.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const { spy, routeOptions } = createSSESessionSpy()
90
+ *
91
+ * // in the app-under-test's route registration
92
+ * app.route(buildApiRoute(contract, handler, { ...routeOptions }))
93
+ *
94
+ * // in the test
95
+ * const { client, serverConnection } = await SSEHttpClient.connect(baseUrl, path, {
96
+ * awaitServerConnection: { spy },
97
+ * })
98
+ * // serverConnection is registered server-side and ready to use
99
+ * ```
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Route with lifecycle hooks of its own: compose instead of replacing them
104
+ * const { spy, withSpy } = createSSESessionSpy()
105
+ *
106
+ * app.route(buildApiRoute(contract, handler, withSpy(productionRouteOptions)))
107
+ * ```
108
+ */
109
+ export declare function createSSESessionSpy<TSession extends SpiedSSESession = ApiSSESession>(): CreateSSESessionSpyResult<TSession>;
@@ -0,0 +1,100 @@
1
+ import { SSESessionSpy } from "../sse/SSESessionSpy.js";
2
+ const isThenable = (value) => typeof value?.then === 'function';
3
+ /**
4
+ * Run a route's own lifecycle hook, then notify the spy — even if the hook fails.
5
+ *
6
+ * The spy is an observer of the connection, not a participant in it: a hook that
7
+ * throws is a failure of the app under test, and swallowing the connection event
8
+ * on top of it would surface as an unrelated `waitForConnection` timeout much
9
+ * later. The hook's own failure is propagated to the route builder unchanged.
10
+ *
11
+ * The spy is notified *after* the hook, mirroring `AbstractSSEController`, so a
12
+ * hook that enriches the session does so before any waiter observes it.
13
+ */
14
+ function runHookThenNotify(hook, notify) {
15
+ let result;
16
+ try {
17
+ result = hook();
18
+ }
19
+ catch (error) {
20
+ notify();
21
+ throw error;
22
+ }
23
+ if (isThenable(result)) {
24
+ return result.then(() => notify(), (error) => {
25
+ notify();
26
+ throw error;
27
+ });
28
+ }
29
+ notify();
30
+ }
31
+ /**
32
+ * Create a standalone connection spy plus the route hooks that drive it.
33
+ *
34
+ * `AbstractSSEController` exposes a spy of its own via `connectionSpy` (gated on
35
+ * `{ enableConnectionSpy: true }`). Routes built with `buildApiRoute` have no
36
+ * such controller, so this factory drives a spy from the route's `onConnect` /
37
+ * `onClose` hooks instead. That removes the race between `connect()` returning
38
+ * (HTTP headers received) and the server-side handler finishing its connection
39
+ * registration.
40
+ *
41
+ * **The hooks have to be passed to the `buildApiRoute()` call that builds the
42
+ * route.** The route builder captures them when it constructs the handler, so
43
+ * attaching them to an already-built `RouteOptions` object has no effect. A
44
+ * route owned by production code therefore has to accept SSE route options (or
45
+ * the hooks themselves) as a parameter for a test to be able to spy on it.
46
+ *
47
+ * **Only useful for sessions that outlive the handler** — `keepAlive` sessions.
48
+ * An `autoClose` route closes its session as the handler returns, before the
49
+ * test can claim it, so `awaitServerConnection` on such a route races and
50
+ * usually times out. Omit `awaitServerConnection` there and assert on the
51
+ * events the client received instead.
52
+ *
53
+ * The spy observes `@lokalise/fastify-api-contracts` sessions by default, which
54
+ * is what `buildApiRoute` hands to its hooks. Pass this package's `SSESession`
55
+ * as the type argument when wiring it to `buildFastifyRoute` instead.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const { spy, routeOptions } = createSSESessionSpy()
60
+ *
61
+ * // in the app-under-test's route registration
62
+ * app.route(buildApiRoute(contract, handler, { ...routeOptions }))
63
+ *
64
+ * // in the test
65
+ * const { client, serverConnection } = await SSEHttpClient.connect(baseUrl, path, {
66
+ * awaitServerConnection: { spy },
67
+ * })
68
+ * // serverConnection is registered server-side and ready to use
69
+ * ```
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * // Route with lifecycle hooks of its own: compose instead of replacing them
74
+ * const { spy, withSpy } = createSSESessionSpy()
75
+ *
76
+ * app.route(buildApiRoute(contract, handler, withSpy(productionRouteOptions)))
77
+ * ```
78
+ */
79
+ export function createSSESessionSpy() {
80
+ const spy = new SSESessionSpy();
81
+ // `TOptions extends object` rather than `extends SSESessionSpyHooks`: with an
82
+ // all-optional constraint, TypeScript falls back to the constraint itself for
83
+ // an argument that declares no hooks, which would drop every other route
84
+ // option from the result type. Hook compatibility is enforced by the
85
+ // intersection in the parameter position instead.
86
+ const withSpy = (options) => {
87
+ const { onConnect, onClose, ...passThrough } = options ?? {};
88
+ return {
89
+ ...passThrough,
90
+ onConnect: (connection) => runHookThenNotify(() => onConnect?.(connection), () => spy.addConnection(connection)),
91
+ onClose: (connection, initiator) => runHookThenNotify(() => onClose?.(connection, initiator), () => spy.addDisconnection(connection.id)),
92
+ };
93
+ };
94
+ return {
95
+ spy,
96
+ routeOptions: withSpy(),
97
+ withSpy,
98
+ };
99
+ }
100
+ //# sourceMappingURL=sseSessionSpyFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseSessionSpyFactory.js","sourceRoot":"","sources":["../../../lib/testing/sseSessionSpyFactory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwB,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAiE7E,MAAM,UAAU,GAAG,CAAC,KAAc,EAA6B,EAAE,CAC/D,OAAQ,KAAwC,EAAE,IAAI,KAAK,UAAU,CAAA;AAEvE;;;;;;;;;;GAUG;AACH,SAAS,iBAAiB,CACxB,IAAgC,EAChC,MAAkB;IAElB,IAAI,MAA4B,CAAA;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,EAAE,CAAA;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,EAAE,CAAA;QACR,MAAM,KAAK,CAAA;IACb,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC,IAAI,CAChB,GAAG,EAAE,CAAC,MAAM,EAAE,EACd,CAAC,KAAc,EAAE,EAAE;YACjB,MAAM,EAAE,CAAA;YACR,MAAM,KAAK,CAAA;QACb,CAAC,CACF,CAAA;IACH,CAAC;IAED,MAAM,EAAE,CAAA;AACV,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,UAAU,mBAAmB;IAGjC,MAAM,GAAG,GAAG,IAAI,aAAa,EAAY,CAAA;IAEzC,8EAA8E;IAC9E,8EAA8E;IAC9E,yEAAyE;IACzE,qEAAqE;IACrE,kDAAkD;IAClD,MAAM,OAAO,GAAG,CACd,OAAiD,EAC8B,EAAE;QACjF,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,WAAW,EAAE,GAC1C,OAAO,IAAK,EAA8C,CAAA;QAE5D,OAAO;YACL,GAAI,WAAuD;YAC3D,SAAS,EAAE,CAAC,UAAU,EAAE,EAAE,CACxB,iBAAiB,CACf,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,EAC7B,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,CACpC;YACH,OAAO,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,CACjC,iBAAiB,CACf,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,EACtC,GAAG,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,CAC1C;SACJ,CAAA;IACH,CAAC,CAAA;IAED,OAAO;QACL,GAAG;QACH,YAAY,EAAE,OAAO,EAAE;QACvB,OAAO;KACR,CAAA;AACH,CAAC"}
@@ -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
  /**
@@ -56,7 +66,8 @@ export interface SSETestConnection {
56
66
  */
57
67
  export type SSEConnectOptions = {
58
68
  headers?: Record<string, string>;
59
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH';
69
+ /** Any method `inject()` accepts (default: `'POST'` for `connectWithBody`) */
70
+ method?: SSEInjectMethod;
60
71
  body?: unknown;
61
72
  };
62
73
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opinionated-machine",
3
- "version": "10.0.0",
3
+ "version": "10.3.0",
4
4
  "description": "Very opinionated DI framework for fastify, built on top of awilix ",
5
5
  "type": "module",
6
6
  "license": "MIT",