@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.
- package/dist/index.cjs +140 -30
- package/dist/index.mjs +141 -31
- 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
|
-
|
|
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))
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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:
|
|
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
|
-
|
|
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))
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
59
|
+
"kizunajs": "2.0.0-beta.22",
|
|
60
60
|
"zod": "^4.0.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"kizunajs": "2.0.0-beta.
|
|
64
|
-
"@kizunajs/express": "2.0.0-beta.
|
|
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",
|