@kubb/plugin-client 5.12.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/client-C-0-puI2.d.ts +554 -0
  3. package/dist/index.cjs +1377 -784
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +117 -78
  6. package/dist/index.d.ts +117 -78
  7. package/dist/index.js +1375 -786
  8. package/dist/index.js.map +1 -1
  9. package/dist/templates/client.d.ts +3 -2
  10. package/dist/templates/core.d.ts +3 -3
  11. package/dist/templates/mediaTypes/formData.d.ts +5 -0
  12. package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
  13. package/dist/templates/mediaTypes.d.ts +2 -0
  14. package/dist/templates/serializers/cookies.d.ts +5 -0
  15. package/dist/templates/serializers/headers.d.ts +5 -0
  16. package/dist/templates/serializers/path.d.ts +5 -0
  17. package/dist/templates/serializers/pathStyles.d.ts +5 -0
  18. package/dist/templates/serializers/query.d.ts +5 -0
  19. package/dist/templates/serializers/queryStyles.d.ts +14 -0
  20. package/dist/templates/serializers.d.ts +118 -2
  21. package/dist/templates/sse.d.ts +20 -113
  22. package/dist/templates/standardSchema.d.ts +81 -2
  23. package/docs/guides/authentication.md +12 -2
  24. package/docs/guides/calling-operations.md +8 -6
  25. package/docs/guides/migrate-to-v6.md +139 -0
  26. package/docs/guides/validate-requests-and-responses.md +13 -8
  27. package/docs/guides/write-your-client.md +118 -12
  28. package/docs/options.md +5 -3
  29. package/docs/overview.md +3 -1
  30. package/kubb-extension.json +21 -3
  31. package/package.json +6 -5
  32. package/src/builders/callConfig.ts +80 -0
  33. package/src/builders/contentType.ts +35 -0
  34. package/src/builders/generics.ts +29 -0
  35. package/src/builders/operationBody.ts +43 -0
  36. package/src/builders/operationData.ts +64 -0
  37. package/src/builders/returnStatement.ts +103 -0
  38. package/src/builders/security.ts +125 -0
  39. package/src/builders/signature.ts +71 -0
  40. package/src/builders/styles.ts +94 -0
  41. package/src/builders/validator.ts +84 -0
  42. package/src/builders/validatorOptions.ts +53 -0
  43. package/src/clientTypes.ts +195 -0
  44. package/src/components/ClientRuntime.tsx +97 -0
  45. package/src/components/Operation.tsx +72 -0
  46. package/src/components/SdkClient.tsx +100 -0
  47. package/src/components/SdkFacade.tsx +35 -0
  48. package/src/generators/clientGenerator.tsx +91 -6
  49. package/src/generators/sdkGenerator.tsx +208 -0
  50. package/src/index.ts +3 -1
  51. package/src/macros.ts +9 -0
  52. package/src/plugin.ts +7 -8
  53. package/src/resolver.ts +28 -0
  54. package/src/setup.ts +90 -0
  55. package/src/types.ts +4 -2
  56. package/templates/client.ts +2 -1
  57. package/templates/core.ts +735 -286
  58. package/templates/mediaTypes/formData.ts +59 -0
  59. package/templates/mediaTypes/urlencoded.ts +33 -0
  60. package/templates/mediaTypes.ts +147 -0
  61. package/templates/serializers/cookies.ts +58 -0
  62. package/templates/serializers/headers.ts +35 -0
  63. package/templates/serializers/path.ts +19 -0
  64. package/templates/serializers/pathStyles.ts +27 -0
  65. package/templates/serializers/query.ts +21 -0
  66. package/templates/serializers/queryStyles.ts +107 -0
  67. package/templates/serializers.ts +48 -364
  68. package/templates/sse.ts +87 -178
  69. package/templates/standardSchema.ts +16 -37
  70. package/dist/client-BxO6Dygu.d.ts +0 -355
  71. package/dist/serializers-Ds8zZwOO.d.ts +0 -190
  72. package/dist/standardSchema-BShJWz2i.d.ts +0 -104
package/templates/core.ts CHANGED
@@ -1,108 +1,147 @@
1
- import type { NativeRequest, NativeResponse } from './client.ts'
2
- import {
3
- applyHeaderStyles,
4
- baseContentType,
5
- defaultBodySerializer,
6
- defaultPathSerializer,
7
- defaultQuerySerializer,
8
- isDefaultJsonBody,
9
- isJsonMediaType,
10
- serializeCookies,
11
- } from './serializers.ts'
12
- import type { HeadersInit, HttpMethod, PathParamStyle, PathSerializer, RequestBody, Serializers, Styles } from './serializers.ts'
1
+ import type { NativeOptions, NativeRequest, NativeResponse } from './client.ts'
2
+ import { defaultMediaTypes, isEncodedBody, resolveMediaType } from './mediaTypes.ts'
3
+ import type { MediaTypeContext, MediaTypes } from './mediaTypes.ts'
4
+ import { baseContentType, toValue } from './serializers.ts'
5
+ import type { HeadersInit, HttpMethod, RequestBody, Serializers, Styles } from './serializers.ts'
13
6
  import { isValidationEnabled, runValidator, type StandardSchemaValidator } from './standardSchema.ts'
14
7
  import type { ValidateOption, ValidationErrorHandler } from './standardSchema.ts'
15
8
 
9
+ export * from './mediaTypes.ts'
10
+
16
11
  /**
17
- * HTTP status codes treated as a success, everything else is an error.
12
+ * The status keys that count as success. `2xx` covers every 2xx status.
18
13
  */
19
- export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
14
+ export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226' | '2xx'
20
15
 
21
16
  export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
22
17
 
23
18
  export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
24
19
 
20
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
21
+
22
+ type ToNumber<T> = T extends `${infer TNumber extends number}` ? TNumber : never
23
+
25
24
  /**
26
- * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
25
+ * Maps a status key such as `404` or `4xx` to its numeric statuses. `default` becomes `number`.
27
26
  */
28
- export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
27
+ export type ToStatusNumber<TStatus, TKeys = never> = TStatus extends `${infer TNumber extends number}`
28
+ ? TNumber
29
+ : TStatus extends `${infer TFirst extends '1' | '2' | '3' | '4' | '5'}xx`
30
+ ? Exclude<ToNumber<`${TFirst}${Digit}${Digit}`>, ToNumber<TKeys>>
31
+ : number
29
32
 
30
33
  /**
31
- * Unwraps content-type variants to keep the bare body on `error`.
34
+ * The `error` of a failed call without a body, so `if (error)` still sees the failure.
32
35
  */
36
+ export type EmptyErrorBody = { status: number; statusText: string }
37
+
33
38
  export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
34
39
 
35
40
  /**
36
- * Keeps `contentType` beside `data` so switching on it narrows the body.
41
+ * A response's `Headers`, whose `get` suggests the header names the spec declares. Values stay strings, as sent.
37
42
  */
38
- export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
43
+ export type ResponseHeaders<THeaders = unknown> = Omit<Headers, 'get'> & {
44
+ get: (name: Extract<keyof THeaders, string> | (string & {})) => string | null
45
+ }
46
+
47
+ // The media types `streamMediaTypes` reads as a stream: a response with one of them resolves to its raw body.
48
+ type StreamContentType =
49
+ | `text/event-stream${string}`
50
+ | `application/jsonl${string}`
51
+ | `application/x-ndjson${string}`
52
+ | `application/json-seq${string}`
53
+ | `application/${string}+json-seq${string}`
54
+
55
+ type HeadersFor<THeaders, TStatus> = ResponseHeaders<TStatus extends keyof THeaders ? THeaders[TStatus] : unknown>
56
+
57
+ export type SuccessVariant<TStatus, TEntry, TRequest, TResponse, TKeys = never, THeaders = unknown> = TEntry extends { contentType: string; data: unknown }
39
58
  ? TEntry extends { contentType: infer TContentType; data: infer TData }
40
- ? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
59
+ ? {
60
+ status: ToStatusNumber<TStatus, TKeys>
61
+ data: TContentType extends StreamContentType ? ReadableStream<Uint8Array> : TData
62
+ error: undefined
63
+ contentType: TContentType
64
+ headers: HeadersFor<THeaders, TStatus>
65
+ request: TRequest
66
+ response: TResponse
67
+ }
41
68
  : never
42
- : { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
69
+ : {
70
+ status: ToStatusNumber<TStatus, TKeys>
71
+ data: TEntry
72
+ error: undefined
73
+ contentType: string | undefined
74
+ headers: HeadersFor<THeaders, TStatus>
75
+ request: TRequest
76
+ response: TResponse
77
+ }
43
78
 
44
- /**
45
- * Narrows `data` or `error` when switching on the numeric status.
46
- */
47
- export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
48
- ? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
79
+ export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse, THeaders = unknown> = TStatus extends SuccessStatusCode
80
+ ? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse, keyof TResponses, THeaders>
49
81
  : {
50
- status: ToStatusNumber<TStatus>
82
+ status: ToStatusNumber<TStatus, keyof TResponses>
51
83
  data: undefined
52
- error: DataOf<TResponses[TStatus]>
84
+ error: DataOf<TResponses[TStatus]> | EmptyErrorBody
53
85
  contentType: string | undefined
86
+ headers: HeadersFor<THeaders, TStatus>
54
87
  request: TRequest
55
88
  response: TResponse
56
89
  }
57
90
 
58
- export type ResultUnion<TResponses, TRequest, TResponse> = {
59
- [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
91
+ export type ResultUnion<TResponses, TRequest, TResponse, THeaders = unknown> = {
92
+ [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse, THeaders>
60
93
  }[keyof TResponses]
61
94
 
62
- /**
63
- * Selects success statuses without letting untyped error payloads widen `data`.
64
- */
65
- export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
66
- [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
95
+ export type SuccessResultUnion<TResponses, TRequest, TResponse, THeaders = unknown> = {
96
+ [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse, THeaders>
67
97
  }[Extract<keyof TResponses, SuccessStatusCode>]
68
98
 
69
99
  /**
70
- * Narrows `data` and `error` by status. With `throwOnError`, includes only success variants.
100
+ * The result of a call, narrowed by `status`. With `throwOnError`, it holds only the success variants. `THeaders` keys the
101
+ * declared response headers by status like the responses record, so `headers.get` suggests them for the narrowed status.
71
102
  */
72
- export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = ThrowOnError extends true
73
- ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
103
+ export type RequestResult<
104
+ TResponses,
105
+ ThrowOnError extends boolean = true,
106
+ TRequest = NativeRequest,
107
+ TResponse = NativeResponse,
108
+ THeaders = unknown,
109
+ > = ThrowOnError extends true
110
+ ? [SuccessResultUnion<TResponses, TRequest, TResponse, THeaders>] extends [never]
74
111
  ? {
75
112
  status: number
76
113
  data: SuccessOf<TResponses>
77
114
  error: undefined
78
115
  contentType: string | undefined
116
+ headers: ResponseHeaders
79
117
  request: TRequest
80
118
  response: TResponse
81
119
  }
82
- : SuccessResultUnion<TResponses, TRequest, TResponse>
83
- : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
84
- ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
85
- : ResultUnion<TResponses, TRequest, TResponse>
120
+ : SuccessResultUnion<TResponses, TRequest, TResponse, THeaders>
121
+ : [ResultUnion<TResponses, TRequest, TResponse, THeaders>] extends [never]
122
+ ? { status: number; data: undefined; error: undefined; contentType: string | undefined; headers: ResponseHeaders; request: TRequest; response: TResponse }
123
+ : ResultUnion<TResponses, TRequest, TResponse, THeaders>
86
124
 
87
125
  /**
88
- * A `RequestResult` promise with an extra `unwrap()` method that resolves to the success body.
126
+ * `RequestResult` with the declared response headers by status: `RequestResultWithHeaders<ListPetsResponses, { '200': ListPetsStatus200Headers }>`.
127
+ */
128
+ export type RequestResultWithHeaders<TResponses, THeaders, ThrowOnError extends boolean = true> = RequestResult<
129
+ TResponses,
130
+ ThrowOnError,
131
+ NativeRequest,
132
+ NativeResponse,
133
+ THeaders
134
+ >
135
+
136
+ /**
137
+ * A result promise whose `unwrap()` resolves to the success body.
89
138
  */
90
139
  export type Unwrappable<T extends { data: unknown; error: unknown }> = Promise<T> & {
91
140
  unwrap: () => Promise<Extract<T, { error: undefined }>['data']>
92
141
  }
93
142
 
94
143
  /**
95
- * Attaches `unwrap()` to a result promise, which rejects with `error` when the result carried one.
96
- *
97
- * @example
98
- * ```ts
99
- * const { data, error } = await getPetById({ path: { petId: 1 } })
100
- * ```
101
- *
102
- * @example
103
- * ```ts
104
- * const pet = await getPetById({ path: { petId: 1 } }).unwrap()
105
- * ```
144
+ * Adds `unwrap()` to a result promise. It rejects with `error` when the result has one.
106
145
  */
107
146
  export function withUnwrap<T extends { data: unknown; error: unknown }>(promise: Promise<T>): Unwrappable<T> {
108
147
  const unwrappable = promise as Unwrappable<T>
@@ -114,64 +153,68 @@ export function withUnwrap<T extends { data: unknown; error: unknown }>(promise:
114
153
  return unwrappable
115
154
  }
116
155
 
117
- /**
118
- * The shape a generated operation returns with `returnType: 'data'`: the success body when
119
- * `throwOnError` is true, or the full `RequestResult` when it is false.
120
- */
121
- export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = ThrowOnError extends true
122
- ? RequestResult<TResponses, true, TRequest, TResponse>['data']
123
- : RequestResult<TResponses, ThrowOnError, TRequest, TResponse>
156
+ export type Unwrapped<T, ThrowOnError extends boolean> = ThrowOnError extends true ? (T extends { data: infer TData } ? TData : never) : T
124
157
 
125
158
  /**
126
- * Returns the success body when `throwOnError` is true, or the full result when it is false.
159
+ * What an operation returns with `returnType: 'data'`: the success body, or the full result when `throwOnError` is false.
127
160
  */
128
- export function unwrapResult<T extends { data: unknown; error: unknown }>(promise: Promise<T>, throwOnError: boolean | undefined): Promise<T | T['data']> {
129
- return promise.then((result) => ((throwOnError ?? true) ? result.data : result))
130
- }
131
-
132
- export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
133
-
134
- export type RequestCredentials = 'omit' | 'same-origin' | 'include'
135
- export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
161
+ export type UnwrappedResult<
162
+ TResponses,
163
+ ThrowOnError extends boolean = true,
164
+ TRequest = NativeRequest,
165
+ TResponse = NativeResponse,
166
+ THeaders = unknown,
167
+ > = Unwrapped<RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>, ThrowOnError>
136
168
 
137
169
  /**
138
- * Parser per response content type. Generated when one success response declares several content
139
- * types that do not share a single parser. Keys are base media types, such as `audio/mpeg` or `audio/*`.
170
+ * `UnwrappedResult` with the declared response headers by status, for the full result a `throwOnError: false` call resolves to.
140
171
  */
141
- export type ResponseTypeMap = Record<string, ResponseType>
172
+ export type UnwrappedResultWithHeaders<TResponses, THeaders, ThrowOnError extends boolean = true> = UnwrappedResult<
173
+ TResponses,
174
+ ThrowOnError,
175
+ NativeRequest,
176
+ NativeResponse,
177
+ THeaders
178
+ >
142
179
 
143
180
  /**
144
- * Decodes a response body for a codec's media type.
181
+ * Resolves to the success body when `throwOnError` is true, or to the full result when it is false.
145
182
  */
146
- export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
183
+ export function unwrapResult<T extends { data: unknown; error: unknown }, ThrowOnError extends boolean>(
184
+ promise: Promise<T>,
185
+ throwOnError: ThrowOnError,
186
+ ): Promise<Unwrapped<T, ThrowOnError>> {
187
+ return promise.then((result) => (throwOnError ? result.data : result) as Unwrapped<T, ThrowOnError>)
188
+ }
147
189
 
148
190
  /**
149
- * Encodes a request body for a codec's media type.
191
+ * The call's `throwOnError`, or the operation's generated default when the call leaves it unset.
150
192
  */
151
- export type ContentBodySerializer = (body: unknown, contentType?: string) => RequestBody | undefined
193
+ export function resolveThrowOnError<ThrowOnError extends boolean>(throwOnError: ThrowOnError | undefined, fallback: boolean): ThrowOnError {
194
+ return (throwOnError ?? fallback) as ThrowOnError
195
+ }
196
+
197
+ export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
198
+
199
+ export type RequestCredentials = 'omit' | 'same-origin' | 'include'
152
200
 
153
201
  /**
154
- * Encodes requests or decodes responses for a media type. Either direction can be omitted.
202
+ * Reads a 2xx body as this format instead of by its media type. An error body still uses its media type.
155
203
  */
156
- export type Codec = {
157
- serialize?: ContentBodySerializer
158
- deserialize?: Deserializer
159
- }
204
+ export type ParseAs = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream'
160
205
 
161
206
  /**
162
- * A string sets the request content type. The object form can also set `Accept`.
207
+ * The request content type, or `{ request, response }` to also set `Accept`.
163
208
  */
164
209
  export type ContentType = string | { request?: string; response?: string }
165
210
 
166
- /**
167
- * Validates request bodies and params, success bodies, or non-throwing error bodies with Standard Schema.
168
- */
169
211
  export type Validator<T = unknown> = StandardSchemaValidator<T>
170
212
 
171
213
  export type { ValidateOption, ValidationErrorContext, ValidationErrorHandler } from './standardSchema.ts'
172
214
 
173
215
  /**
174
- * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
216
+ * A security scheme of a generated call, passed to the `auth` resolver. OAuth2 `flows` and `metadataUrl` (RFC 8414) and
217
+ * the OpenID Connect `url` tell the resolver where to get a token.
175
218
  */
176
219
  export type Auth = {
177
220
  type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
@@ -179,49 +222,116 @@ export type Auth = {
179
222
  name?: string
180
223
  in?: 'header' | 'query' | 'cookie'
181
224
  scopes?: Array<string>
225
+ flows?: Record<string, { authorizationUrl?: string; deviceAuthorizationUrl?: string; tokenUrl?: string; refreshUrl?: string }>
226
+ metadataUrl?: string
227
+ url?: string
182
228
  }
183
229
 
184
230
  /**
185
- * Return `user:password` for basic auth, or `undefined` to skip a scheme.
231
+ * One scheme, or the schemes one requirement sends together.
232
+ */
233
+ export type SecurityRequirement = Auth | Array<Auth>
234
+
235
+ /**
236
+ * `user:password` for basic auth, or `undefined` to skip the scheme.
186
237
  */
187
238
  export type AuthToken = string | undefined
188
239
 
189
240
  /**
190
- * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
241
+ * A static token, or a callback per scheme. The first requirement whose schemes all get a token is sent.
191
242
  */
192
243
  export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
193
244
 
245
+ /**
246
+ * One Server-Sent Event. `data` is parsed JSON when it parses, otherwise the raw string.
247
+ */
248
+ export type ServerSentEvent<TData = unknown> = {
249
+ data: TData
250
+ event: string
251
+ id?: string
252
+ retry?: number
253
+ }
254
+
255
+ /**
256
+ * An event the document types by name, plus the `id` and `retry` any event may carry.
257
+ */
258
+ export type TypedEvent<TEvent> = TEvent & Pick<ServerSentEvent, 'id' | 'retry'>
259
+
260
+ /**
261
+ * Per-call options of a stream operation.
262
+ */
263
+ export type EventStreamOptions = {
264
+ /**
265
+ * The most characters one event or JSON line may hold. A larger one throws a `RangeError`, and `Infinity` turns the limit off.
266
+ * @default 16777216
267
+ */
268
+ maxEventSize?: number | undefined
269
+ /**
270
+ * Ends the stream when an event's raw `data` or a JSON line equals this value, such as `[DONE]`.
271
+ */
272
+ terminator?: string | undefined
273
+ /**
274
+ * Maps each Server-Sent Event before validation.
275
+ */
276
+ transform?: (event: ServerSentEvent) => ServerSentEvent
277
+ /**
278
+ * What `validator.response` checks on each event: the payload (`data`) or the whole event (`event`).
279
+ */
280
+ eventShape?: 'event' | 'data'
281
+ /**
282
+ * Keeps each Server-Sent Event's `data` as the raw text instead of JSON-parsing it. The generated client sets it when the
283
+ * document types `data` as a string. A list of event names keeps it for those events only.
284
+ */
285
+ rawData?: boolean | Array<string> | undefined
286
+ }
287
+
194
288
  export type RequestConfig<TBody = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
195
289
  baseURL?: string
196
290
  url?: string
197
291
  method?: HttpMethod
198
292
  path?: unknown
199
293
  query?: unknown
200
- /** Alias of `query`, kept for callers that pass the query under this name. */
294
+ /**
295
+ * Alias of `query`.
296
+ */
201
297
  params?: unknown
202
298
  cookies?: unknown
203
299
  body?: TBody
204
300
  headers?: unknown
205
- styles?: Styles
206
- signal?: AbortSignal
207
- credentials?: RequestCredentials
208
- options?: Record<string, unknown>
301
+ styles?: Styles | undefined
302
+ signal?: AbortSignal | undefined
303
+ credentials?: RequestCredentials | undefined
304
+ options?: NativeOptions | undefined
209
305
  contentType?: ContentType
210
- responseType?: ResponseType | ResponseTypeMap
306
+ parseAs?: ParseAs
211
307
  throwOnError?: boolean
212
308
  client?: ClientInstance<TRequest, TResponse>
213
309
  transport?: Transport<TRequest, TResponse>
214
310
  serializer?: Serializers
215
- codecs?: Record<string, Codec>
216
- validator?: { request?: Validator; path?: Validator; query?: Validator; headers?: Validator; response?: Validator; error?: Validator }
311
+ /**
312
+ * Merged per key over the client's media types.
313
+ */
314
+ mediaTypes?: MediaTypes
315
+ /**
316
+ * `error` takes one validator, or a map by status such as `{ 404: schema, '5xx': schema, default: schema }`.
317
+ */
318
+ validator?: {
319
+ request?: Validator
320
+ path?: Validator
321
+ query?: Validator
322
+ headers?: Validator
323
+ cookies?: Validator
324
+ response?: Validator
325
+ error?: Validator | Record<string, Validator>
326
+ }
217
327
  onValidationError?: ValidationErrorHandler
218
328
  validate?: ValidateOption
219
- security?: Array<Auth>
329
+ security?: Array<SecurityRequirement>
220
330
  auth?: AuthResolver
221
331
  }
222
332
 
223
333
  /**
224
- * Combines client options with the operation's typed request params and body.
334
+ * The options of a generated operation: its typed request groups plus the call config.
225
335
  */
226
336
  export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = Omit<
227
337
  RequestConfig<unknown, TRequest, TResponse>,
@@ -232,107 +342,187 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
232
342
  throwOnError?: ThrowOnError
233
343
  }
234
344
 
345
+ export type StreamOptions<TData extends DataShape, ThrowOnError extends boolean = true> = Options<TData, ThrowOnError> & EventStreamOptions
346
+
235
347
  /**
236
- * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
348
+ * Config shared by every call of a client. A call's own config overrides it.
237
349
  */
238
350
  export type ClientConfig<TRequest = NativeRequest, TResponse = NativeResponse> = {
239
351
  baseURL?: string
240
352
  headers?: HeadersInit
241
353
  credentials?: RequestCredentials
242
- options?: Record<string, unknown>
354
+ options?: NativeOptions
243
355
  throwOnError?: boolean
244
356
  transport?: Transport<TRequest, TResponse>
245
357
  serializer?: Serializers
246
- codecs?: Record<string, Codec>
358
+ /**
359
+ * Merged per key over `defaultMediaTypes`.
360
+ */
361
+ mediaTypes?: MediaTypes
247
362
  onValidationError?: ValidationErrorHandler
248
363
  validate?: ValidateOption
249
364
  auth?: AuthResolver
250
365
  }
251
366
 
252
367
  /**
253
- * The normalized request the transport receives, with all serialization, auth, and header work already done.
368
+ * The request a transport sends, with its URL, headers, body, and auth already resolved.
254
369
  */
255
370
  export type ResolvedRequest = {
256
371
  url: string
257
372
  method: string
258
373
  headers: Record<string, string>
259
- body?: RequestBody
260
- signal?: AbortSignal
261
- credentials?: RequestCredentials
262
- options?: Record<string, unknown>
263
- responseType?: ResponseType | ResponseTypeMap
374
+ body?: RequestBody | undefined
375
+ signal?: AbortSignal | undefined
376
+ credentials?: RequestCredentials | undefined
377
+ options?: NativeOptions | undefined
378
+ /**
379
+ * Read the response as a stream. A transport that buffers bodies, such as axios, must stream it.
380
+ */
381
+ stream?: boolean
264
382
  }
265
383
 
266
384
  /**
267
- * What a transport returns: the parsed body plus the native request and response objects.
385
+ * A transport's `Response` with the native request and response that results and errors carry.
268
386
  */
269
- export type TransportResult<TData = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
270
- data: TData
271
- status: number
272
- statusText: string
273
- headers: Headers
274
- contentType?: string
387
+ export type TransportResult<TRequest = NativeRequest, TResponse = NativeResponse> = {
388
+ webResponse: Response
275
389
  request: TRequest
276
390
  response: TResponse
277
391
  }
278
392
 
279
393
  /**
280
- * The per-plugin send, supplied to `createClientCore` as `defaultTransport`.
394
+ * Sends a resolved request and returns its `Response` with the body unread. The client decodes it.
281
395
  */
282
396
  export type Transport<TRequest = NativeRequest, TResponse = NativeResponse> = (
283
397
  request: ResolvedRequest,
284
- ) => Promise<TransportResult<unknown, TRequest, TResponse>>
398
+ ) => Promise<Response | TransportResult<TRequest, TResponse>>
285
399
 
286
- export type CallResult<TRequest = NativeRequest, TResponse = NativeResponse> = {
400
+ /**
401
+ * What a response interceptor gets: the decoded body with the status, headers, and native objects.
402
+ */
403
+ export type DecodedResponse<TData = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
404
+ data: TData
287
405
  status: number
288
- data: unknown
289
- error: unknown
290
- contentType: string | undefined
406
+ statusText: string
407
+ headers: Headers
408
+ contentType?: string | undefined
291
409
  request: TRequest
292
410
  response: TResponse
293
411
  }
294
412
 
295
- export type InterceptorFn<T, TContext = never> = (value: T, context?: TContext) => T | Promise<T>
413
+ export type InterceptorFn<T, TContext = never, TResult = T> = (value: T, context?: TContext) => TResult | Promise<TResult>
296
414
 
297
- /**
298
- * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API.
299
- */
300
- export type InterceptorStack<T, TContext = never> = {
301
- use: (fn: InterceptorFn<T, TContext>) => number
415
+ export type InterceptorStack<T, TContext = never, TResult = T> = {
416
+ use: (fn: InterceptorFn<T, TContext, TResult>) => number
302
417
  eject: (id: number) => void
303
- update: (id: number, fn: InterceptorFn<T, TContext>) => void
418
+ update: (id: number, fn: InterceptorFn<T, TContext, TResult>) => void
304
419
  run: (value: T, context?: TContext) => Promise<T>
305
420
  }
306
421
 
307
422
  export type Interceptors<TRequest = NativeRequest, TResponse = NativeResponse> = {
308
423
  request: InterceptorStack<ResolvedRequest, RequestConfig<unknown, TRequest, TResponse>>
309
- response: InterceptorStack<TransportResult<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
310
- error: InterceptorStack<ResponseError<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
424
+ response: InterceptorStack<DecodedResponse<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
425
+ /**
426
+ * Sees every failure: a `ResponseError`, a network error, an abort, or a validation error. Return another error to throw it instead.
427
+ */
428
+ error: InterceptorStack<ResponseError<unknown, TRequest, TResponse> | Error, RequestConfig<unknown, TRequest, TResponse>, Error | void>
311
429
  }
312
430
 
313
431
  export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>
314
432
 
433
+ type StreamParts<TEvent> = AsyncIterable<TEvent> & {
434
+ stream: AsyncGenerator<TEvent>
435
+ /**
436
+ * The last event ID the server sent, for a `Last-Event-ID` header on a new call.
437
+ */
438
+ readonly lastEventId: string
439
+ }
440
+
441
+ type WithEvents<TResult, TEvent> = TResult extends { error: undefined } ? Omit<TResult, 'data'> & { data: AsyncGenerator<TEvent> } : TResult
442
+
315
443
  /**
316
- * A client instance: the callable send plus configuration, interceptors, and an isolated
317
- * `createClient` factory bound to the same transport.
444
+ * The result of a stream call. Iterate it, or its `data`, for the items. A failed call yields none.
445
+ */
446
+ export type StreamResult<
447
+ TResponses,
448
+ ThrowOnError extends boolean = true,
449
+ TEvent = ServerSentEvent<SuccessOf<TResponses>>,
450
+ TRequest = NativeRequest,
451
+ TResponse = NativeResponse,
452
+ THeaders = unknown,
453
+ > = WithEvents<RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>, TEvent> & StreamParts<TEvent>
454
+
455
+ /**
456
+ * `StreamResult` with the declared response headers by status: `StreamResultWithHeaders<StreamEventsResponses, { '200': StreamEventsStatus200Headers }>`.
457
+ */
458
+ export type StreamResultWithHeaders<TResponses, THeaders, ThrowOnError extends boolean = true, TEvent = ServerSentEvent<SuccessOf<TResponses>>> = StreamResult<
459
+ TResponses,
460
+ ThrowOnError,
461
+ TEvent,
462
+ NativeRequest,
463
+ NativeResponse,
464
+ THeaders
465
+ >
466
+
467
+ /**
468
+ * What a stream operation returns with `returnType: 'data'`.
469
+ */
470
+ export type UnwrappedStreamResult<
471
+ TResponses,
472
+ ThrowOnError extends boolean = true,
473
+ TEvent = ServerSentEvent<SuccessOf<TResponses>>,
474
+ THeaders = unknown,
475
+ > = Unwrapped<StreamResult<TResponses, ThrowOnError, TEvent, NativeRequest, NativeResponse, THeaders>, ThrowOnError>
476
+
477
+ /**
478
+ * `UnwrappedStreamResult` with the declared response headers by status, for the full result a `throwOnError: false` call resolves to.
479
+ */
480
+ export type UnwrappedStreamResultWithHeaders<
481
+ TResponses,
482
+ THeaders,
483
+ ThrowOnError extends boolean = true,
484
+ TEvent = ServerSentEvent<SuccessOf<TResponses>>,
485
+ > = UnwrappedStreamResult<TResponses, ThrowOnError, TEvent, THeaders>
486
+
487
+ /**
488
+ * A client. Call it to send a request.
318
489
  */
319
490
  export type ClientInstance<TRequest = NativeRequest, TResponse = NativeResponse> = {
320
- <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
491
+ <TResponses = unknown, ThrowOnError extends boolean = true, THeaders = unknown>(
492
+ config: RequestConfig<unknown, TRequest, TResponse> & { throwOnError?: ThrowOnError },
493
+ ): Promise<RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>>
494
+ /**
495
+ * Sends the request and reads a 2xx body item by item. A body whose media type cannot stream throws a `TypeError`.
496
+ */
497
+ stream: <TResponses = unknown, ThrowOnError extends boolean = true, TItem = ServerSentEvent<SuccessOf<TResponses>>, THeaders = unknown>(
498
+ config: RequestConfig<unknown, TRequest, TResponse> & EventStreamOptions & { throwOnError?: ThrowOnError },
499
+ ) => Promise<StreamResult<TResponses, ThrowOnError, TItem, TRequest, TResponse, THeaders>>
321
500
  getConfig: () => ClientConfig<TRequest, TResponse>
322
501
  setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
323
502
  getUrl: (config: UrlConfig) => string
324
503
  interceptors: Interceptors<TRequest, TResponse>
504
+ /**
505
+ * A client with this config and no interceptors.
506
+ */
325
507
  createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
508
+ /**
509
+ * A client with this config that runs this client's interceptors before its own.
510
+ */
511
+ extend: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
326
512
  }
327
513
 
328
514
  /**
329
- * Thrown for a non-2xx response, so a resolved call always means success.
515
+ * Thrown for a non-2xx response when `throwOnError` is true.
330
516
  */
331
517
  export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse> extends Error {
332
518
  data: TError
333
519
  status: number
334
520
  statusText: string
335
521
  contentType: string | undefined
522
+ /**
523
+ * The response's `Headers`, the same on every transport.
524
+ */
525
+ headers: ResponseHeaders
336
526
  request: TRequest
337
527
  response: TResponse
338
528
 
@@ -340,13 +530,14 @@ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse
340
530
  data: TError
341
531
  status: number
342
532
  statusText: string
343
- contentType?: string
533
+ contentType?: string | undefined
534
+ headers?: Headers | undefined
344
535
  request: TRequest
345
536
  response: TResponse
346
- method?: string
347
- url?: string
537
+ method?: string | undefined
538
+ url?: string | undefined
348
539
  }) {
349
- // The query and hash are left out: sensitive query parameters or fragments must not reach logs.
540
+ // The query and hash stay out of the message, so secrets in them do not reach logs.
350
541
  const cleanUrl = config.url?.split(/[?#]/)[0]
351
542
  const target = [config.method, cleanUrl].filter(Boolean).join(' ')
352
543
  const statusText = config.statusText?.trim()
@@ -356,12 +547,13 @@ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse
356
547
  this.status = config.status
357
548
  this.statusText = config.statusText
358
549
  this.contentType = config.contentType
550
+ this.headers = config.headers ?? new Headers()
359
551
  this.request = config.request
360
552
  this.response = config.response
361
553
  }
362
554
 
363
555
  /**
364
- * Matches on `name`, not `instanceof`: every generated client bundles its own `ResponseError` class.
556
+ * Matches on `name`, since every generated client has its own `ResponseError` class.
365
557
  */
366
558
  static is(error: unknown): error is ResponseError<unknown, unknown, unknown> {
367
559
  return error instanceof Error && error.name === 'ResponseError'
@@ -370,8 +562,41 @@ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse
370
562
 
371
563
  export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
372
564
 
565
+ type ErrorStatus<TResponses> = Exclude<keyof TResponses, SuccessStatusCode>
566
+
567
+ /**
568
+ * The `ResponseError` an operation throws. Checking `status` narrows `data` to that status's body, and `headers.get` to
569
+ * the header names in `THeaders`, the same record `RequestResult` takes.
570
+ */
571
+ export type ResponseErrorOf<TResponses, TRequest = NativeRequest, TResponse = NativeResponse, THeaders = unknown> = [ErrorStatus<TResponses>] extends [never]
572
+ ? ResponseError<unknown, TRequest, TResponse>
573
+ : {
574
+ [TStatus in ErrorStatus<TResponses>]: ResponseError<DataOf<TResponses[TStatus]>, TRequest, TResponse> & {
575
+ status: ToStatusNumber<TStatus, keyof TResponses>
576
+ headers: HeadersFor<THeaders, TStatus>
577
+ }
578
+ }[ErrorStatus<TResponses>]
579
+
580
+ /**
581
+ * Narrows a caught error to the `ResponseError` of the operation whose responses record you pass.
582
+ *
583
+ * @example
584
+ * ```ts
585
+ * try {
586
+ * await getPetById({ path: { petId: 7 } })
587
+ * } catch (error) {
588
+ * if (isResponseError<GetPetByIdResponses>(error) && error.status === 404) error.data // GetPetById404
589
+ * }
590
+ * ```
591
+ */
592
+ export function isResponseError<TResponses = unknown, TRequest = NativeRequest, TResponse = NativeResponse, THeaders = unknown>(
593
+ error: unknown,
594
+ ): error is ResponseErrorOf<TResponses, TRequest, TResponse, THeaders> {
595
+ return ResponseError.is(error)
596
+ }
597
+
373
598
  function headerEntries(headers: object): Array<[string, unknown]> {
374
- if (Array.isArray(headers)) return headers
599
+ if (Array.isArray(headers)) return headers as Array<[string, unknown]>
375
600
  return headers instanceof Headers ? [...headers] : Object.entries(headers)
376
601
  }
377
602
 
@@ -381,7 +606,8 @@ function serializeHeaders(headers: HeadersInit | undefined): Record<string, stri
381
606
  const result: Record<string, string> = {}
382
607
  for (const [key, value] of entries) {
383
608
  if (value === undefined || value === null) continue
384
- result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
609
+ // A `Date` is a unit like in the parameter serializers, not an object to send as JSON.
610
+ result[key] = typeof value === 'object' && !(value instanceof Date) ? JSON.stringify(value) : toValue(value)
385
611
  }
386
612
  return result
387
613
  }
@@ -404,36 +630,45 @@ function collectHeadersCaseInsensitive(...sources: Array<unknown>): Record<strin
404
630
  return result
405
631
  }
406
632
 
407
- function getHeader(headers: Record<string, string>, name: string): string | undefined {
633
+ /**
634
+ * The value of a header, matched by name in any letter case.
635
+ */
636
+ export function getHeader(headers: Record<string, string>, name: string): string | undefined {
408
637
  const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
409
638
  return key ? headers[key] : undefined
410
639
  }
411
640
 
641
+ function deleteHeader(headers: Record<string, string>, name: string): void {
642
+ for (const key of Object.keys(headers)) {
643
+ if (key.toLowerCase() === name.toLowerCase()) delete headers[key]
644
+ }
645
+ }
646
+
412
647
  function serializeUrl({
413
648
  parts,
414
649
  pathParams,
415
- search,
416
- pathSerializer = defaultPathSerializer,
417
- pathStyles,
650
+ query,
651
+ serializers,
652
+ styles,
418
653
  }: {
419
654
  parts: Array<string | undefined>
420
655
  pathParams: Record<string, unknown>
421
- search: string
422
- pathSerializer?: PathSerializer
423
- pathStyles?: Record<string, PathParamStyle>
656
+ query: Record<string, unknown>
657
+ serializers: Serializers
658
+ styles?: Styles | undefined
424
659
  }): string {
425
660
  const path = parts
426
- .filter(Boolean)
427
- .join('')
428
- .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
661
+ // A base URL that ends in `/` would give `//` before a path that starts with one.
662
+ .reduce<string>((url, part) => (part ? url + (url.endsWith('/') && part.startsWith('/') ? part.slice(1) : part) : url), '')
663
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => requireSerializer(serializers, 'path')({ name: key, value: pathParams[key], options: styles?.path?.[key] }))
664
+ // A query without values needs no serializer.
665
+ const hasQuery = Object.values(query).some((value) => value !== undefined && value !== null)
666
+ const search = serializers.query || hasQuery ? requireSerializer(serializers, 'query')(query, styles?.query) : ''
429
667
  return path + (search ? `?${search}` : '')
430
668
  }
431
669
 
432
- /**
433
- * Creates a transport-agnostic interceptor channel that runs interceptors in registration order.
434
- */
435
- export function createInterceptorStack<T, TContext = never>(): InterceptorStack<T, TContext> {
436
- let entries: Array<{ id: number; fn: InterceptorFn<T, TContext> }> = []
670
+ export function createInterceptorStack<T, TContext = never, TResult = T>(): InterceptorStack<T, TContext, TResult> {
671
+ let entries: Array<{ id: number; fn: InterceptorFn<T, TContext, TResult> }> = []
437
672
  let counter = 0
438
673
  return {
439
674
  use(fn) {
@@ -451,18 +686,38 @@ export function createInterceptorStack<T, TContext = never>(): InterceptorStack<
451
686
  async run(value, context) {
452
687
  let result = value
453
688
  for (const entry of entries) {
454
- result = await entry.fn(result, context)
689
+ const next = await entry.fn(result, context)
690
+ if (next !== undefined) result = next as T
455
691
  }
456
692
  return result
457
693
  },
458
694
  }
459
695
  }
460
696
 
697
+ function placeToken({ scheme, token, headers, query }: { scheme: Auth; token: string; headers: Record<string, string>; query: Record<string, unknown> }): void {
698
+ if (scheme.type !== 'apiKey') {
699
+ if (getHeader(headers, 'Authorization') === undefined) {
700
+ headers['Authorization'] = scheme.scheme === 'basic' ? `Basic ${btoa(String.fromCharCode(...new TextEncoder().encode(token)))}` : `Bearer ${token}`
701
+ }
702
+ return
703
+ }
704
+ const name = scheme.name ?? 'Authorization'
705
+ if (scheme.in === 'query') {
706
+ if (query[name] === undefined) query[name] = token
707
+ return
708
+ }
709
+ if (scheme.in === 'cookie') {
710
+ appendCookie(headers, `${name}=${token}`)
711
+ return
712
+ }
713
+ if (getHeader(headers, name) === undefined) headers[name] = token
714
+ }
715
+
461
716
  /**
462
- * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
717
+ * Adds the tokens of the first security requirement whose schemes all get one to `headers` and `query`.
463
718
  */
464
719
  export async function resolveAuth(params: {
465
- security: Array<Auth> | undefined
720
+ security: Array<SecurityRequirement> | undefined
466
721
  auth: AuthResolver | undefined
467
722
  headers: Record<string, string>
468
723
  query: Record<string, unknown>
@@ -470,94 +725,117 @@ export async function resolveAuth(params: {
470
725
  const { security, auth, headers, query } = params
471
726
  if (!security?.length || auth === undefined) return
472
727
 
473
- for (const scheme of security) {
474
- const token = typeof auth === 'function' ? await auth(scheme) : auth
475
- if (token === undefined) continue
476
-
477
- if (scheme.type !== 'apiKey') {
478
- if (getHeader(headers, 'Authorization') === undefined) {
479
- headers['Authorization'] = scheme.scheme === 'basic' ? `Basic ${btoa(String.fromCharCode(...new TextEncoder().encode(token)))}` : `Bearer ${token}`
480
- }
481
- return
482
- }
483
-
484
- const name = scheme.name ?? 'Authorization'
485
- if (scheme.in === 'query') {
486
- if (query[name] === undefined) query[name] = token
487
- return
488
- }
489
- if (scheme.in === 'cookie') {
490
- headers['Cookie'] = [headers['Cookie'], `${name}=${token}`].filter(Boolean).join('; ')
491
- return
492
- }
493
- if (getHeader(headers, name) === undefined) headers[name] = token
728
+ for (const requirement of security) {
729
+ const schemes = Array.isArray(requirement) ? requirement : [requirement]
730
+ const tokens = await Promise.all(schemes.map((scheme) => (typeof auth === 'function' ? auth(scheme) : auth)))
731
+ if (tokens.some((token) => token === undefined)) continue
732
+ schemes.forEach((scheme, index) => placeToken({ scheme, token: tokens[index]!, headers, query }))
494
733
  return
495
734
  }
496
735
  }
497
736
 
498
- /** A body a transport left as a stream: a web `ReadableStream` or an async iterable such as a Node readable. */
499
- export function isStreamed(data: unknown): data is ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string> {
500
- return data instanceof ReadableStream || (!!data && typeof data === 'object' && Symbol.asyncIterator in data)
737
+ function appendCookie(headers: Record<string, string>, cookie: string): void {
738
+ const key = Object.keys(headers).find((name) => name.toLowerCase() === 'cookie') ?? 'Cookie'
739
+ headers[key] = [headers[key], cookie].filter(Boolean).join('; ')
501
740
  }
502
741
 
503
742
  /**
504
- * Picks the parser for a response. A string `responseType` wins. A map is matched against the media type: the exact
505
- * type, then a wildcard such as `audio/*`, then a catch-all entry. Otherwise the parser is guessed from the media type.
743
+ * The `RequestInit` of a resolved request, for `fetch` or a library that takes the same init, such as ky.
744
+ *
745
+ * @example
746
+ * ```ts
747
+ * client.setConfig({ transport: (request) => ky(request.url, { ...toRequestInit(request), throwHttpErrors: false, retry: 0, timeout: false }) })
748
+ * ```
506
749
  */
507
- export function resolveResponseType(
508
- responseType: ResponseType | ResponseTypeMap | undefined,
509
- contentType: string | null | undefined,
510
- ): ResponseType | undefined {
511
- if (typeof responseType === 'string') return responseType
512
- const base = baseContentType(contentType)
513
- if (!base) return undefined
514
- const mapped = responseType?.[base] ?? responseType?.[`${base.split('/')[0]}/*`] ?? responseType?.['*/*']
515
- if (mapped) return mapped
516
- if (base === 'text/event-stream') return 'stream'
517
- if (isJsonMediaType(base)) return 'json'
518
- if (base.startsWith('text/')) return 'text'
519
- if (base === 'application/octet-stream' || base === 'application/pdf' || /^(image|audio|video)\//.test(base)) return 'blob'
520
- return undefined
750
+ export function toRequestInit(request: ResolvedRequest): RequestInit {
751
+ const init: RequestInit = {
752
+ ...request.options,
753
+ method: request.method,
754
+ headers: request.headers,
755
+ body: request.body ?? null,
756
+ signal: request.signal ?? null,
757
+ }
758
+ if (request.credentials) init.credentials = request.credentials
759
+ return init
760
+ }
761
+
762
+ function pickErrorValidator(error: Validator | Record<string, Validator> | undefined, status: number): Validator | undefined {
763
+ if (!error || '~standard' in error) return error as Validator | undefined
764
+ const range = String(status)[0]
765
+ return error[String(status)] ?? error[`${range}xx`] ?? error['default']
766
+ }
767
+
768
+ // Checks the shape, so a `Response` from another realm or a polyfill counts too.
769
+ function isResponse(value: unknown): value is Response {
770
+ return typeof (value as Response | undefined)?.arrayBuffer === 'function'
771
+ }
772
+
773
+ // Leaves out the body, which was already sent. A relative URL keeps the resolved request.
774
+ function toNativeRequest(request: ResolvedRequest): Request | ResolvedRequest {
775
+ try {
776
+ return new Request(request.url, { ...toRequestInit(request), body: null })
777
+ } catch {
778
+ return request
779
+ }
780
+ }
781
+
782
+ async function readAs(response: Response, parseAs: Exclude<ParseAs, 'json'>): Promise<unknown> {
783
+ switch (parseAs) {
784
+ case 'text':
785
+ return response.text()
786
+ case 'blob':
787
+ return response.blob()
788
+ case 'arrayBuffer':
789
+ return response.arrayBuffer()
790
+ case 'stream':
791
+ return response.body ?? undefined
792
+ }
521
793
  }
522
794
 
523
- /** The media type of the `Content-Type` response header, without its parameters. */
524
- export function getResponseContentType(headers: Headers | Record<string, string> | undefined): string | undefined {
525
- if (!headers) return undefined
526
- const value = headers instanceof Headers ? headers.get('Content-Type') : (headers['Content-Type'] ?? headers['content-type'])
527
- return baseContentType(value)
795
+ function isEmpty(response: Response, method: string): boolean {
796
+ return [204, 205, 304].includes(response.status) || method === 'HEAD' || !response.body || response.headers.get('Content-Length') === '0'
528
797
  }
529
798
 
530
- function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
799
+ function mergeConfig<T extends { headers?: HeadersInit; mediaTypes?: MediaTypes; serializer?: Serializers }>(config: T, next: T): T {
531
800
  return {
532
- querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
533
- bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
534
- pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
801
+ ...config,
802
+ ...next,
803
+ headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) },
804
+ mediaTypes: { ...config.mediaTypes, ...next.mediaTypes },
805
+ serializer: { ...config.serializer, ...next.serializer },
535
806
  }
536
807
  }
537
808
 
809
+ // The serializer for `name`, which `.kubb/client.ts` registers for a spec that needs it.
810
+ function requireSerializer<TName extends keyof Serializers>(serializers: Serializers, name: TName): NonNullable<Serializers[TName]> {
811
+ const serializer = serializers[name]
812
+ if (!serializer) throw new TypeError(`Cannot send ${name} parameters without a serializer, register serializer.${name} on the client.`)
813
+ return serializer
814
+ }
815
+
538
816
  async function resolveRequest<TBody, TRequest, TResponse>({
539
817
  config,
540
818
  requestConfig,
541
819
  }: {
542
820
  config: ClientConfig<TRequest, TResponse>
543
821
  requestConfig: RequestConfig<TBody, TRequest, TResponse>
544
- }): Promise<{ request: ResolvedRequest; codecs: Record<string, Codec>; validate: ValidateOption | undefined }> {
545
- const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
546
- const codecs = { ...config.codecs, ...requestConfig.codecs }
822
+ }): Promise<{ request: ResolvedRequest; mediaTypes: MediaTypes; validate: ValidateOption | undefined; responseContentType: string | undefined }> {
823
+ const serializers = { ...config.serializer, ...requestConfig.serializer }
824
+ const mediaTypes = { ...defaultMediaTypes, ...config.mediaTypes, ...requestConfig.mediaTypes }
547
825
  const onValidationError = requestConfig.onValidationError ?? config.onValidationError
548
826
  const validate = requestConfig.validate ?? config.validate
549
827
  const validationContext = { direction: 'request', method: requestConfig.method, url: requestConfig.url } as const
550
828
 
551
- // Params are encoded before anything reads them: headers before styling, query before auth adds its values.
552
829
  const paramValues = {
553
830
  path: { ...(requestConfig.path as Record<string, unknown> | undefined) },
554
831
  query: { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) },
555
832
  headers: collectHeadersCaseInsensitive(config.headers, requestConfig.headers),
833
+ cookies: requestConfig.cookies as Record<string, unknown> | undefined,
556
834
  }
557
- const [validatedPath, validatedQuery, validatedHeaders] = await Promise.all(
558
- (['path', 'query', 'headers'] as const).map((source) =>
835
+ const [validatedPath, validatedQuery, validatedHeaders, validatedCookies] = await Promise.all(
836
+ (['path', 'query', 'headers', 'cookies'] as const).map((source) =>
559
837
  runValidator({
560
- validator: isValidationEnabled(validate, 'params') ? requestConfig.validator?.[source] : undefined,
838
+ validator: isValidationEnabled(validate, 'params') && paramValues[source] !== undefined ? requestConfig.validator?.[source] : undefined,
561
839
  value: paramValues[source],
562
840
  context: { ...validationContext, source },
563
841
  onValidationError,
@@ -565,10 +843,10 @@ async function resolveRequest<TBody, TRequest, TResponse>({
565
843
  ),
566
844
  )
567
845
 
568
- const headers = serializeHeaders(applyHeaderStyles(validatedHeaders as HeadersInit | undefined, requestConfig.styles?.header))
846
+ const headerValues = validatedHeaders as HeadersInit | undefined
847
+ const headers = serializeHeaders(serializers.header ? serializers.header(headerValues, requestConfig.styles?.header) : headerValues)
569
848
  const contentType = requestConfig.contentType
570
849
  const { request: requestContentTypeOption, response: responseContentType } = typeof contentType === 'string' ? { request: contentType } : (contentType ?? {})
571
- const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
572
850
  if (responseContentType && getHeader(headers, 'accept') === undefined) {
573
851
  headers['Accept'] = responseContentType
574
852
  }
@@ -582,9 +860,11 @@ async function resolveRequest<TBody, TRequest, TResponse>({
582
860
  query,
583
861
  })
584
862
 
585
- if (requestConfig.cookies) {
586
- const cookie = serializeCookies(requestConfig.cookies as Record<string, unknown>, requestConfig.styles?.cookie)
587
- if (cookie) headers['Cookie'] = [headers['Cookie'], cookie].filter(Boolean).join('; ')
863
+ const cookies = validatedCookies as Record<string, unknown> | undefined
864
+ // An object with only nullish values sends nothing, so it needs no serializer.
865
+ if (cookies && Object.values(cookies).some((value) => value !== undefined && value !== null)) {
866
+ const cookie = requireSerializer(serializers, 'cookie')(cookies, requestConfig.styles?.cookie)
867
+ if (cookie) appendCookie(headers, cookie)
588
868
  }
589
869
 
590
870
  const validatedBody = await runValidator({
@@ -593,129 +873,298 @@ async function resolveRequest<TBody, TRequest, TResponse>({
593
873
  context: { ...validationContext, source: 'body' },
594
874
  onValidationError,
595
875
  })
596
- const requestContentTypeBase = baseContentType(requestContentType)
597
- const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
598
- const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
599
- const body = contentCodec?.serialize
600
- ? contentCodec.serialize(validatedBody, requestContentType)
601
- : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
602
- if (requestContentTypeOption) headers['Content-Type'] = requestContentTypeOption
603
- if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && getHeader(headers, 'content-type') === undefined) {
604
- headers['Content-Type'] = 'application/json'
605
- }
606
- // The transport must set the multipart boundary, even when a content type was explicitly provided.
607
- if (body instanceof FormData) {
608
- for (const key of Object.keys(headers)) {
609
- if (key.toLowerCase() === 'content-type') delete headers[key]
610
- }
876
+ const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type') ?? (isEncodedBody(validatedBody) ? undefined : 'application/json')
877
+ const body =
878
+ // `null` is a valid JSON body, such as a merge patch that clears a resource.
879
+ validatedBody === undefined
880
+ ? undefined
881
+ : resolveMediaType({ mediaTypes, contentType: requestContentType, use: 'encode' })?.encode?.(validatedBody, {
882
+ contentType: requestContentType ?? '',
883
+ signal: requestConfig.signal,
884
+ config: requestConfig,
885
+ })
886
+ if (body !== undefined && requestContentType && getHeader(headers, 'content-type') !== requestContentType) {
887
+ deleteHeader(headers, 'content-type')
888
+ headers['Content-Type'] = requestContentType
611
889
  }
890
+ // The transport sets the multipart boundary.
891
+ if (body instanceof FormData) deleteHeader(headers, 'content-type')
612
892
 
613
893
  const url = serializeUrl({
614
894
  parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
615
895
  pathParams: (validatedPath ?? {}) as Record<string, unknown>,
616
- search: querySerializer(query, requestConfig.styles?.query),
617
- pathSerializer,
618
- pathStyles: requestConfig.styles?.path,
896
+ query,
897
+ serializers,
898
+ styles: requestConfig.styles,
619
899
  })
620
900
 
621
901
  const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
902
+ const accept = getHeader(headers, 'accept')
622
903
 
623
904
  return {
624
- codecs,
905
+ mediaTypes,
625
906
  validate,
907
+ responseContentType,
626
908
  request: {
627
909
  url,
628
910
  method: (requestConfig.method ?? 'GET').toUpperCase(),
629
911
  headers,
630
912
  body,
631
- signal: requestConfig.signal,
913
+ signal: signalOf({ signal: requestConfig.signal, options }),
632
914
  credentials: requestConfig.credentials ?? config.credentials,
633
915
  options,
634
- responseType: requestConfig.responseType,
916
+ stream:
917
+ requestConfig.parseAs === 'stream' || Boolean(accept?.split(',').some((type) => resolveMediaType({ mediaTypes, contentType: type, use: 'stream' }))),
635
918
  },
636
919
  }
637
920
  }
638
921
 
922
+ // A signal in `options` counts too, since every transport spreads `options` under the resolved fields.
923
+ function signalOf(config: { signal?: AbortSignal | undefined; options?: unknown }): AbortSignal | undefined {
924
+ return config.signal ?? (config.options as { signal?: AbortSignal } | undefined)?.signal
925
+ }
926
+
927
+ async function* readItems({
928
+ items,
929
+ validateItem,
930
+ signal,
931
+ }: {
932
+ items: AsyncIterable<unknown>
933
+ validateItem: (item: unknown) => unknown
934
+ signal: AbortSignal | undefined
935
+ }): AsyncGenerator<unknown> {
936
+ try {
937
+ for await (const item of items) {
938
+ yield await validateItem(item)
939
+ if (signal?.aborted) throw signal.reason
940
+ }
941
+ } catch (error) {
942
+ // A transport reports an abort in its own error type, such as axios's `CanceledError`.
943
+ throw signal?.aborted ? signal.reason : error
944
+ }
945
+ }
946
+
947
+ type InterceptorRuns<TRequest, TResponse> = {
948
+ request: (value: ResolvedRequest, context?: RequestConfig<unknown, TRequest, TResponse>) => Promise<ResolvedRequest>
949
+ response: (
950
+ value: DecodedResponse<unknown, TRequest, TResponse>,
951
+ context?: RequestConfig<unknown, TRequest, TResponse>,
952
+ ) => Promise<DecodedResponse<unknown, TRequest, TResponse>>
953
+ error: (value: Error, context?: RequestConfig<unknown, TRequest, TResponse>) => Promise<Error>
954
+ }
955
+
639
956
  /**
640
- * Creates an isolated client using the supplied default transport.
957
+ * Creates a client that sends through `defaultTransport`.
641
958
  */
642
959
  export function createClientCore<TRequest = NativeRequest, TResponse = NativeResponse>(
643
- options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
960
+ options: { defaultTransport: Transport<TRequest, TResponse>; parent?: InterceptorRuns<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
644
961
  ): ClientInstance<TRequest, TResponse> {
645
- const { defaultTransport, ...initialConfig } = options
962
+ const { defaultTransport, parent, ...initialConfig } = options
646
963
  let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
647
964
 
648
- const interceptors: Interceptors<TRequest, TResponse> = {
649
- request: createInterceptorStack<ResolvedRequest, RequestConfig<unknown, TRequest, TResponse>>(),
650
- response: createInterceptorStack<TransportResult<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>(),
651
- error: createInterceptorStack<ResponseError<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>(),
965
+ const own: Interceptors<TRequest, TResponse> = {
966
+ request: createInterceptorStack(),
967
+ response: createInterceptorStack(),
968
+ error: createInterceptorStack(),
969
+ }
970
+ const run: InterceptorRuns<TRequest, TResponse> = {
971
+ request: async (value, context) => own.request.run(parent ? await parent.request(value, context) : value, context),
972
+ response: async (value, context) => own.response.run(parent ? await parent.response(value, context) : value, context),
973
+ error: async (value, context) => own.error.run(parent ? await parent.error(value, context) : value, context),
652
974
  }
653
975
 
654
- const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
976
+ async function send(requestConfig: RequestConfig<unknown, TRequest, TResponse> & EventStreamOptions, streaming: boolean) {
655
977
  const transport = requestConfig.transport ?? config.transport ?? defaultTransport
656
- const { request, codecs, validate } = await resolveRequest({ config, requestConfig })
978
+ // Read before the first await, so a `setConfig` during the call does not change it.
979
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
980
+ const onValidationError = requestConfig.onValidationError ?? config.onValidationError
981
+ const { request, mediaTypes, validate, responseContentType } = await resolveRequest({ config, requestConfig })
982
+ if (streaming) {
983
+ request.stream = true
984
+ // `client.stream` without a response content type would otherwise ask for the default, often JSON.
985
+ if (getHeader(request.headers, 'accept') === undefined) request.headers['Accept'] = 'text/event-stream'
986
+ }
987
+
988
+ const resolvedRequest = await run.request(request, requestConfig)
989
+ const sent = await transport(resolvedRequest)
990
+ const {
991
+ webResponse,
992
+ request: nativeRequest,
993
+ response: nativeResponse,
994
+ } = isResponse(sent) ? { webResponse: sent, request: toNativeRequest(resolvedRequest) as TRequest, response: sent as TResponse & Response } : sent
995
+
996
+ const status = webResponse.status
997
+ const isSuccess = status >= 200 && status < 300
998
+ const header = webResponse.headers.get('Content-Type')
999
+ const contentType = header ?? (isSuccess ? responseContentType : undefined) ?? '*/*'
1000
+ const cursor = { lastEventId: '' }
1001
+ const context: MediaTypeContext = { contentType, status, signal: resolvedRequest.signal, config: requestConfig, cursor }
1002
+ const streamType = isSuccess ? resolveMediaType({ mediaTypes, contentType, use: 'stream' }) : undefined
1003
+ const decode = async (): Promise<unknown> => {
1004
+ if (isEmpty(webResponse, resolvedRequest.method)) return undefined
1005
+ if (streamType) return webResponse.body
1006
+ const parseAs = isSuccess ? requestConfig.parseAs : undefined
1007
+ if (parseAs && parseAs !== 'json') return readAs(webResponse, parseAs)
1008
+ // `parseAs: 'json'` uses the registered JSON handler, so a lenient one applies.
1009
+ return resolveMediaType({ mediaTypes, contentType: parseAs === 'json' ? 'application/json' : contentType, use: 'decode' })?.decode?.(webResponse, context)
1010
+ }
657
1011
 
658
- const resolvedRequest = await interceptors.request.run(request, requestConfig)
659
- const result = await interceptors.response.run(await transport(resolvedRequest), requestConfig)
1012
+ const result = await run.response(
1013
+ {
1014
+ data: await decode(),
1015
+ status,
1016
+ statusText: webResponse.statusText,
1017
+ headers: webResponse.headers,
1018
+ contentType: baseContentType(header),
1019
+ request: nativeRequest,
1020
+ response: nativeResponse,
1021
+ },
1022
+ requestConfig,
1023
+ )
660
1024
 
661
- const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
662
1025
  const validator = requestConfig.validator
663
- const onValidationError = requestConfig.onValidationError ?? config.onValidationError
664
- const isSuccess = result.status >= 200 && result.status < 300
665
- const direction = isSuccess ? 'response' : 'error'
666
- const contentType = result.contentType ?? getResponseContentType(result.headers)
667
- const codec = contentType ? codecs[contentType] : undefined
668
- const decoded = contentType && codec?.deserialize ? await codec.deserialize(result.data, contentType) : result.data
669
- // A streamed success body is validated event by event in `toEventStream`, not as a whole. A JSON answer to a streamed call is a whole body.
670
- const streaming = isSuccess && resolveResponseType(resolvedRequest.responseType, contentType) === 'stream' && isStreamed(decoded)
671
- const data = await runValidator({
672
- validator: isValidationEnabled(validate, direction) && !streaming ? validator?.[direction] : undefined,
673
- value: decoded,
674
- context: { direction, source: 'body', method: resolvedRequest.method, url: resolvedRequest.url, status: result.status },
675
- onValidationError,
676
- })
677
- if (!isSuccess && throwOnError) {
678
- const responseError = new ResponseError({
1026
+ const resultSuccess = result.status >= 200 && result.status < 300
1027
+ const direction = resultSuccess ? 'response' : 'error'
1028
+ const validationContext = { direction, source: 'body', method: resolvedRequest.method, url: resolvedRequest.url, status: result.status } as const
1029
+ const validatorFor = isValidationEnabled(validate, direction)
1030
+ ? resultSuccess
1031
+ ? validator?.response
1032
+ : pickErrorValidator(validator?.error, result.status)
1033
+ : undefined
1034
+ const rawStream = result.data instanceof ReadableStream
1035
+ const readsItems = streaming && resultSuccess && streamType?.stream !== undefined && rawStream
1036
+
1037
+ if (streaming && resultSuccess && !readsItems && result.data != null) {
1038
+ // Release the connection, since nobody reads this body.
1039
+ if (rawStream) await (result.data as ReadableStream).cancel().catch(() => {})
1040
+ // `cause` is set afterwards, since the constructor option needs the ES2022 library.
1041
+ const error: TypeError & { cause?: unknown } = new TypeError(`Expected a streamed body, got ${result.contentType ?? 'a body without a content type'}`)
1042
+ error.cause = result.data
1043
+ throw error
1044
+ }
1045
+
1046
+ const data = rawStream ? result.data : await runValidator({ validator: validatorFor, value: result.data, context: validationContext, onValidationError })
1047
+ if (!resultSuccess && throwOnError) {
1048
+ throw new ResponseError({
679
1049
  data,
680
1050
  status: result.status,
681
1051
  statusText: result.statusText,
682
- contentType,
1052
+ contentType: result.contentType,
1053
+ headers: result.headers,
683
1054
  request: result.request,
684
1055
  response: result.response,
685
1056
  method: resolvedRequest.method,
686
1057
  url: resolvedRequest.url,
687
1058
  })
688
- await interceptors.error.run(responseError, requestConfig)
689
- throw responseError
690
1059
  }
1060
+
1061
+ const validateItem = async (item: unknown) => {
1062
+ if (!validatorFor) return item
1063
+ const validate = (value: unknown) => runValidator({ validator: validatorFor, value, context: validationContext, onValidationError })
1064
+ if (requestConfig.eventShape === 'data') {
1065
+ const event = item as ServerSentEvent
1066
+ return { ...event, data: await validate(event.data) }
1067
+ }
1068
+ // Merged over the event, since the schema may not declare `id` and `retry`.
1069
+ if (requestConfig.eventShape === 'event') return { ...(item as object), ...((await validate(item)) as object) }
1070
+ return validate(item)
1071
+ }
1072
+ const items = readsItems && streamType?.stream && result.data instanceof ReadableStream ? streamType.stream(result.data, context) : undefined
691
1073
  return {
692
- status: result.status,
693
- data: isSuccess ? data : undefined,
694
- error: isSuccess ? undefined : data,
695
- contentType,
696
- request: result.request,
697
- response: result.response,
1074
+ cursor,
1075
+ items: readItems({ items: items ?? emptyItems(), validateItem, signal: resolvedRequest.signal }),
1076
+ result: {
1077
+ status: result.status,
1078
+ data: resultSuccess ? data : undefined,
1079
+ error: resultSuccess
1080
+ ? undefined
1081
+ : data == null || data === ''
1082
+ ? ({ status: result.status, statusText: result.statusText } satisfies EmptyErrorBody)
1083
+ : data,
1084
+ contentType: result.contentType,
1085
+ headers: result.headers,
1086
+ request: result.request,
1087
+ response: result.response,
1088
+ },
698
1089
  }
699
- }) as ClientInstance<TRequest, TResponse>
1090
+ }
1091
+
1092
+ async function call<T>(requestConfig: RequestConfig<unknown, TRequest, TResponse>, work: () => Promise<T>): Promise<T> {
1093
+ const signal = signalOf(requestConfig)
1094
+ try {
1095
+ return await work()
1096
+ } catch (caught) {
1097
+ const error: unknown = signal?.aborted ? signal.reason : caught
1098
+ throw error instanceof Error ? await run.error(error, requestConfig) : error
1099
+ }
1100
+ }
1101
+
1102
+ const client = (<TResponses, ThrowOnError extends boolean, THeaders>(requestConfig: RequestConfig<unknown, TRequest, TResponse>) =>
1103
+ call(
1104
+ requestConfig,
1105
+ async () => (await send(requestConfig, false)).result as RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>,
1106
+ )) as ClientInstance<TRequest, TResponse>
1107
+
1108
+ client.stream = <TResponses, ThrowOnError extends boolean, TItem, THeaders>(
1109
+ requestConfig: RequestConfig<unknown, TRequest, TResponse> & EventStreamOptions,
1110
+ ) =>
1111
+ call(requestConfig, async () => {
1112
+ const { result, items, cursor } = await send(requestConfig, true)
1113
+ const stream = items as AsyncGenerator<TItem>
1114
+ const streamed = {
1115
+ ...result,
1116
+ stream,
1117
+ data: result.error === undefined ? stream : undefined,
1118
+ get lastEventId() {
1119
+ return cursor.lastEventId
1120
+ },
1121
+ [Symbol.asyncIterator]: () => stream,
1122
+ }
1123
+ return streamed as unknown as StreamResult<TResponses, ThrowOnError, TItem, TRequest, TResponse, THeaders>
1124
+ })
700
1125
 
701
1126
  client.getConfig = () => config
702
1127
  client.setConfig = (next) => {
703
- config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
1128
+ config = mergeConfig(config, next)
704
1129
  return config
705
1130
  }
706
1131
  client.getUrl = (requestConfig) => {
707
- const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
708
1132
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
709
1133
  return serializeUrl({
710
1134
  parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
711
1135
  pathParams: (requestConfig.path ?? {}) as Record<string, unknown>,
712
- search: querySerializer(query, requestConfig.styles?.query),
713
- pathSerializer,
714
- pathStyles: requestConfig.styles?.path,
1136
+ query,
1137
+ serializers: { ...config.serializer, ...requestConfig.serializer },
1138
+ styles: requestConfig.styles,
715
1139
  })
716
1140
  }
717
- client.interceptors = interceptors
718
- client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
1141
+ client.interceptors = own
1142
+ client.createClient = (next = {}) => createClientCore({ defaultTransport, ...mergeConfig(config, next) })
1143
+ client.extend = (next = {}) => createClientCore({ defaultTransport, parent: run, ...mergeConfig(config, next) })
719
1144
 
720
1145
  return client
721
1146
  }
1147
+
1148
+ async function* emptyItems(): AsyncGenerator<unknown> {}
1149
+
1150
+ /**
1151
+ * A client created on first use, so importing an operation runs no setup. Config set before that is passed to `create`.
1152
+ */
1153
+ export function createLazyClient<TRequest = NativeRequest, TResponse = NativeResponse>(
1154
+ create: (config: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>,
1155
+ ): ClientInstance<TRequest, TResponse> {
1156
+ let instance: ClientInstance<TRequest, TResponse> | undefined
1157
+ let pending: ClientConfig<TRequest, TResponse> = {}
1158
+ const get = () => (instance ??= create(pending))
1159
+
1160
+ const client = (<TResponses, ThrowOnError extends boolean>(requestConfig: RequestConfig<unknown, TRequest, TResponse> & { throwOnError?: ThrowOnError }) =>
1161
+ get()<TResponses, ThrowOnError>(requestConfig)) as ClientInstance<TRequest, TResponse>
1162
+ client.stream = (requestConfig) => get().stream(requestConfig)
1163
+ client.getConfig = () => instance?.getConfig() ?? pending
1164
+ client.setConfig = (next) => (instance ? instance.setConfig(next) : (pending = mergeConfig(pending, next)))
1165
+ client.getUrl = (requestConfig) => get().getUrl(requestConfig)
1166
+ client.createClient = (next) => get().createClient(next)
1167
+ client.extend = (next) => get().extend(next)
1168
+ Object.defineProperty(client, 'interceptors', { get: () => get().interceptors, enumerable: true })
1169
+ return client
1170
+ }