@kubb/plugin-axios 5.0.0-beta.79 → 5.0.0-beta.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-axios",
3
- "version": "5.0.0-beta.79",
3
+ "version": "5.0.0-beta.81",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on axios.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -49,11 +49,11 @@
49
49
  "registry": "https://registry.npmjs.org/"
50
50
  },
51
51
  "dependencies": {
52
- "@kubb/ast": "5.0.0-beta.79",
53
- "@kubb/core": "5.0.0-beta.79",
54
- "@kubb/renderer-jsx": "5.0.0-beta.79",
55
- "@kubb/plugin-ts": "5.0.0-beta.79",
56
- "@kubb/plugin-zod": "5.0.0-beta.79"
52
+ "@kubb/ast": "5.0.0-beta.80",
53
+ "@kubb/core": "5.0.0-beta.80",
54
+ "@kubb/renderer-jsx": "5.0.0-beta.80",
55
+ "@kubb/plugin-ts": "5.0.0-beta.81",
56
+ "@kubb/plugin-zod": "5.0.0-beta.81"
57
57
  },
58
58
  "devDependencies": {
59
59
  "axios": "^1.18.1",
@@ -63,7 +63,7 @@
63
63
  "@internals/utils": "0.0.0"
64
64
  },
65
65
  "peerDependencies": {
66
- "@kubb/renderer-jsx": "5.0.0-beta.79",
66
+ "@kubb/renderer-jsx": "5.0.0-beta.80",
67
67
  "axios": "^1.7.2"
68
68
  },
69
69
  "peerDependenciesMeta": {
package/src/plugin.ts CHANGED
@@ -80,6 +80,7 @@ export const pluginAxios = definePlugin<PluginAxios>((options) => {
80
80
  ctx.addGenerator(...selectedGenerators)
81
81
 
82
82
  const root = path.resolve(ctx.config.root, ctx.config.output.path)
83
+ const baseURLExpression = baseURL ? (baseURL.includes('${') ? `\`${baseURL.replaceAll('`', '\\`')}\`` : JSON.stringify(baseURL)) : undefined
83
84
 
84
85
  ctx.injectFile({
85
86
  baseName: 'serializers.ts',
@@ -91,7 +92,7 @@ export const pluginAxios = definePlugin<PluginAxios>((options) => {
91
92
  baseName: 'client.ts',
92
93
  path: path.resolve(root, '.kubb/client.ts'),
93
94
  copy: axiosClientTemplatePath,
94
- footer: baseURL ? `client.setConfig({ baseURL: ${JSON.stringify(baseURL)} })` : undefined,
95
+ footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : undefined,
95
96
  })
96
97
 
97
98
  ctx.injectFile({
@@ -254,108 +254,6 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
254
254
  /**
255
255
  * Thrown for a non-2xx response, so a resolved call always means success.
256
256
  */
257
- /**
258
- * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
259
- */
260
- export type ServerSentEvent<TData = unknown> = {
261
- data: TData
262
- event?: string
263
- id?: string
264
- retry?: number
265
- }
266
-
267
- async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
268
- if (!('getReader' in stream)) {
269
- yield* stream
270
- return
271
- }
272
-
273
- const reader = stream.getReader()
274
- try {
275
- while (true) {
276
- const { done, value } = await reader.read()
277
- if (done) return
278
- yield value
279
- }
280
- } finally {
281
- await reader.cancel().catch(() => {})
282
- }
283
- }
284
-
285
- function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
286
- const data: Array<string> = []
287
- const event: ServerSentEvent<TData> = { data: undefined as TData }
288
- let seen = false
289
-
290
- for (const line of raw.split('\n')) {
291
- if (!line || line.startsWith(':')) continue
292
- seen = true
293
- const index = line.indexOf(':')
294
- const field = index === -1 ? line : line.slice(0, index)
295
- const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
296
- if (field === 'data') data.push(value)
297
- else if (field === 'event') event.event = value
298
- else if (field === 'id') event.id = value
299
- else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
300
- }
301
-
302
- if (!seen) return undefined
303
-
304
- if (data.length) {
305
- const joined = data.join('\n')
306
- try {
307
- event.data = JSON.parse(joined) as TData
308
- } catch {
309
- event.data = joined as TData
310
- }
311
- }
312
- return event
313
- }
314
-
315
- /**
316
- * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
317
- */
318
- export async function* parseEventStream<TData = unknown>(
319
- stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
320
- ): AsyncGenerator<ServerSentEvent<TData>> {
321
- const decoder = new TextDecoder()
322
- const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
323
- let buffer = ''
324
-
325
- for await (const chunk of readBytes(stream)) {
326
- const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
327
- buffer = blocks.pop() ?? ''
328
- for (const block of blocks) {
329
- const event = parseEvent<TData>(block)
330
- if (event) yield event
331
- }
332
- }
333
-
334
- const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
335
- if (event) yield event
336
- }
337
-
338
- /**
339
- * The resolved shape returned by a generated `text/event-stream` operation: the typed event
340
- * `stream` plus the native `response`.
341
- */
342
- export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
343
- stream: AsyncGenerator<ServerSentEvent<TData>>
344
- response: TResponse
345
- }
346
-
347
- /**
348
- * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
349
- * the parsed events as a typed async iterator. Generated SSE operations call this.
350
- */
351
- export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
352
- const { data, response } = await result
353
- return {
354
- response,
355
- stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
356
- }
357
- }
358
-
359
257
  export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
360
258
  data: TError
361
259
  status: number
@@ -416,6 +314,32 @@ function serializeUrl({
416
314
  return path + (search ? `?${search}` : '')
417
315
  }
418
316
 
317
+ /**
318
+ * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
319
+ */
320
+ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
321
+ const ids = new Map<number, number>()
322
+ let counter = 0
323
+ return {
324
+ use(fn) {
325
+ const id = ++counter
326
+ ids.set(id, register(fn))
327
+ return id
328
+ },
329
+ eject(id) {
330
+ const nativeId = ids.get(id)
331
+ if (nativeId === undefined) return
332
+ ejectNative(nativeId)
333
+ ids.delete(id)
334
+ },
335
+ update(id, fn) {
336
+ const nativeId = ids.get(id)
337
+ if (nativeId !== undefined) ejectNative(nativeId)
338
+ ids.set(id, register(fn))
339
+ },
340
+ }
341
+ }
342
+
419
343
  /**
420
344
  * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
421
345
  */
@@ -428,16 +352,23 @@ export async function resolveAuth(params: {
428
352
  const { security, auth, headers, query } = params
429
353
  if (!security?.length || auth === undefined) return
430
354
 
355
+ // An explicit caller-set header wins over the resolved token.
356
+ const hasHeader = (name: string) => Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase())
357
+
431
358
  for (const scheme of security) {
432
359
  const token = typeof auth === 'function' ? await auth(scheme) : auth
433
360
  if (token === undefined) continue
434
361
 
435
362
  if (scheme.type === 'apiKey') {
436
363
  const name = scheme.name ?? 'Authorization'
437
- if (scheme.in === 'query') query[name] = token
438
- else if (scheme.in === 'cookie') headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
439
- else headers[name] = token
440
- } else {
364
+ if (scheme.in === 'query') {
365
+ if (query[name] === undefined) query[name] = token
366
+ } else if (scheme.in === 'cookie') {
367
+ headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
368
+ } else if (!hasHeader(name)) {
369
+ headers[name] = token
370
+ }
371
+ } else if (!hasHeader('Authorization')) {
441
372
  headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
442
373
  }
443
374
  return
@@ -449,32 +380,6 @@ async function runValidator<T>(validator: Validator<T> | undefined, value: T): P
449
380
  return validateStandardSchema(validator, value)
450
381
  }
451
382
 
452
- /**
453
- * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
454
- */
455
- function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
456
- const ids = new Map<number, number>()
457
- let counter = 0
458
- return {
459
- use(fn) {
460
- const id = ++counter
461
- ids.set(id, register(fn))
462
- return id
463
- },
464
- eject(id) {
465
- const nativeId = ids.get(id)
466
- if (nativeId === undefined) return
467
- ejectNative(nativeId)
468
- ids.delete(id)
469
- },
470
- update(id, fn) {
471
- const nativeId = ids.get(id)
472
- if (nativeId !== undefined) ejectNative(nativeId)
473
- ids.set(id, register(fn))
474
- },
475
- }
476
- }
477
-
478
383
  /**
479
384
  * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
480
385
  */
@@ -500,6 +405,135 @@ function resolveContentType(contentType: ContentType | undefined): { request?: s
500
405
  return contentType ?? {}
501
406
  }
502
407
 
408
+ /**
409
+ * The per-concern serializers for a call, the per-call serializer winning over the client's and
410
+ * falling back to the defaults.
411
+ */
412
+ function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) {
413
+ return {
414
+ querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer,
415
+ bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer,
416
+ pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer,
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Resolves everything a call needs before it touches axios: merged headers with the negotiated
422
+ * content type, auth on headers or query, serialized cookies, the validated and serialized body,
423
+ * and the final axios request config with `throwOnError` riding `validateStatus`.
424
+ */
425
+ async function resolveRequest<TBody, TRequest, TResponse>({
426
+ config,
427
+ requestConfig,
428
+ }: {
429
+ config: ClientConfig
430
+ requestConfig: RequestConfig<TBody, TRequest, TResponse>
431
+ }): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record<string, Codec>; throwOnError: boolean }> {
432
+ const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
433
+ const codecs = { ...config.codecs, ...requestConfig.codecs }
434
+
435
+ const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
436
+ const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
437
+ const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
438
+ if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
439
+ headers['Accept'] = responseContentType
440
+ }
441
+
442
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
443
+
444
+ await resolveAuth({
445
+ security: requestConfig.security,
446
+ auth: requestConfig.auth ?? config.auth,
447
+ headers,
448
+ query,
449
+ })
450
+
451
+ if (requestConfig.cookies) {
452
+ const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
453
+ if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
454
+ }
455
+
456
+ const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
457
+ const requestContentTypeBase = baseContentType(requestContentType)
458
+ const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
459
+ const body = contentCodec?.serialize
460
+ ? contentCodec.serialize(validatedBody, requestContentType)
461
+ : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
462
+ // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
463
+ if (body instanceof FormData) {
464
+ delete headers['Content-Type']
465
+ delete headers['content-type']
466
+ } else if (requestContentTypeOption) {
467
+ headers['Content-Type'] = requestContentTypeOption
468
+ }
469
+
470
+ const pathParams = requestConfig.path ?? {}
471
+ const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
472
+ pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
473
+ )
474
+
475
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
476
+ const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
477
+
478
+ const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
479
+
480
+ const axiosConfig: AxiosRequestConfig = {
481
+ ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
482
+ url,
483
+ baseURL: requestConfig.baseURL ?? config.baseURL,
484
+ method: requestConfig.method ?? 'GET',
485
+ headers,
486
+ params: query,
487
+ paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
488
+ data: body,
489
+ transformRequest: (data) => data,
490
+ signal: requestConfig.signal,
491
+ responseType: requestConfig.responseType,
492
+ validateStatus,
493
+ }
494
+
495
+ // Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser.
496
+ // The default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
497
+ if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
498
+ axiosConfig.adapter = 'fetch'
499
+ }
500
+
501
+ return { axiosConfig, codecs, throwOnError }
502
+ }
503
+
504
+ /**
505
+ * Turns an axios response into the call result: decodes the body through the matching codec and
506
+ * validates the success or error body. A thrown axios error never reaches this, so `error` here is
507
+ * always the body of a non-2xx response that `validateStatus` let through.
508
+ */
509
+ async function settleResponse<TRequest, TResponse>({
510
+ response,
511
+ codecs,
512
+ validator,
513
+ }: {
514
+ response: AxiosResponse
515
+ codecs: Record<string, Codec>
516
+ validator: { response?: Validator; error?: Validator } | undefined
517
+ }): Promise<CallResult<TRequest, TResponse>> {
518
+ const isSuccess = response.status >= 200 && response.status < 300
519
+ const contentType = getResponseContentType(response.headers as Record<string, unknown>)
520
+ let decoded: unknown = response.data
521
+ if (contentType) {
522
+ const codec = codecs[contentType]
523
+ if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
524
+ }
525
+ const data = isSuccess ? await runValidator(validator?.response, decoded) : undefined
526
+ const error = isSuccess ? undefined : await runValidator(validator?.error, decoded)
527
+ return {
528
+ status: response.status,
529
+ data,
530
+ error,
531
+ contentType,
532
+ request: response.config as TRequest,
533
+ response: response as TResponse,
534
+ }
535
+ }
536
+
503
537
  /**
504
538
  * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`.
505
539
  */
@@ -530,96 +564,11 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
530
564
 
531
565
  const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
532
566
  const activeInstance = requestConfig.transport ?? config.transport ?? instance
533
- const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
534
- const bodySerializer = requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer
535
- const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
536
- const codecs = { ...config.codecs, ...requestConfig.codecs }
537
-
538
- const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
539
- const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
540
- const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
541
- if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
542
- headers['Accept'] = responseContentType
543
- }
544
-
545
- const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
546
-
547
- await resolveAuth({
548
- security: requestConfig.security,
549
- auth: requestConfig.auth ?? config.auth,
550
- headers,
551
- query,
552
- })
553
-
554
- if (requestConfig.cookies) {
555
- const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
556
- if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
557
- }
558
-
559
- const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
560
- const requestContentTypeBase = baseContentType(requestContentType)
561
- const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
562
- const body = contentCodec?.serialize
563
- ? contentCodec.serialize(validatedBody, requestContentType)
564
- : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
565
- // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
566
- if (body instanceof FormData) {
567
- delete headers['Content-Type']
568
- delete headers['content-type']
569
- } else if (requestContentTypeOption) {
570
- headers['Content-Type'] = requestContentTypeOption
571
- }
572
- const pathParams = requestConfig.path ?? {}
573
- const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
574
- pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
575
- )
576
- const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
577
-
578
- const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
579
- const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
580
-
581
- const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
582
-
583
- const axiosConfig: AxiosRequestConfig = {
584
- ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
585
- url,
586
- baseURL,
587
- method: requestConfig.method ?? 'GET',
588
- headers,
589
- params: query,
590
- paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
591
- data: body,
592
- transformRequest: (data) => data,
593
- signal: requestConfig.signal,
594
- responseType: requestConfig.responseType,
595
- validateStatus,
596
- }
597
-
598
- // Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser;
599
- // the default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
600
- if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
601
- axiosConfig.adapter = 'fetch'
602
- }
567
+ const { axiosConfig, codecs, throwOnError } = await resolveRequest({ config, requestConfig })
603
568
 
604
569
  try {
605
570
  const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
606
- const isSuccess = response.status >= 200 && response.status < 300
607
- const contentType = getResponseContentType(response.headers as Record<string, unknown>)
608
- let decoded: unknown = response.data
609
- if (contentType) {
610
- const codec = codecs[contentType]
611
- if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
612
- }
613
- const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
614
- const error = isSuccess ? undefined : await runValidator(requestConfig.validator?.error, decoded)
615
- return {
616
- status: response.status,
617
- data,
618
- error,
619
- contentType,
620
- request: response.config as TRequest,
621
- response: response as TResponse,
622
- }
571
+ return await settleResponse({ response, codecs, validator: requestConfig.validator })
623
572
  } catch (error) {
624
573
  const axiosError = error as AxiosError
625
574
  if (throwOnError && axiosError.response) {
@@ -642,11 +591,10 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
642
591
  return config
643
592
  }
644
593
  client.getUrl = (requestConfig) => {
645
- const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
646
- const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
594
+ const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
647
595
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
648
596
  return serializeUrl({
649
- parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
597
+ parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
650
598
  pathParams: requestConfig.path ?? {},
651
599
  search: querySerializer(query, requestConfig.styles?.query),
652
600
  pathSerializer,
@@ -659,6 +607,108 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
659
607
  return client
660
608
  }
661
609
 
610
+ /**
611
+ * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
612
+ */
613
+ export type ServerSentEvent<TData = unknown> = {
614
+ data: TData
615
+ event?: string
616
+ id?: string
617
+ retry?: number
618
+ }
619
+
620
+ async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
621
+ if (!('getReader' in stream)) {
622
+ yield* stream
623
+ return
624
+ }
625
+
626
+ const reader = stream.getReader()
627
+ try {
628
+ while (true) {
629
+ const { done, value } = await reader.read()
630
+ if (done) return
631
+ yield value
632
+ }
633
+ } finally {
634
+ await reader.cancel().catch(() => {})
635
+ }
636
+ }
637
+
638
+ function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
639
+ const data: Array<string> = []
640
+ const event: ServerSentEvent<TData> = { data: undefined as TData }
641
+ let seen = false
642
+
643
+ for (const line of raw.split('\n')) {
644
+ if (!line || line.startsWith(':')) continue
645
+ seen = true
646
+ const index = line.indexOf(':')
647
+ const field = index === -1 ? line : line.slice(0, index)
648
+ const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
649
+ if (field === 'data') data.push(value)
650
+ else if (field === 'event') event.event = value
651
+ else if (field === 'id') event.id = value
652
+ else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
653
+ }
654
+
655
+ if (!seen) return undefined
656
+
657
+ if (data.length) {
658
+ const joined = data.join('\n')
659
+ try {
660
+ event.data = JSON.parse(joined) as TData
661
+ } catch {
662
+ event.data = joined as TData
663
+ }
664
+ }
665
+ return event
666
+ }
667
+
668
+ /**
669
+ * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
670
+ */
671
+ export async function* parseEventStream<TData = unknown>(
672
+ stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
673
+ ): AsyncGenerator<ServerSentEvent<TData>> {
674
+ const decoder = new TextDecoder()
675
+ const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
676
+ let buffer = ''
677
+
678
+ for await (const chunk of readBytes(stream)) {
679
+ const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
680
+ buffer = blocks.pop() ?? ''
681
+ for (const block of blocks) {
682
+ const event = parseEvent<TData>(block)
683
+ if (event) yield event
684
+ }
685
+ }
686
+
687
+ const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
688
+ if (event) yield event
689
+ }
690
+
691
+ /**
692
+ * The resolved shape returned by a generated `text/event-stream` operation: the typed event
693
+ * `stream` plus the native `response`.
694
+ */
695
+ export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
696
+ stream: AsyncGenerator<ServerSentEvent<TData>>
697
+ response: TResponse
698
+ }
699
+
700
+ /**
701
+ * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
702
+ * the parsed events as a typed async iterator. Generated SSE operations call this.
703
+ */
704
+ export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
705
+ const { data, response } = await result
706
+ return {
707
+ response,
708
+ stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
709
+ }
710
+ }
711
+
662
712
  export const client = createClientCore()
663
713
 
664
714
  export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)