@kubb/plugin-fetch 5.0.0-beta.100

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,818 @@
1
+ import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers'
2
+ import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
3
+ import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
4
+
5
+ /**
6
+ * HTTP status codes treated as a success, everything else is an error.
7
+ */
8
+ export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
9
+
10
+ /**
11
+ * The success members of a per-status responses record.
12
+ */
13
+ export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
14
+
15
+ /**
16
+ * The error members of a per-status responses record, every documented status that is not a 2xx.
17
+ */
18
+ export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
19
+
20
+ /**
21
+ * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
22
+ */
23
+ export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
24
+
25
+ /**
26
+ * The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`.
27
+ */
28
+ export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
29
+
30
+ /**
31
+ * The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it.
32
+ */
33
+ export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
34
+ ? TEntry extends { contentType: infer TContentType; data: infer TData }
35
+ ? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
36
+ : never
37
+ : { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
38
+
39
+ /**
40
+ * One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`.
41
+ */
42
+ export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
43
+ ? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
44
+ : {
45
+ status: ToStatusNumber<TStatus>
46
+ data: undefined
47
+ error: DataOf<TResponses[TStatus]>
48
+ contentType: string | undefined
49
+ request: TRequest
50
+ response: TResponse
51
+ }
52
+
53
+ /**
54
+ * The union of every documented status' result variant.
55
+ */
56
+ export type ResultUnion<TResponses, TRequest, TResponse> = {
57
+ [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
58
+ }[keyof TResponses]
59
+
60
+ /**
61
+ * The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`.
62
+ */
63
+ export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
64
+ [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
65
+ }[Extract<keyof TResponses, SuccessStatusCode>]
66
+
67
+ /**
68
+ * The shape every generated function returns, discriminated by the top-level `status`, narrowing to the 2xx variants under `throwOnError` and to every documented status without it.
69
+ */
70
+ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
71
+ ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
72
+ ? {
73
+ status: number
74
+ data: SuccessOf<TResponses>
75
+ error: undefined
76
+ contentType: string | undefined
77
+ request: TRequest
78
+ response: TResponse
79
+ }
80
+ : SuccessResultUnion<TResponses, TRequest, TResponse>
81
+ : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
82
+ ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
83
+ : ResultUnion<TResponses, TRequest, TResponse>
84
+
85
+ /**
86
+ * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
87
+ */
88
+ export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
89
+
90
+ export type RequestCredentials = 'omit' | 'same-origin' | 'include'
91
+ export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
92
+
93
+ /**
94
+ * Turns a raw response body into a parsed value, registered per media type as a codec's `deserialize` to handle formats the runtime does not decode itself.
95
+ */
96
+ export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
97
+
98
+ /**
99
+ * Serializes a request body for a single media type, registered per content type as a codec's `serialize` to encode formats the default serializer does not handle.
100
+ */
101
+ export type ContentBodySerializer = (body: unknown, contentType?: string) => BodyInit | undefined
102
+
103
+ /**
104
+ * A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the
105
+ * request body for that media type and `deserialize` decodes the response body. Either half is
106
+ * optional, so a codec can handle one direction.
107
+ */
108
+ export type Codec = {
109
+ serialize?: ContentBodySerializer
110
+ deserialize?: Deserializer
111
+ }
112
+
113
+ /**
114
+ * The per-call content type selection, where a bare string sets the request content type and the object form also sets the response format sent as `Accept`.
115
+ */
116
+ export type ContentType = string | { request?: string; response?: string }
117
+
118
+ /**
119
+ * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
120
+ * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
121
+ * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
122
+ * non-2xx call does not throw).
123
+ */
124
+ export type Validator<T = unknown> = StandardSchemaValidator<T>
125
+
126
+ /**
127
+ * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
128
+ */
129
+ export type Auth = {
130
+ type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
131
+ scheme?: 'bearer' | 'basic'
132
+ name?: string
133
+ in?: 'header' | 'query' | 'cookie'
134
+ }
135
+
136
+ /**
137
+ * The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
138
+ */
139
+ export type AuthToken = string | undefined
140
+
141
+ /**
142
+ * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
143
+ */
144
+ export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
145
+
146
+ /**
147
+ * Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for fields the runtime does not set itself such as `cache`, `redirect`, and Next.js's `next`.
148
+ */
149
+ export type FetchOptions = RequestInit & { next?: Record<string, unknown> }
150
+
151
+ /**
152
+ * The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
153
+ */
154
+ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Response> = {
155
+ baseURL?: string
156
+ url?: string
157
+ method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
158
+ path?: Record<string, unknown>
159
+ query?: unknown
160
+ params?: unknown
161
+ cookies?: Record<string, unknown>
162
+ body?: TBody
163
+ headers?: HeadersInit
164
+ styles?: Styles
165
+ signal?: AbortSignal
166
+ credentials?: RequestCredentials
167
+ options?: FetchOptions
168
+ contentType?: ContentType
169
+ responseType?: ResponseType
170
+ throwOnError?: boolean
171
+ client?: ClientInstance<TRequest, TResponse>
172
+ transport?: Transport<TRequest, TResponse>
173
+ serializer?: Serializers
174
+ codecs?: Record<string, Codec>
175
+ validator?: { request?: Validator; response?: Validator; error?: Validator }
176
+ security?: Array<Auth>
177
+ auth?: AuthResolver
178
+ }
179
+
180
+ /**
181
+ * The grouped options object passed to every generated function: the request config minus the
182
+ * data-shaped keys and the literal `url`, plus the per-operation `<Name>Request`.
183
+ */
184
+ export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = Omit<
185
+ RequestConfig<unknown, TRequest, TResponse>,
186
+ keyof DataShape | 'url'
187
+ > &
188
+ TData & {
189
+ client?: ClientInstance<TRequest, TResponse>
190
+ throwOnError?: ThrowOnError
191
+ }
192
+
193
+ /**
194
+ * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
195
+ */
196
+ export type ClientConfig<TRequest = Request, TResponse = Response> = {
197
+ baseURL?: string
198
+ headers?: HeadersInit
199
+ credentials?: RequestCredentials
200
+ options?: FetchOptions
201
+ throwOnError?: boolean
202
+ transport?: Transport<TRequest, TResponse>
203
+ serializer?: Serializers
204
+ codecs?: Record<string, Codec>
205
+ auth?: AuthResolver
206
+ }
207
+
208
+ /**
209
+ * The normalized request the transport receives, with all serialization, auth, and header work already done.
210
+ */
211
+ export type ResolvedRequest = {
212
+ url: string
213
+ method: string
214
+ headers: Record<string, string>
215
+ body?: BodyInit
216
+ signal?: AbortSignal
217
+ credentials?: RequestCredentials
218
+ options?: FetchOptions
219
+ responseType?: ResponseType
220
+ }
221
+
222
+ /**
223
+ * What a transport returns: the parsed body plus the native request and response objects.
224
+ */
225
+ export type TransportResult<TData = unknown, TRequest = Request, TResponse = Response> = {
226
+ data: TData
227
+ status: number
228
+ statusText: string
229
+ headers: Headers
230
+ contentType?: string
231
+ request: TRequest
232
+ response: TResponse
233
+ }
234
+
235
+ /**
236
+ * The per-plugin send, supplied to `createClientCore` as `defaultTransport`.
237
+ */
238
+ export type Transport<TRequest = Request, TResponse = Response> = (request: ResolvedRequest) => Promise<TransportResult<unknown, TRequest, TResponse>>
239
+
240
+ /**
241
+ * The result a resolved call produces before it is cast to `RequestResult` by the generated wrapper.
242
+ */
243
+ export type CallResult<TRequest = Request, TResponse = Response> = {
244
+ status: number
245
+ data: unknown
246
+ error: unknown
247
+ contentType: string | undefined
248
+ request: TRequest
249
+ response: TResponse
250
+ }
251
+
252
+ export type InterceptorFn<T> = (value: T) => T | Promise<T>
253
+
254
+ /**
255
+ * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API.
256
+ */
257
+ export type InterceptorStack<T> = {
258
+ use: (fn: InterceptorFn<T>) => number
259
+ eject: (id: number) => void
260
+ update: (id: number, fn: InterceptorFn<T>) => void
261
+ run: (value: T) => Promise<T>
262
+ }
263
+
264
+ /**
265
+ * The three interceptor channels every client instance exposes.
266
+ */
267
+ export type Interceptors<TRequest = Request, TResponse = Response> = {
268
+ request: InterceptorStack<ResolvedRequest>
269
+ response: InterceptorStack<TransportResult<unknown, TRequest, TResponse>>
270
+ error: InterceptorStack<ResponseError<unknown, TRequest, TResponse>>
271
+ }
272
+
273
+ /**
274
+ * A client instance: the callable send plus configuration, interceptors, and an isolated
275
+ * `createClient` factory bound to the same transport.
276
+ */
277
+ export type ClientInstance<TRequest = Request, TResponse = Response> = {
278
+ <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
279
+ getConfig: () => ClientConfig<TRequest, TResponse>
280
+ setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
281
+ getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string
282
+ interceptors: Interceptors<TRequest, TResponse>
283
+ createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
284
+ }
285
+
286
+ /**
287
+ * Thrown for a non-2xx response, so a resolved call always means success.
288
+ */
289
+ export class ResponseError<TError = unknown, TRequest = Request, TResponse = Response> extends Error {
290
+ data: TError
291
+ status: number
292
+ statusText: string
293
+ contentType: string | undefined
294
+ request: TRequest
295
+ response: TResponse
296
+
297
+ constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) {
298
+ super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
299
+ this.name = 'ResponseError'
300
+ this.data = config.data
301
+ this.status = config.status
302
+ this.statusText = config.statusText
303
+ this.contentType = config.contentType
304
+ this.request = config.request
305
+ this.response = config.response
306
+ }
307
+ }
308
+
309
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
310
+
311
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
312
+ if (!headers) return {}
313
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
314
+ const result: Record<string, string> = {}
315
+ for (const [key, value] of entries) {
316
+ if (value === undefined || value === null) continue
317
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
318
+ }
319
+ return result
320
+ }
321
+
322
+ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string, string> {
323
+ return Object.assign({}, ...sources.map(serializeHeaders))
324
+ }
325
+
326
+ function getHeader(headers: Record<string, string>, name: string): string | undefined {
327
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
328
+ return key ? headers[key] : undefined
329
+ }
330
+
331
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
332
+ return Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase())
333
+ }
334
+
335
+ /**
336
+ * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, shared by the send path and `getUrl`.
337
+ */
338
+ function serializeUrl({
339
+ parts,
340
+ pathParams,
341
+ search,
342
+ pathSerializer = defaultPathSerializer,
343
+ pathStyles,
344
+ }: {
345
+ parts: Array<string | undefined>
346
+ pathParams: Record<string, unknown>
347
+ search: string
348
+ pathSerializer?: PathSerializer
349
+ pathStyles?: Record<string, PathParamStyle>
350
+ }): string {
351
+ const path = parts
352
+ .filter(Boolean)
353
+ .join('')
354
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
355
+ return path + (search ? `?${search}` : '')
356
+ }
357
+
358
+ /**
359
+ * Creates a transport-agnostic interceptor channel that runs interceptors in registration order.
360
+ */
361
+ export function createInterceptorStack<T>(): InterceptorStack<T> {
362
+ let entries: Array<{ id: number; fn: InterceptorFn<T> }> = []
363
+ let counter = 0
364
+ return {
365
+ use(fn) {
366
+ const id = ++counter
367
+ entries.push({ id, fn })
368
+ return id
369
+ },
370
+ eject(id) {
371
+ entries = entries.filter((entry) => entry.id !== id)
372
+ },
373
+ update(id, fn) {
374
+ const entry = entries.find((item) => item.id === id)
375
+ if (entry) entry.fn = fn
376
+ },
377
+ async run(value) {
378
+ let result = value
379
+ for (const entry of entries) {
380
+ result = await entry.fn(result)
381
+ }
382
+ return result
383
+ },
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
389
+ */
390
+ export async function resolveAuth(params: {
391
+ security: Array<Auth> | undefined
392
+ auth: AuthResolver | undefined
393
+ headers: Record<string, string>
394
+ query: Record<string, unknown>
395
+ }): Promise<void> {
396
+ const { security, auth, headers, query } = params
397
+ if (!security?.length || auth === undefined) return
398
+
399
+ for (const scheme of security) {
400
+ const token = typeof auth === 'function' ? await auth(scheme) : auth
401
+ if (token === undefined) continue
402
+
403
+ if (scheme.type === 'apiKey') {
404
+ const name = scheme.name ?? 'Authorization'
405
+ if (scheme.in === 'query') {
406
+ if (query[name] === undefined) query[name] = token
407
+ } else if (scheme.in === 'cookie') {
408
+ headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
409
+ } else if (!hasHeader(headers, name)) {
410
+ headers[name] = token
411
+ }
412
+ } else if (!hasHeader(headers, 'Authorization')) {
413
+ headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
414
+ }
415
+ return
416
+ }
417
+ }
418
+
419
+ async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
420
+ if (!validator) return value
421
+ return validateStandardSchema(validator, value)
422
+ }
423
+
424
+ /**
425
+ * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
426
+ */
427
+ function baseContentType(value: string | null | undefined): string | undefined {
428
+ if (!value) return undefined
429
+ return value.split(';')[0]!.trim().toLowerCase() || undefined
430
+ }
431
+
432
+ /**
433
+ * Reads the negotiated response content type from the response headers as a base media type.
434
+ */
435
+ function getResponseContentType(headers: Headers | Record<string, string> | undefined): string | undefined {
436
+ if (!headers) return undefined
437
+ const value = headers instanceof Headers ? headers.get('Content-Type') : (headers['Content-Type'] ?? headers['content-type'])
438
+ return baseContentType(value)
439
+ }
440
+
441
+ /**
442
+ * Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type.
443
+ */
444
+ function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } {
445
+ if (typeof contentType === 'string') return { request: contentType }
446
+ return contentType ?? {}
447
+ }
448
+
449
+ /**
450
+ * The per-concern serializers for a call, the per-call serializer winning over the client's and
451
+ * falling back to the defaults.
452
+ */
453
+ function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
454
+ return {
455
+ querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
456
+ bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
457
+ pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Resolves everything a call needs before it touches the transport: merged headers with the
463
+ * negotiated content type, auth on headers or query, serialized cookies, the validated and
464
+ * serialized body, and the full URL.
465
+ */
466
+ async function resolveRequest<TBody, TRequest, TResponse>({
467
+ config,
468
+ requestConfig,
469
+ }: {
470
+ config: ClientConfig<TRequest, TResponse>
471
+ requestConfig: RequestConfig<TBody, TRequest, TResponse>
472
+ }): Promise<{ request: ResolvedRequest; codecs: Record<string, Codec> }> {
473
+ const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
474
+ const codecs = { ...config.codecs, ...requestConfig.codecs }
475
+
476
+ const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
477
+ const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
478
+ const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
479
+ if (responseContentType && !hasHeader(headers, 'accept')) {
480
+ headers['Accept'] = responseContentType
481
+ }
482
+
483
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
484
+
485
+ await resolveAuth({
486
+ security: requestConfig.security,
487
+ auth: requestConfig.auth ?? config.auth,
488
+ headers,
489
+ query,
490
+ })
491
+
492
+ if (requestConfig.cookies) {
493
+ const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
494
+ if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
495
+ }
496
+
497
+ const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
498
+ const requestContentTypeBase = baseContentType(requestContentType)
499
+ const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
500
+ const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer
501
+ const body = contentCodec?.serialize
502
+ ? contentCodec.serialize(validatedBody, requestContentType)
503
+ : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
504
+ // A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
505
+ if (body instanceof FormData) {
506
+ for (const key of Object.keys(headers)) {
507
+ if (key.toLowerCase() === 'content-type') delete headers[key]
508
+ }
509
+ } else if (requestContentTypeOption) {
510
+ headers['Content-Type'] = requestContentTypeOption
511
+ } else if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && !hasHeader(headers, 'content-type')) {
512
+ headers['Content-Type'] = 'application/json'
513
+ }
514
+
515
+ const url = serializeUrl({
516
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
517
+ pathParams: requestConfig.path ?? {},
518
+ search: querySerializer(query, requestConfig.styles?.query),
519
+ pathSerializer,
520
+ pathStyles: requestConfig.styles?.path,
521
+ })
522
+
523
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
524
+
525
+ return {
526
+ codecs,
527
+ request: {
528
+ url,
529
+ method: (requestConfig.method ?? 'GET').toUpperCase(),
530
+ headers,
531
+ body,
532
+ signal: requestConfig.signal,
533
+ credentials: requestConfig.credentials,
534
+ options,
535
+ responseType: requestConfig.responseType,
536
+ },
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Turns a transport result into the call result: decodes the body through the matching codec,
542
+ * validates it, and throws a `ResponseError` (after running the error interceptors) for a non-2xx
543
+ * response under `throwOnError`.
544
+ */
545
+ async function settleResult<TRequest, TResponse>({
546
+ result,
547
+ codecs,
548
+ throwOnError,
549
+ validator,
550
+ errorInterceptors,
551
+ }: {
552
+ result: TransportResult<unknown, TRequest, TResponse>
553
+ codecs: Record<string, Codec>
554
+ throwOnError: boolean
555
+ validator: { response?: Validator; error?: Validator } | undefined
556
+ errorInterceptors: InterceptorStack<ResponseError<unknown, TRequest, TResponse>>
557
+ }): Promise<CallResult<TRequest, TResponse>> {
558
+ const isSuccess = result.status >= 200 && result.status < 300
559
+ const contentType = result.contentType ?? getResponseContentType(result.headers)
560
+ let decoded = result.data
561
+ if (contentType) {
562
+ const codec = codecs[contentType]
563
+ if (codec?.deserialize) decoded = await codec.deserialize(result.data, contentType)
564
+ }
565
+
566
+ if (isSuccess) {
567
+ const data = await runValidator(validator?.response, decoded)
568
+ return { status: result.status, data, error: undefined, contentType, request: result.request, response: result.response }
569
+ }
570
+
571
+ const error = await runValidator(validator?.error, decoded)
572
+ if (throwOnError) {
573
+ const responseError = new ResponseError({
574
+ data: error,
575
+ status: result.status,
576
+ statusText: result.statusText,
577
+ contentType,
578
+ request: result.request,
579
+ response: result.response,
580
+ })
581
+ await errorInterceptors.run(responseError)
582
+ throw responseError
583
+ }
584
+ return { status: result.status, data: undefined, error, contentType, request: result.request, response: result.response }
585
+ }
586
+
587
+ /**
588
+ * Builds the shared client core bound to a transport, exported by each plugin as `client` plus a `createClient` factory.
589
+ */
590
+ export function createClientCore<TRequest = Request, TResponse = Response>(
591
+ options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
592
+ ): ClientInstance<TRequest, TResponse> {
593
+ const { defaultTransport, ...initialConfig } = options
594
+ let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
595
+
596
+ const interceptors: Interceptors<TRequest, TResponse> = {
597
+ request: createInterceptorStack<ResolvedRequest>(),
598
+ response: createInterceptorStack<TransportResult<unknown, TRequest, TResponse>>(),
599
+ error: createInterceptorStack<ResponseError<unknown, TRequest, TResponse>>(),
600
+ }
601
+
602
+ const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
603
+ const transport = requestConfig.transport ?? config.transport ?? defaultTransport
604
+ const { request, codecs } = await resolveRequest({ config, requestConfig })
605
+
606
+ const resolvedRequest = await interceptors.request.run(request)
607
+ const result = await interceptors.response.run(await transport(resolvedRequest))
608
+
609
+ return settleResult({
610
+ result,
611
+ codecs,
612
+ throwOnError: requestConfig.throwOnError ?? config.throwOnError ?? true,
613
+ validator: requestConfig.validator,
614
+ errorInterceptors: interceptors.error,
615
+ })
616
+ }) as ClientInstance<TRequest, TResponse>
617
+
618
+ client.getConfig = () => config
619
+ client.setConfig = (next) => {
620
+ config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
621
+ return config
622
+ }
623
+ client.getUrl = (requestConfig) => {
624
+ const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
625
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
626
+ return serializeUrl({
627
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
628
+ pathParams: requestConfig.path ?? {},
629
+ search: querySerializer(query, requestConfig.styles?.query),
630
+ pathSerializer,
631
+ pathStyles: requestConfig.styles?.path,
632
+ })
633
+ }
634
+ client.interceptors = interceptors
635
+ client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
636
+
637
+ return client
638
+ }
639
+
640
+ /**
641
+ * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized.
642
+ */
643
+ function detectResponseType(contentType: string | null): ResponseType | undefined {
644
+ if (!contentType) return undefined
645
+ if (contentType.includes('text/event-stream')) return 'stream'
646
+ if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
647
+ if (contentType.includes('text/')) return 'text'
648
+ if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
649
+ return undefined
650
+ }
651
+
652
+ /**
653
+ * Parses a `fetch` response body using the `responseType` (explicit or detected from the `Content-Type`), falling back to JSON-then-text.
654
+ */
655
+ async function parseResponse(response: Response, responseType?: ResponseType): Promise<unknown> {
656
+ if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) {
657
+ return undefined
658
+ }
659
+
660
+ switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) {
661
+ case 'text':
662
+ case 'document':
663
+ return response.text()
664
+ case 'blob':
665
+ return response.blob()
666
+ case 'arraybuffer':
667
+ return response.arrayBuffer()
668
+ case 'stream':
669
+ return response.body ?? undefined
670
+ case 'json': {
671
+ // An empty body with a JSON content-type would make response.json() throw, so treat it as no data.
672
+ const body = await response.text()
673
+ return body ? JSON.parse(body) : undefined
674
+ }
675
+ }
676
+
677
+ const text = await response.text()
678
+ if (!text) return undefined
679
+ try {
680
+ return JSON.parse(text)
681
+ } catch {
682
+ return text
683
+ }
684
+ }
685
+
686
+ /**
687
+ * The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response.
688
+ */
689
+ const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
690
+ const init: RequestInit = {
691
+ ...request.options, // cache, mode, redirect, keepalive, duplex, next, …
692
+ method: request.method,
693
+ headers: request.headers,
694
+ body: request.body,
695
+ signal: request.signal,
696
+ }
697
+ if (request.credentials) init.credentials = request.credentials
698
+
699
+ const nativeRequest = new Request(request.url, init)
700
+ const response = await globalThis.fetch(nativeRequest)
701
+ const data = await parseResponse(response, request.responseType)
702
+
703
+ return {
704
+ data,
705
+ status: response.status,
706
+ statusText: response.statusText,
707
+ headers: response.headers,
708
+ contentType: getResponseContentType(response.headers),
709
+ request: nativeRequest,
710
+ response,
711
+ }
712
+ }
713
+
714
+ /**
715
+ * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
716
+ */
717
+ export type ServerSentEvent<TData = unknown> = {
718
+ data: TData
719
+ event?: string
720
+ id?: string
721
+ retry?: number
722
+ }
723
+
724
+ async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
725
+ if (!('getReader' in stream)) {
726
+ yield* stream
727
+ return
728
+ }
729
+
730
+ const reader = stream.getReader()
731
+ try {
732
+ while (true) {
733
+ const { done, value } = await reader.read()
734
+ if (done) return
735
+ yield value
736
+ }
737
+ } finally {
738
+ await reader.cancel().catch(() => {})
739
+ }
740
+ }
741
+
742
+ function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
743
+ const data: Array<string> = []
744
+ const event: ServerSentEvent<TData> = { data: undefined as TData }
745
+ let seen = false
746
+
747
+ for (const line of raw.split('\n')) {
748
+ if (!line || line.startsWith(':')) continue
749
+ seen = true
750
+ const index = line.indexOf(':')
751
+ const field = index === -1 ? line : line.slice(0, index)
752
+ const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
753
+ if (field === 'data') data.push(value)
754
+ else if (field === 'event') event.event = value
755
+ else if (field === 'id') event.id = value
756
+ else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
757
+ }
758
+
759
+ if (!seen) return undefined
760
+
761
+ if (data.length) {
762
+ const joined = data.join('\n')
763
+ try {
764
+ event.data = JSON.parse(joined) as TData
765
+ } catch {
766
+ event.data = joined as TData
767
+ }
768
+ }
769
+ return event
770
+ }
771
+
772
+ /**
773
+ * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
774
+ */
775
+ export async function* parseEventStream<TData = unknown>(
776
+ stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
777
+ ): AsyncGenerator<ServerSentEvent<TData>> {
778
+ const decoder = new TextDecoder()
779
+ const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
780
+ let buffer = ''
781
+
782
+ for await (const chunk of readBytes(stream)) {
783
+ const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
784
+ buffer = blocks.pop() ?? ''
785
+ for (const block of blocks) {
786
+ const event = parseEvent<TData>(block)
787
+ if (event) yield event
788
+ }
789
+ }
790
+
791
+ const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
792
+ if (event) yield event
793
+ }
794
+
795
+ /**
796
+ * The resolved shape returned by a generated `text/event-stream` operation: the typed event
797
+ * `stream` plus the native `response`.
798
+ */
799
+ export type EventStreamResult<TData = unknown, TResponse = Response> = {
800
+ stream: AsyncGenerator<ServerSentEvent<TData>>
801
+ response: TResponse
802
+ }
803
+
804
+ /**
805
+ * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
806
+ * the parsed events as a typed async iterator. Generated SSE operations call this.
807
+ */
808
+ export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: Response }>): Promise<EventStreamResult<TData>> {
809
+ const { data, response } = await result
810
+ return {
811
+ response,
812
+ stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
813
+ }
814
+ }
815
+
816
+ export const client = createClientCore({ defaultTransport })
817
+
818
+ export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)