@kizunajs/fetch 2.0.0-beta.20 → 2.0.0-beta.22

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 (3) hide show
  1. package/dist/index.cjs +140 -30
  2. package/dist/index.mjs +141 -31
  3. package/package.json +4 -4
package/dist/index.cjs CHANGED
@@ -10,10 +10,13 @@ let zod = require("zod");
10
10
  */
11
11
  const runtimeSource = String.raw`/**
12
12
  * What the client knows about one response: nothing for a body it reads as
13
- * JSON, the media type for one that streams.
13
+ * JSON, how to read one that is not, the media type for one that streams.
14
14
  */
15
15
  export type GeneratedResponse =
16
16
  | Record<string, never>
17
+ | {
18
+ read: 'bytes' | 'text';
19
+ }
17
20
  | {
18
21
  stream: true;
19
22
  contentType: string;
@@ -58,6 +61,7 @@ export interface ClientConfig {
58
61
  fetch?: typeof fetch;
59
62
  onRequest?: (request: OutgoingRequest) => void | Promise<void>;
60
63
  retry?: RetryOptions;
64
+ reconnect?: ReconnectOptions;
61
65
  }
62
66
 
63
67
  /**
@@ -79,6 +83,25 @@ export interface RetryOptions {
79
83
  maximumDelay?: number;
80
84
  }
81
85
 
86
+ /**
87
+ * Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its
88
+ * connection drops, sending the id of the last event as Last-Event-ID.
89
+ */
90
+ export interface ReconnectOptions {
91
+ /**
92
+ * Reconnections in a row before the stream fails. An event received resets the count.
93
+ */
94
+ maximumAttempts?: number;
95
+ /**
96
+ * Milliseconds to wait before reconnecting, until the server sends a retry field.
97
+ */
98
+ reconnectionTime?: number;
99
+ /**
100
+ * Milliseconds the wait may grow to after reconnections in a row that fail.
101
+ */
102
+ maximumDelay?: number;
103
+ }
104
+
82
105
  /**
83
106
  * One method on the client. Its '~route' names the method it sends and
84
107
  * whether its response streams, so a wrapper like @kizunajs/tanstack-query
@@ -187,17 +210,29 @@ const findLineEnd = (buffer: string, final: boolean): { index: number; length: n
187
210
  /**
188
211
  * Reads text/event-stream as the WHATWG HTML specification parses it.
189
212
  */
190
- async function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<ServerSentEvent> {
213
+ interface EventStreamState {
214
+ lastEventId: string;
215
+ reconnectionTime: number;
216
+ }
217
+
218
+ async function* readEvents(
219
+ body: ReadableStream<Uint8Array>,
220
+ state: EventStreamState = {
221
+ lastEventId: '',
222
+ reconnectionTime: 3000,
223
+ }
224
+ ): AsyncGenerator<ServerSentEvent> {
191
225
  const reader = body.getReader();
192
226
  const decoder = new TextDecoder();
193
227
  let buffer = '';
194
228
  let first = true;
195
229
  let dataLines: string[] = [];
196
230
  let eventType = '';
197
- let lastEventId = '';
231
+ let lastEventId = state.lastEventId;
198
232
  let retry: number | undefined;
199
233
 
200
234
  const dispatch = (): ServerSentEvent | undefined => {
235
+ state.lastEventId = lastEventId;
201
236
  if (dataLines.length === 0) {
202
237
  eventType = '';
203
238
  return undefined;
@@ -232,7 +267,10 @@ async function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<Ser
232
267
  if (!value.includes('\0')) lastEventId = value;
233
268
  break;
234
269
  case 'retry':
235
- if (/^\d+$/.test(value)) retry = Number(value);
270
+ if (/^\d+$/.test(value)) {
271
+ retry = Number(value);
272
+ state.reconnectionTime = Math.min(retry, 3_600_000);
273
+ }
236
274
  break;
237
275
  }
238
276
  return undefined;
@@ -262,6 +300,57 @@ async function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<Ser
262
300
  }
263
301
  }
264
302
 
303
+ async function* reconnectingEvents(
304
+ first: Response,
305
+ reopen: (lastEventId: string) => Promise<Response>,
306
+ options: ReconnectOptions,
307
+ signal: AbortSignal | null | undefined
308
+ ): AsyncGenerator<ServerSentEvent> {
309
+ const maximumAttempts = options.maximumAttempts ?? 5;
310
+ const maximumDelay = options.maximumDelay ?? 30_000;
311
+ const state: EventStreamState = {
312
+ lastEventId: '',
313
+ reconnectionTime: options.reconnectionTime ?? 3000,
314
+ };
315
+ let response: Response | undefined = first;
316
+ let attempts = 0;
317
+ let failure: unknown;
318
+ for (;;) {
319
+ if (response?.body) {
320
+ try {
321
+ for await (const message of readEvents(response.body, state)) {
322
+ attempts = 0;
323
+ failure = undefined;
324
+ yield message;
325
+ }
326
+ } catch (error) {
327
+ failure = error;
328
+ }
329
+ }
330
+ if (signal?.aborted) throw signal.reason;
331
+ if (attempts >= maximumAttempts) {
332
+ if (failure !== undefined) throw failure;
333
+ return;
334
+ }
335
+ attempts += 1;
336
+ const ceiling = Math.min(maximumDelay, state.reconnectionTime * 2 ** (attempts - 1));
337
+ await wait(state.reconnectionTime + Math.random() * Math.max(0, ceiling - state.reconnectionTime), signal);
338
+ try {
339
+ response = await reopen(state.lastEventId);
340
+ } catch (error) {
341
+ if (signal?.aborted) throw error;
342
+ failure = error;
343
+ response = undefined;
344
+ continue;
345
+ }
346
+ if (response.status === 204) return;
347
+ const contentType = response.headers.get('content-type') ?? '';
348
+ if (response.status !== first.status || !contentType.toLowerCase().startsWith('text/event-stream')) {
349
+ throw new Error('Reconnecting to the event stream failed with status ' + response.status);
350
+ }
351
+ }
352
+ }
353
+
265
354
  async function* readText(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {
266
355
  const reader = body.getReader();
267
356
  const decoder = new TextDecoder();
@@ -360,41 +449,52 @@ const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
360
449
  const call = async (args: CallArgs = {}) => {
361
450
  const query = args.query === undefined ? '' : buildSearchParams(args.query).toString();
362
451
  const url = config.baseUrl + buildPath(route.path, args.params) + (query.length > 0 ? '?' + query : '');
363
- const headers = new Headers(config.baseHeaders);
364
- for (const [name, value] of Object.entries(args.headers ?? {})) {
365
- if (value !== undefined) headers.set(name, value);
366
- }
367
- const init: RequestInit = {
368
- method: route.method,
369
- headers,
370
- };
371
- if (args.body !== undefined) init.body = encodeBody(route, args.body, headers);
372
- if (config.credentials !== undefined) init.credentials = config.credentials;
373
- await config.onRequest?.({
374
- url,
375
- method: route.method,
376
- headers,
377
- route,
378
- });
379
-
380
- const response = await send(
381
- url,
382
- {
452
+ const prepare = async (lastEventId = ''): Promise<RequestInit> => {
453
+ const headers = new Headers(config.baseHeaders);
454
+ for (const [name, value] of Object.entries(args.headers ?? {})) {
455
+ if (value !== undefined) headers.set(name, value);
456
+ }
457
+ if (lastEventId !== '') headers.set('Last-Event-ID', lastEventId);
458
+ const init: RequestInit = {
459
+ method: route.method,
460
+ headers,
461
+ };
462
+ if (args.body !== undefined) init.body = encodeBody(route, args.body, headers);
463
+ if (config.credentials !== undefined) init.credentials = config.credentials;
464
+ await config.onRequest?.({
465
+ url,
466
+ method: route.method,
467
+ headers,
468
+ route,
469
+ });
470
+ return {
383
471
  ...init,
384
472
  ...args.fetchOptions,
385
- },
386
- route,
387
- config
388
- );
473
+ };
474
+ };
475
+
476
+ const response = await send(url, await prepare(), route, config);
389
477
  const responseHeaders: Record<string, string> = {};
390
478
  response.headers.forEach((value, name) => {
391
479
  responseHeaders[name] = value;
392
480
  });
393
481
  const declared = route.responses[response.status];
394
482
  if (declared !== undefined && 'stream' in declared && response.body !== null) {
483
+ const essence = (declared.contentType.split(';')[0] ?? '').trim().toLowerCase();
484
+ const reconnect = essence === 'text/event-stream' && IDEMPOTENT_METHODS.has(route.method) ? config.reconnect : undefined;
485
+ const reopen = async (lastEventId: string) => send(url, await prepare(lastEventId), route, config);
395
486
  return {
396
487
  status: response.status,
397
- body: readStream(response.body, declared.contentType),
488
+ body: reconnect
489
+ ? reconnectingEvents(response, reopen, reconnect, args.fetchOptions?.signal)
490
+ : readStream(response.body, declared.contentType),
491
+ headers: responseHeaders,
492
+ };
493
+ }
494
+ if (declared !== undefined && 'read' in declared) {
495
+ return {
496
+ status: response.status,
497
+ body: declared.read === 'bytes' ? new Uint8Array(await response.arrayBuffer()) : await response.text(),
398
498
  headers: responseHeaders,
399
499
  };
400
500
  }
@@ -850,7 +950,7 @@ const emitRoute = (routeKey, key, route, collector, namespaceName) => {
850
950
  const argsType = `{\n${indent(args.map((arg) => `${arg};`).join("\n"))}\n}`;
851
951
  const tableEntries = [`method: '${route.method}'`, `path: '${route.path}'`];
852
952
  if (route.contentType) tableEntries.push(`contentType: '${route.contentType}'`);
853
- const responses = Object.entries(route.responses).map(([status, response]) => (0, kizunajs_generator.isStreamResponse)(response) ? `${status}: { stream: true, contentType: '${(0, kizunajs_generator.streamContentType)(response)}' }` : `${status}: {}`);
953
+ const responses = Object.entries(route.responses).map(([status, response]) => (0, kizunajs_generator.isStreamResponse)(response) ? `${status}: { stream: true, contentType: '${(0, kizunajs_generator.streamContentType)(response)}' }` : `${status}: ${readEntry(response)}`);
854
954
  if (validationBody && !declaredBadRequest) responses.push("400: {}");
855
955
  tableEntries.push(`responses: {\n${indent(responses.map((entry) => `${entry},`).join("\n"))}\n}`);
856
956
  const streams = Object.values(route.responses).some((response) => (0, kizunajs_generator.isStreamResponse)(response));
@@ -861,6 +961,16 @@ const emitRoute = (routeKey, key, route, collector, namespaceName) => {
861
961
  table: `${key}: {\n${indent(tableEntries.map((entry) => `${entry},`).join("\n"))}\n},`
862
962
  };
863
963
  };
964
+ /**
965
+ * How the client reads a body that does not stream: as bytes for a binary schema, as text for any other media type than
966
+ * JSON, as JSON otherwise.
967
+ */
968
+ const readEntry = (response) => {
969
+ const schema = (0, kizunajs_generator.resolveResponseBody)(response);
970
+ if (schema && (0, kizunajs_generator.isBinarySchema)(schema)) return `{ read: 'bytes' }`;
971
+ const contentType = (0, kizunajs_generator.resolveResponseContentType)(response);
972
+ return contentType !== void 0 && !(0, kizunajs_generator.isJsonMediaType)(contentType) ? `{ read: 'text' }` : "{}";
973
+ };
864
974
  const isRoute = (value) => !!value && typeof value === "object" && typeof value.method === "string";
865
975
  const emitTree = (routes, prefix, collector, namespaceName) => {
866
976
  const signatures = [];
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ValidationErrorSchema } from "kizunajs/schemas";
2
- import { defineClient, isBinarySchema, isFileSchema, isStreamResponse, isVoidSchema, listedRoutes, readDef, readDeprecation, readDiscriminatedUnion, readMetaBrand, readMetaDescription, readMetaExamples, readMetaId, readObjectShape, resolveResponseBody, resolveResponseHeaders, sanitizeIdentifier, streamContentType, streamMode, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
2
+ import { defineClient, isBinarySchema, isFileSchema, isJsonMediaType, isStreamResponse, isVoidSchema, listedRoutes, readDef, readDeprecation, readDiscriminatedUnion, readMetaBrand, readMetaDescription, readMetaExamples, readMetaId, readObjectShape, resolveResponseBody, resolveResponseContentType, resolveResponseHeaders, sanitizeIdentifier, streamContentType, streamMode, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
3
3
  import { z } from "zod";
4
4
  //#region src/runtime.ts
5
5
  /**
@@ -9,10 +9,13 @@ import { z } from "zod";
9
9
  */
10
10
  const runtimeSource = String.raw`/**
11
11
  * What the client knows about one response: nothing for a body it reads as
12
- * JSON, the media type for one that streams.
12
+ * JSON, how to read one that is not, the media type for one that streams.
13
13
  */
14
14
  export type GeneratedResponse =
15
15
  | Record<string, never>
16
+ | {
17
+ read: 'bytes' | 'text';
18
+ }
16
19
  | {
17
20
  stream: true;
18
21
  contentType: string;
@@ -57,6 +60,7 @@ export interface ClientConfig {
57
60
  fetch?: typeof fetch;
58
61
  onRequest?: (request: OutgoingRequest) => void | Promise<void>;
59
62
  retry?: RetryOptions;
63
+ reconnect?: ReconnectOptions;
60
64
  }
61
65
 
62
66
  /**
@@ -78,6 +82,25 @@ export interface RetryOptions {
78
82
  maximumDelay?: number;
79
83
  }
80
84
 
85
+ /**
86
+ * Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its
87
+ * connection drops, sending the id of the last event as Last-Event-ID.
88
+ */
89
+ export interface ReconnectOptions {
90
+ /**
91
+ * Reconnections in a row before the stream fails. An event received resets the count.
92
+ */
93
+ maximumAttempts?: number;
94
+ /**
95
+ * Milliseconds to wait before reconnecting, until the server sends a retry field.
96
+ */
97
+ reconnectionTime?: number;
98
+ /**
99
+ * Milliseconds the wait may grow to after reconnections in a row that fail.
100
+ */
101
+ maximumDelay?: number;
102
+ }
103
+
81
104
  /**
82
105
  * One method on the client. Its '~route' names the method it sends and
83
106
  * whether its response streams, so a wrapper like @kizunajs/tanstack-query
@@ -186,17 +209,29 @@ const findLineEnd = (buffer: string, final: boolean): { index: number; length: n
186
209
  /**
187
210
  * Reads text/event-stream as the WHATWG HTML specification parses it.
188
211
  */
189
- async function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<ServerSentEvent> {
212
+ interface EventStreamState {
213
+ lastEventId: string;
214
+ reconnectionTime: number;
215
+ }
216
+
217
+ async function* readEvents(
218
+ body: ReadableStream<Uint8Array>,
219
+ state: EventStreamState = {
220
+ lastEventId: '',
221
+ reconnectionTime: 3000,
222
+ }
223
+ ): AsyncGenerator<ServerSentEvent> {
190
224
  const reader = body.getReader();
191
225
  const decoder = new TextDecoder();
192
226
  let buffer = '';
193
227
  let first = true;
194
228
  let dataLines: string[] = [];
195
229
  let eventType = '';
196
- let lastEventId = '';
230
+ let lastEventId = state.lastEventId;
197
231
  let retry: number | undefined;
198
232
 
199
233
  const dispatch = (): ServerSentEvent | undefined => {
234
+ state.lastEventId = lastEventId;
200
235
  if (dataLines.length === 0) {
201
236
  eventType = '';
202
237
  return undefined;
@@ -231,7 +266,10 @@ async function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<Ser
231
266
  if (!value.includes('\0')) lastEventId = value;
232
267
  break;
233
268
  case 'retry':
234
- if (/^\d+$/.test(value)) retry = Number(value);
269
+ if (/^\d+$/.test(value)) {
270
+ retry = Number(value);
271
+ state.reconnectionTime = Math.min(retry, 3_600_000);
272
+ }
235
273
  break;
236
274
  }
237
275
  return undefined;
@@ -261,6 +299,57 @@ async function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<Ser
261
299
  }
262
300
  }
263
301
 
302
+ async function* reconnectingEvents(
303
+ first: Response,
304
+ reopen: (lastEventId: string) => Promise<Response>,
305
+ options: ReconnectOptions,
306
+ signal: AbortSignal | null | undefined
307
+ ): AsyncGenerator<ServerSentEvent> {
308
+ const maximumAttempts = options.maximumAttempts ?? 5;
309
+ const maximumDelay = options.maximumDelay ?? 30_000;
310
+ const state: EventStreamState = {
311
+ lastEventId: '',
312
+ reconnectionTime: options.reconnectionTime ?? 3000,
313
+ };
314
+ let response: Response | undefined = first;
315
+ let attempts = 0;
316
+ let failure: unknown;
317
+ for (;;) {
318
+ if (response?.body) {
319
+ try {
320
+ for await (const message of readEvents(response.body, state)) {
321
+ attempts = 0;
322
+ failure = undefined;
323
+ yield message;
324
+ }
325
+ } catch (error) {
326
+ failure = error;
327
+ }
328
+ }
329
+ if (signal?.aborted) throw signal.reason;
330
+ if (attempts >= maximumAttempts) {
331
+ if (failure !== undefined) throw failure;
332
+ return;
333
+ }
334
+ attempts += 1;
335
+ const ceiling = Math.min(maximumDelay, state.reconnectionTime * 2 ** (attempts - 1));
336
+ await wait(state.reconnectionTime + Math.random() * Math.max(0, ceiling - state.reconnectionTime), signal);
337
+ try {
338
+ response = await reopen(state.lastEventId);
339
+ } catch (error) {
340
+ if (signal?.aborted) throw error;
341
+ failure = error;
342
+ response = undefined;
343
+ continue;
344
+ }
345
+ if (response.status === 204) return;
346
+ const contentType = response.headers.get('content-type') ?? '';
347
+ if (response.status !== first.status || !contentType.toLowerCase().startsWith('text/event-stream')) {
348
+ throw new Error('Reconnecting to the event stream failed with status ' + response.status);
349
+ }
350
+ }
351
+ }
352
+
264
353
  async function* readText(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {
265
354
  const reader = body.getReader();
266
355
  const decoder = new TextDecoder();
@@ -359,41 +448,52 @@ const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
359
448
  const call = async (args: CallArgs = {}) => {
360
449
  const query = args.query === undefined ? '' : buildSearchParams(args.query).toString();
361
450
  const url = config.baseUrl + buildPath(route.path, args.params) + (query.length > 0 ? '?' + query : '');
362
- const headers = new Headers(config.baseHeaders);
363
- for (const [name, value] of Object.entries(args.headers ?? {})) {
364
- if (value !== undefined) headers.set(name, value);
365
- }
366
- const init: RequestInit = {
367
- method: route.method,
368
- headers,
369
- };
370
- if (args.body !== undefined) init.body = encodeBody(route, args.body, headers);
371
- if (config.credentials !== undefined) init.credentials = config.credentials;
372
- await config.onRequest?.({
373
- url,
374
- method: route.method,
375
- headers,
376
- route,
377
- });
378
-
379
- const response = await send(
380
- url,
381
- {
451
+ const prepare = async (lastEventId = ''): Promise<RequestInit> => {
452
+ const headers = new Headers(config.baseHeaders);
453
+ for (const [name, value] of Object.entries(args.headers ?? {})) {
454
+ if (value !== undefined) headers.set(name, value);
455
+ }
456
+ if (lastEventId !== '') headers.set('Last-Event-ID', lastEventId);
457
+ const init: RequestInit = {
458
+ method: route.method,
459
+ headers,
460
+ };
461
+ if (args.body !== undefined) init.body = encodeBody(route, args.body, headers);
462
+ if (config.credentials !== undefined) init.credentials = config.credentials;
463
+ await config.onRequest?.({
464
+ url,
465
+ method: route.method,
466
+ headers,
467
+ route,
468
+ });
469
+ return {
382
470
  ...init,
383
471
  ...args.fetchOptions,
384
- },
385
- route,
386
- config
387
- );
472
+ };
473
+ };
474
+
475
+ const response = await send(url, await prepare(), route, config);
388
476
  const responseHeaders: Record<string, string> = {};
389
477
  response.headers.forEach((value, name) => {
390
478
  responseHeaders[name] = value;
391
479
  });
392
480
  const declared = route.responses[response.status];
393
481
  if (declared !== undefined && 'stream' in declared && response.body !== null) {
482
+ const essence = (declared.contentType.split(';')[0] ?? '').trim().toLowerCase();
483
+ const reconnect = essence === 'text/event-stream' && IDEMPOTENT_METHODS.has(route.method) ? config.reconnect : undefined;
484
+ const reopen = async (lastEventId: string) => send(url, await prepare(lastEventId), route, config);
394
485
  return {
395
486
  status: response.status,
396
- body: readStream(response.body, declared.contentType),
487
+ body: reconnect
488
+ ? reconnectingEvents(response, reopen, reconnect, args.fetchOptions?.signal)
489
+ : readStream(response.body, declared.contentType),
490
+ headers: responseHeaders,
491
+ };
492
+ }
493
+ if (declared !== undefined && 'read' in declared) {
494
+ return {
495
+ status: response.status,
496
+ body: declared.read === 'bytes' ? new Uint8Array(await response.arrayBuffer()) : await response.text(),
397
497
  headers: responseHeaders,
398
498
  };
399
499
  }
@@ -849,7 +949,7 @@ const emitRoute = (routeKey, key, route, collector, namespaceName) => {
849
949
  const argsType = `{\n${indent(args.map((arg) => `${arg};`).join("\n"))}\n}`;
850
950
  const tableEntries = [`method: '${route.method}'`, `path: '${route.path}'`];
851
951
  if (route.contentType) tableEntries.push(`contentType: '${route.contentType}'`);
852
- const responses = Object.entries(route.responses).map(([status, response]) => isStreamResponse(response) ? `${status}: { stream: true, contentType: '${streamContentType(response)}' }` : `${status}: {}`);
952
+ const responses = Object.entries(route.responses).map(([status, response]) => isStreamResponse(response) ? `${status}: { stream: true, contentType: '${streamContentType(response)}' }` : `${status}: ${readEntry(response)}`);
853
953
  if (validationBody && !declaredBadRequest) responses.push("400: {}");
854
954
  tableEntries.push(`responses: {\n${indent(responses.map((entry) => `${entry},`).join("\n"))}\n}`);
855
955
  const streams = Object.values(route.responses).some((response) => isStreamResponse(response));
@@ -860,6 +960,16 @@ const emitRoute = (routeKey, key, route, collector, namespaceName) => {
860
960
  table: `${key}: {\n${indent(tableEntries.map((entry) => `${entry},`).join("\n"))}\n},`
861
961
  };
862
962
  };
963
+ /**
964
+ * How the client reads a body that does not stream: as bytes for a binary schema, as text for any other media type than
965
+ * JSON, as JSON otherwise.
966
+ */
967
+ const readEntry = (response) => {
968
+ const schema = resolveResponseBody(response);
969
+ if (schema && isBinarySchema(schema)) return `{ read: 'bytes' }`;
970
+ const contentType = resolveResponseContentType(response);
971
+ return contentType !== void 0 && !isJsonMediaType(contentType) ? `{ read: 'text' }` : "{}";
972
+ };
863
973
  const isRoute = (value) => !!value && typeof value === "object" && typeof value.method === "string";
864
974
  const emitTree = (routes, prefix, collector, namespaceName) => {
865
975
  const signatures = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kizunajs/fetch",
3
- "version": "2.0.0-beta.20",
3
+ "version": "2.0.0-beta.22",
4
4
  "description": "Fetch client generation for Kizuna.js APIs",
5
5
  "keywords": [
6
6
  "kizunajs",
@@ -56,12 +56,12 @@
56
56
  "access": "public"
57
57
  },
58
58
  "peerDependencies": {
59
- "kizunajs": "2.0.0-beta.20",
59
+ "kizunajs": "2.0.0-beta.22",
60
60
  "zod": "^4.0.0"
61
61
  },
62
62
  "devDependencies": {
63
- "kizunajs": "2.0.0-beta.20",
64
- "@kizunajs/express": "2.0.0-beta.20",
63
+ "kizunajs": "2.0.0-beta.22",
64
+ "@kizunajs/express": "2.0.0-beta.22",
65
65
  "@types/express": "^5.0.0",
66
66
  "express": "^5.0.0",
67
67
  "tsdown": "^0.21.0",