opinionated-machine 10.0.0 → 10.2.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.
@@ -0,0 +1,202 @@
1
+ import type { ApiContract, ClientErrorHttpStatusCode, ExpandStatusRangeKey, HttpStatusCode, HttpStatusCodeRange, InformationalHttpStatusCode, RedirectionHttpStatusCode, ServerErrorHttpStatusCode, SuccessfulHttpStatusCode } from '@lokalise/api-contracts';
2
+ import type { InjectByApiContractParams } from '@lokalise/fastify-api-contracts';
3
+ import type { z } from 'zod';
4
+ import type { SSEResponse } from './sseTestTypes.ts';
5
+ /**
6
+ * Request params for {@link injectApiSSE}, derived from a `defineApiContract` contract.
7
+ *
8
+ * Identical to the params of `injectByApiContract` from
9
+ * `@lokalise/fastify-api-contracts` — `pathParams`, `body`, `queryParams` and
10
+ * `headers` are each required only when the contract declares the matching
11
+ * request schema, `headers` also accepts a (sync or async) factory, and
12
+ * `pathPrefix` is always optional.
13
+ */
14
+ export type InjectApiSSEParams<Contract extends ApiContract> = InjectByApiContractParams<Contract>;
15
+ /** The `responsesByStatusCode` map of a contract. */
16
+ type Responses<Contract extends ApiContract> = Contract['responsesByStatusCode'];
17
+ /** The concrete status codes a contract declares exactly (non-wildcard keys). */
18
+ type ExactStatusCodes<Contract extends ApiContract> = keyof Responses<Contract> & HttpStatusCode;
19
+ /** Status codes covered by any range key (e.g. `'2xx'`, `'4xx'`) the contract declares. */
20
+ type RangeStatusCodes<Contract extends ApiContract> = {
21
+ [Key in keyof Responses<Contract> & HttpStatusCodeRange]: ExpandStatusRangeKey<Key>;
22
+ }[keyof Responses<Contract> & HttpStatusCodeRange];
23
+ /**
24
+ * Maps a `responsesByStatusCode` key to the statuses it covers, mirroring the runtime
25
+ * lookup precedence (exact → range → `'default'`): a concrete key stays as-is; a range key
26
+ * expands to its status class minus the exactly-declared codes; `'default'` expands to every
27
+ * status not covered by an exact or range key.
28
+ */
29
+ type StatusesForKey<Contract extends ApiContract, Key> = Key extends 'default' ? Exclude<HttpStatusCode, ExactStatusCodes<Contract> | RangeStatusCodes<Contract>> : Key extends HttpStatusCodeRange ? Exclude<ExpandStatusRangeKey<Key>, ExactStatusCodes<Contract>> : Key;
30
+ /** The range key a concrete status code falls into. */
31
+ type RangeKeyOf<Status extends HttpStatusCode> = Status extends InformationalHttpStatusCode ? '1xx' : Status extends SuccessfulHttpStatusCode ? '2xx' : Status extends RedirectionHttpStatusCode ? '3xx' : Status extends ClientErrorHttpStatusCode ? '4xx' : Status extends ServerErrorHttpStatusCode ? '5xx' : never;
32
+ /** The media type `injectApiSSE` always asks for — see {@link StreamSchemasOfEntry}. */
33
+ type SSEMediaType = 'text/event-stream';
34
+ /**
35
+ * The `event name -> schema` map of an SSE body descriptor, or `never` for any other
36
+ * descriptor (a bare Zod schema is JSON, `blobBody()` is opaque bytes).
37
+ */
38
+ type SseSchemasOfDescriptor<Descriptor> = Descriptor extends {
39
+ _tag: 'SseBody';
40
+ schemaByEventName: infer Schemas;
41
+ } ? Schemas : never;
42
+ /**
43
+ * Every SSE schema map a response entry declares, across all of its media types — the
44
+ * type-level counterpart of the maps `getSseSchemaByEventName` collects at runtime.
45
+ */
46
+ type SseSchemasOfEntry<Entry> = Entry extends {
47
+ content: infer Content;
48
+ } ? SseSchemasOfDescriptor<Content[keyof Content]> : never;
49
+ /**
50
+ * The SSE schema map an entry declares under `text/event-stream` specifically.
51
+ *
52
+ * `injectApiSSE` always sends `accept: text/event-stream`, so this is the descriptor a
53
+ * status resolves to whenever it declares one — even on a dual-mode content map that also
54
+ * carries a JSON schema.
55
+ */
56
+ type StreamSchemasOfEntry<Entry> = Entry extends {
57
+ content: infer Content;
58
+ } ? SseSchemasOfDescriptor<Content[SSEMediaType & keyof Content]> : never;
59
+ /**
60
+ * The JSON Zod schema of a single response entry, or `never` when this helper can't reach a
61
+ * JSON body there. A bare schema is JSON; a content-map entry contributes the schemas of its
62
+ * non-blob, non-SSE descriptors.
63
+ *
64
+ * An entry that declares a `text/event-stream` body has no reachable JSON side: the request
65
+ * asks for the stream, so a dual-mode status answers with SSE and `bodyForStatus` would throw.
66
+ * Read those with `events()` instead.
67
+ */
68
+ type JsonSchemaOfEntry<Entry> = [StreamSchemasOfEntry<Entry>] extends [never] ? Entry extends z.ZodType ? Entry : Entry extends {
69
+ content: infer Content;
70
+ } ? Extract<Content[keyof Content], z.ZodType> : never : never;
71
+ /** Indexes a responses map with a key that may not exist on it (yielding `never` if it doesn't). */
72
+ type EntryAt<Contract extends ApiContract, Key> = NonNullable<Responses<Contract>[Key & keyof Responses<Contract>]>;
73
+ /** The response entry that serves a concrete status, following exact → range → `'default'`. */
74
+ type EntryForStatus<Contract extends ApiContract, Status extends HttpStatusCode> = [
75
+ EntryAt<Contract, Status>
76
+ ] extends [never] ? [EntryAt<Contract, RangeKeyOf<Status>>] extends [never] ? EntryAt<Contract, 'default'> : EntryAt<Contract, RangeKeyOf<Status>> : EntryAt<Contract, Status>;
77
+ /**
78
+ * Status codes for which the contract declares a JSON response body.
79
+ *
80
+ * Resolves to `never` for contracts that declare none, so `bodyForStatus` is uncallable there.
81
+ * Range and `'default'` keys expand to the concrete statuses they serve, so a contract
82
+ * declaring only `4xx` still allows `bodyForStatus(404)`.
83
+ */
84
+ export type ApiDeclaredResponseStatus<Contract extends ApiContract> = {
85
+ [Key in keyof Responses<Contract>]: [JsonSchemaOfEntry<EntryAt<Contract, Key>>] extends [never] ? never : StatusesForKey<Contract, Key>;
86
+ }[keyof Responses<Contract>] & HttpStatusCode;
87
+ /** Infers a schema's output type, or `never` when there is no schema. */
88
+ type InferJsonBody<Schema> = Schema extends z.ZodType ? z.output<Schema> : never;
89
+ /** The parsed JSON response body a contract declares for a concrete status. */
90
+ export type ApiDeclaredResponseBody<Contract extends ApiContract, Status extends HttpStatusCode> = InferJsonBody<JsonSchemaOfEntry<EntryForStatus<Contract, Status>>>;
91
+ /**
92
+ * Union of the SSE schema maps a contract declares, over *every* status key.
93
+ *
94
+ * Deliberately not `InferSseSuccessResponses`, which only looks at success / `'2xx'` /
95
+ * `'default'` keys: the runtime `getSseSchemaByEventName` merges the maps of every entry in
96
+ * `responsesByStatusCode`, so a stream declared under e.g. `'4xx'` produces validated events
97
+ * too and has to be visible here.
98
+ */
99
+ type ApiSSEEventSchemas<Contract extends ApiContract> = SseSchemasOfEntry<NonNullable<Responses<Contract>[keyof Responses<Contract>]>>;
100
+ /**
101
+ * The event names of a union of schema maps.
102
+ *
103
+ * `keyof` a union yields only the keys shared by every member, which collapses to `never` as
104
+ * soon as two statuses declare different events — so distribute first and union the keys,
105
+ * mirroring the runtime merge.
106
+ */
107
+ type SseEventNamesOf<Schemas> = Schemas extends unknown ? keyof Schemas & string : never;
108
+ /**
109
+ * The schema(s) a union of maps declares for one event name. Maps that don't declare it drop
110
+ * out; two maps declaring it with different schemas yield a union, since the runtime merge
111
+ * keeps only one of them and the reader can't tell which.
112
+ */
113
+ type SseSchemaForEventName<Schemas, Name extends string> = Schemas extends Record<Name, infer Schema> ? Schema : never;
114
+ /** Builds the event union for an already-resolved set of schema maps. */
115
+ type ApiSSEEventOf<Schemas> = {
116
+ [Name in SseEventNamesOf<Schemas>]: {
117
+ /** Event ID, when the server sent an `id:` field. */
118
+ id?: string;
119
+ /** Event name, as sent in the `event:` field (defaults to `message`). */
120
+ event: Name;
121
+ /** Reconnection hint in milliseconds, when the server sent a `retry:` field. */
122
+ retry?: number;
123
+ /** `data:` payload, JSON-parsed and validated against the contract's schema. */
124
+ data: InferJsonBody<SseSchemaForEventName<Schemas, Name>>;
125
+ };
126
+ }[SseEventNamesOf<Schemas>];
127
+ /**
128
+ * Discriminated union of the SSE events a contract declares, with `data` parsed and typed
129
+ * per event name. `never` for a contract that declares no SSE response at all.
130
+ */
131
+ export type ApiSSEEvent<Contract extends ApiContract> = ApiSSEEventOf<ApiSSEEventSchemas<Contract>>;
132
+ /** Whether a contract declares an SSE response on any status. */
133
+ type HasApiSSEResponse<Contract extends ApiContract> = [ApiSSEEventSchemas<Contract>] extends [
134
+ never
135
+ ] ? false : true;
136
+ /**
137
+ * The callable form of {@link InjectApiSSEResult.events}.
138
+ *
139
+ * Always a function, so the internal binder has a type to return regardless of what the
140
+ * contract declares; the result type below hides it behind {@link HasApiSSEResponse}.
141
+ */
142
+ export type ApiSSEEventReader<Contract extends ApiContract> = () => Promise<ApiSSEEvent<Contract>[]>;
143
+ /**
144
+ * Result of an {@link injectApiSSE} call.
145
+ *
146
+ * The `defineApiContract` counterpart of `InjectSSEResult`: same `closed` promise and
147
+ * `bodyForStatus` accessor, plus `events()` for reading the stream typed against the
148
+ * contract's `sseResponse` / `sseBody` schemas.
149
+ */
150
+ export type InjectApiSSEResult<Contract extends ApiContract> = {
151
+ /**
152
+ * Resolves when the response completes with the full SSE body.
153
+ * Parse the body with `parseSSEEvents()` — or use `events()` for typed events.
154
+ */
155
+ closed: Promise<SSEResponse>;
156
+ /**
157
+ * Awaits the response, asserts the status code matches, parses the body against the
158
+ * contract's JSON schema for that status, and returns the parsed object. Useful for
159
+ * asserting on the documented error responses a handler emits before streaming starts.
160
+ *
161
+ * Throws (with the offending status and a truncated body snippet) if:
162
+ * - the actual status code doesn't match the expected one;
163
+ * - the contract declares no JSON body for that status;
164
+ * - the body isn't valid JSON;
165
+ * - the body doesn't match the declared Zod schema.
166
+ *
167
+ * At the type level, `statusCode` is constrained to the statuses the contract declares a
168
+ * JSON body for. Contracts without any can't call this method (`statusCode: never`).
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * const { bodyForStatus } = injectApiSSE(app, contract, { body })
173
+ * const error = await bodyForStatus(400) // typed as z.output<400-schema>
174
+ * expect(error.message).toBe('Bad request')
175
+ * ```
176
+ */
177
+ bodyForStatus<Status extends ApiDeclaredResponseStatus<Contract>>(statusCode: Status): Promise<ApiDeclaredResponseBody<Contract, Status>>;
178
+ /**
179
+ * Awaits the response, parses the SSE body, and validates every event against the
180
+ * contract's SSE schemas, returning them as a discriminated union on `event`.
181
+ *
182
+ * Events are typed from the SSE schemas of *every* status the contract declares, merged
183
+ * exactly as the runtime merges them — a contract streaming different events on two
184
+ * statuses yields the union of both.
185
+ *
186
+ * Throws if the response isn't an SSE stream (use `bodyForStatus` for the documented
187
+ * error statuses), if an event name isn't declared by the contract, or if an event payload
188
+ * doesn't match its schema.
189
+ *
190
+ * A contract that declares no SSE response at all types this as `never`, so calling it is
191
+ * a compile error rather than a guaranteed throw — reach for `injectByApiContract` there.
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const events = await injectApiSSE(app, contract, { body }).events()
196
+ * const review = events.find((event) => event.event === 'review')
197
+ * expect(review?.data.score).toBe(42) // `data` typed by the `review` schema
198
+ * ```
199
+ */
200
+ events: HasApiSSEResponse<Contract> extends true ? ApiSSEEventReader<Contract> : never;
201
+ };
202
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apiSseTestTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiSseTestTypes.js","sourceRoot":"","sources":["../../../lib/testing/apiSseTestTypes.ts"],"names":[],"mappings":""}
@@ -1,5 +1,8 @@
1
- export { type HasSessionSpy, SSEHttpClient, type SSEHttpConnectOptions, type SSEHttpConnectResult, type SSEHttpConnectWithSpyOptions, } from './sseHttpClient.js';
1
+ export { injectApiSSE } from './apiSseInjectHelpers.js';
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';
2
4
  export { SSEInjectClient, SSEInjectConnection } from './sseInjectClient.js';
3
5
  export { injectPayloadSSE, injectSSE } from './sseInjectHelpers.js';
6
+ export { type CreateSSESessionSpyResult, createSSESessionSpy, type SSESessionSpyHooks, type SSESessionSpyRouteOptions, } from './sseSessionSpyFactory.js';
4
7
  export { SSETestServer } from './sseTestServer.js';
5
8
  export type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult, SSEConnectOptions, SSEResponse, SSETestConnection, } from './sseTestTypes.js';
@@ -1,5 +1,7 @@
1
+ export { injectApiSSE } from './apiSseInjectHelpers.js';
1
2
  export { SSEHttpClient, } from './sseHttpClient.js';
2
3
  export { SSEInjectClient, SSEInjectConnection } from './sseInjectClient.js';
3
4
  export { injectPayloadSSE, injectSSE } from './sseInjectHelpers.js';
5
+ export { createSSESessionSpy, } from './sseSessionSpyFactory.js';
4
6
  export { SSETestServer } from './sseTestServer.js';
5
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,GAId,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,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
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,GAKd,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"}
@@ -1,5 +1,5 @@
1
1
  import type { SSESession } from '../routes/fastifyRouteTypes.ts';
2
- import type { SSESessionSpy } from '../sse/SSESessionSpy.ts';
2
+ import type { SpiedSSESession, SSESessionSpy } from '../sse/SSESessionSpy.ts';
3
3
  import { type ParsedSSEEvent } from '../sse/sseParser.ts';
4
4
  /**
5
5
  * Interface for objects that have a sessionSpy (e.g., SSE controllers in test mode).
@@ -17,7 +17,8 @@ export type SSEHttpConnectOptions = {
17
17
  headers?: Record<string, string>;
18
18
  };
19
19
  /**
20
- * Options for connecting with automatic server-side connection waiting.
20
+ * Options for connecting with automatic server-side connection waiting,
21
+ * driven by a controller's built-in `connectionSpy`.
21
22
  */
22
23
  export type SSEHttpConnectWithSpyOptions = SSEHttpConnectOptions & {
23
24
  /**
@@ -32,12 +33,32 @@ export type SSEHttpConnectWithSpyOptions = SSEHttpConnectOptions & {
32
33
  timeout?: number;
33
34
  };
34
35
  };
36
+ /**
37
+ * Options for connecting with automatic server-side connection waiting,
38
+ * driven by a standalone spy from `createSSESessionSpy()`.
39
+ *
40
+ * Use this for routes built with `buildApiRoute`, which have no controller to
41
+ * read a `connectionSpy` off of.
42
+ */
43
+ export type SSEHttpConnectWithSessionSpyOptions<TSession extends SpiedSSESession> = SSEHttpConnectOptions & {
44
+ /**
45
+ * Wait for server-side connection registration after HTTP headers are received.
46
+ * This eliminates the race condition between `connect()` returning and the
47
+ * server-side handler completing connection registration.
48
+ */
49
+ awaitServerConnection: {
50
+ /** A standalone spy, wired to the route via `createSSESessionSpy()`'s `routeOptions` */
51
+ spy: SSESessionSpy<TSession>;
52
+ /** Timeout in milliseconds (default: 5000) */
53
+ timeout?: number;
54
+ };
55
+ };
35
56
  /**
36
57
  * Result when connecting with awaitServerConnection option.
37
58
  */
38
- export type SSEHttpConnectResult = {
59
+ export type SSEHttpConnectResult<TSession extends SpiedSSESession = SSESession> = {
39
60
  client: SSEHttpClient;
40
- serverConnection: SSESession;
61
+ serverConnection: TSession;
41
62
  };
42
63
  /**
43
64
  * SSE client for testing long-lived connections using real HTTP.
@@ -129,9 +150,20 @@ export declare class SSEHttpClient {
129
150
  * )
130
151
  * // serverConnection is ready to use immediately
131
152
  * await controller.sendEvent(serverConnection.id, { event: 'test', data: {} })
153
+ *
154
+ * // Same, for a `buildApiRoute` route with no controller: wire a standalone
155
+ * // spy into the route's hooks with createSSESessionSpy()
156
+ * const { spy, routeOptions } = createSSESessionSpy()
157
+ * const { client, serverConnection } = await SSEHttpClient.connect(
158
+ * 'http://localhost:3000',
159
+ * '/api/stream',
160
+ * { awaitServerConnection: { spy } }
161
+ * )
162
+ * await serverConnection.send('test', {})
132
163
  * ```
133
164
  */
134
165
  static connect(baseUrl: string, path: string, options: SSEHttpConnectWithSpyOptions): Promise<SSEHttpConnectResult>;
166
+ static connect<TSession extends SpiedSSESession>(baseUrl: string, path: string, options: SSEHttpConnectWithSessionSpyOptions<TSession>): Promise<SSEHttpConnectResult<TSession>>;
135
167
  static connect(baseUrl: string, path: string, options?: SSEHttpConnectOptions): Promise<SSEHttpClient>;
136
168
  /**
137
169
  * Async generator that yields parsed SSE events as they arrive.
@@ -86,15 +86,38 @@ export class SSEHttpClient {
86
86
  },
87
87
  signal: abortController.signal,
88
88
  });
89
- const client = new SSEHttpClient(response, abortController);
89
+ let client;
90
+ try {
91
+ client = new SSEHttpClient(response, abortController);
92
+ }
93
+ catch (error) {
94
+ // Nothing owns the response yet, so abort it here rather than leaking it.
95
+ abortController.abort();
96
+ throw error;
97
+ }
90
98
  // If awaitServerConnection is specified, wait for server-side registration
91
99
  if (options && 'awaitServerConnection' in options && options.awaitServerConnection) {
92
- const { controller, timeout } = options.awaitServerConnection;
93
- const serverConnection = await controller.connectionSpy.waitForConnection({
94
- timeout: timeout ?? 5000,
100
+ const awaitOptions = options.awaitServerConnection;
101
+ const waitOptions = {
102
+ timeout: awaitOptions.timeout ?? 5000,
95
103
  predicate: (conn) => conn.request.url === pathWithQuery,
96
- });
97
- return { client, serverConnection };
104
+ };
105
+ try {
106
+ // Both branches call the same method; the spy is invariant in its session
107
+ // type, so they cannot be collapsed into one reference.
108
+ const serverConnection = 'spy' in awaitOptions
109
+ ? await awaitOptions.spy.waitForConnection(waitOptions)
110
+ : await awaitOptions.controller.connectionSpy.waitForConnection(waitOptions);
111
+ return { client, serverConnection };
112
+ }
113
+ catch (error) {
114
+ // The HTTP connection is already established and the caller never gets a
115
+ // handle to it, so close it here. Left open, a keep-alive route keeps the
116
+ // socket streaming and the test's `app.close()` hangs, hiding this error
117
+ // behind a suite-level timeout.
118
+ client.close();
119
+ throw error;
120
+ }
98
121
  }
99
122
  return client;
100
123
  }
@@ -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>;