@routar/core 1.2.0 → 1.3.0
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 +147 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +162 -103
- package/dist/index.d.ts +162 -103
- package/dist/index.js +146 -100
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ interface ExecuteOptions {
|
|
|
6
6
|
params?: Record<string, unknown>;
|
|
7
7
|
body?: unknown;
|
|
8
8
|
/**
|
|
9
|
-
* Per-request headers injected by
|
|
9
|
+
* Per-request headers injected by a plugin's `onRequest` hook.
|
|
10
10
|
* Headers cannot be set from `createApi` call sites directly — use middleware
|
|
11
11
|
* to add dynamic headers such as `Authorization` or `X-Request-Id`.
|
|
12
12
|
*/
|
|
@@ -22,21 +22,51 @@ interface Executor {
|
|
|
22
22
|
execute(options: ExecuteOptions): Promise<unknown>;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* A named, composable unit of executor behavior.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Each lifecycle hook is optional — implement only what you need.
|
|
28
|
+
* Related concerns (e.g. auth header injection + 401 refresh) can be
|
|
29
|
+
* bundled into a single plugin.
|
|
30
30
|
*
|
|
31
|
-
* @example
|
|
31
|
+
* @example Auth plugin
|
|
32
32
|
* ```ts
|
|
33
|
-
* const
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* const authPlugin: ExecutorPlugin = {
|
|
34
|
+
* name: 'auth',
|
|
35
|
+
* onRequest: async (opts) => ({
|
|
36
|
+
* ...opts,
|
|
37
|
+
* headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
|
|
38
|
+
* }),
|
|
39
|
+
* onError: async (err) => {
|
|
40
|
+
* if (isUnauthorized(err)) await refreshToken();
|
|
41
|
+
* throw err;
|
|
42
|
+
* },
|
|
36
43
|
* };
|
|
37
44
|
* ```
|
|
38
45
|
*/
|
|
39
|
-
|
|
46
|
+
interface ExecutorPlugin {
|
|
47
|
+
/** Optional name — used for introspection and `eject`. */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Runs before the request is sent. Return modified opts to transform the request. */
|
|
50
|
+
onRequest?: (opts: ExecuteOptions) => ExecuteOptions | Promise<ExecuteOptions>;
|
|
51
|
+
/** Runs after a successful response. Return a modified value to transform the response. */
|
|
52
|
+
onResponse?: (response: unknown, opts: ExecuteOptions) => unknown | Promise<unknown>;
|
|
53
|
+
/** Runs when the request throws. Must re-throw (or throw a different error). */
|
|
54
|
+
onError?: (error: unknown, opts: ExecuteOptions) => never | Promise<never>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Options for {@link createExecutor}.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const executor = createExecutor(transport, {
|
|
62
|
+
* plugins: [authPlugin, logger()],
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
interface CreateExecutorOptions {
|
|
67
|
+
/** Plugins applied in declaration order (first plugin is outermost). */
|
|
68
|
+
plugins?: ExecutorPlugin[];
|
|
69
|
+
}
|
|
40
70
|
/**
|
|
41
71
|
* Any object with a `parse` method — compatible with Zod, Valibot, Yup, etc.
|
|
42
72
|
*
|
|
@@ -176,13 +206,38 @@ type ApiClient<TEndpoints extends RouterEndpoints> = {
|
|
|
176
206
|
* @param router - A {@link RouterDef} produced by {@link defineRouter}.
|
|
177
207
|
* @param options - Optional settings (e.g. `validate` to skip schema parsing in production).
|
|
178
208
|
*
|
|
179
|
-
* @example
|
|
209
|
+
* @example Basic usage
|
|
180
210
|
* ```ts
|
|
181
211
|
* const todoApi = createApi(executor, todoRouter);
|
|
182
212
|
* const todos = await todoApi.getList({});
|
|
183
213
|
* const todo = await todoApi.getDetail({ path: { id: 1 } });
|
|
214
|
+
* const next = await todoApi.create({ body: { title: 'buy milk' } });
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @example Nested router — access via dot notation
|
|
218
|
+
* ```ts
|
|
219
|
+
* const api = createApi(executor, apiRouter); // apiRouter has users → todos nesting
|
|
220
|
+
* await api.users.getList({});
|
|
221
|
+
* await api.users.todos.getList({});
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @example Cancel in-flight requests with AbortSignal
|
|
225
|
+
* ```ts
|
|
226
|
+
* const controller = new AbortController();
|
|
227
|
+
* const todos = await todoApi.getList({}, controller.signal);
|
|
228
|
+
* controller.abort();
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @example Extract types from the client — no duplication
|
|
232
|
+
* ```ts
|
|
233
|
+
* import type { ApiTypes } from '@routar/core';
|
|
234
|
+
* type TodoApiTypes = ApiTypes<typeof todoApi>;
|
|
235
|
+
* type Todo = TodoApiTypes['getDetail']['response'];
|
|
236
|
+
* type CreateRequest = TodoApiTypes['create']['request'];
|
|
237
|
+
* ```
|
|
184
238
|
*
|
|
185
|
-
*
|
|
239
|
+
* @example Skip response validation in production
|
|
240
|
+
* ```ts
|
|
186
241
|
* const prodApi = createApi(executor, todoRouter, {
|
|
187
242
|
* validate: { request: true, response: process.env.NODE_ENV !== 'production' },
|
|
188
243
|
* });
|
|
@@ -204,55 +259,43 @@ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executo
|
|
|
204
259
|
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
|
|
205
260
|
|
|
206
261
|
/**
|
|
207
|
-
* Creates an {@link Executor} by wrapping a transport function with
|
|
208
|
-
* optional middleware chain.
|
|
262
|
+
* Creates an {@link Executor} by wrapping a transport function with plugins.
|
|
209
263
|
*
|
|
210
|
-
*
|
|
211
|
-
* outermost wrapper and runs first on each request.
|
|
264
|
+
* Plugins run in declaration order (first plugin is outermost).
|
|
212
265
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
266
|
+
* For `retry` and `timeout`, use the options on {@link createFetchExecutor}
|
|
267
|
+
* or configure them via the underlying HTTP client (axios, ky).
|
|
215
268
|
*
|
|
216
269
|
* @example
|
|
217
270
|
* ```ts
|
|
218
|
-
* const executor = createExecutor(
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* return res.json();
|
|
222
|
-
* },
|
|
223
|
-
* [withTimeout(5000), withRetry(3), withLogger()],
|
|
224
|
-
* );
|
|
271
|
+
* const executor = createExecutor(transport, {
|
|
272
|
+
* plugins: [authPlugin, logger()],
|
|
273
|
+
* });
|
|
225
274
|
* ```
|
|
226
275
|
*/
|
|
227
|
-
declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>,
|
|
276
|
+
declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, options?: CreateExecutorOptions): Executor;
|
|
228
277
|
/**
|
|
229
278
|
* Creates an {@link Executor} that selects the underlying transport at
|
|
230
279
|
* request time based on the result of `resolver`.
|
|
231
280
|
*
|
|
232
|
-
* Use this to unify SSR and CSR behind a single API client — the resolver
|
|
233
|
-
* picks the right executor per request, so `createApi` is called once and
|
|
234
|
-
* works in both environments without duplicate `*ServerApi` instances.
|
|
235
|
-
*
|
|
236
|
-
* The resolver receives the full {@link ExecuteOptions} so it can branch on
|
|
237
|
-
* environment, URL prefix, auth context, or any runtime condition.
|
|
238
|
-
*
|
|
239
|
-
* @param resolver - Called on every request; returns the executor to delegate to.
|
|
240
|
-
*
|
|
241
281
|
* @example
|
|
242
282
|
* ```ts
|
|
243
|
-
* // SSR vs CSR — pick transport based on environment
|
|
244
283
|
* const apiExecutor = dispatchExecutor(() =>
|
|
245
284
|
* typeof window === 'undefined' ? serverExecutor : clientExecutor,
|
|
246
285
|
* );
|
|
247
|
-
*
|
|
248
|
-
* // Route by URL prefix — internal routes use a different transport
|
|
249
|
-
* const apiExecutor = dispatchExecutor((opts) =>
|
|
250
|
-
* opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,
|
|
251
|
-
* );
|
|
252
286
|
* ```
|
|
253
287
|
*/
|
|
254
288
|
declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
|
|
255
289
|
|
|
290
|
+
type FetchRetryOption = number | {
|
|
291
|
+
count: number;
|
|
292
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
293
|
+
};
|
|
294
|
+
interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
295
|
+
defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
296
|
+
retry?: FetchRetryOption;
|
|
297
|
+
timeout?: number;
|
|
298
|
+
}
|
|
256
299
|
/**
|
|
257
300
|
* Creates an {@link Executor} backed by the browser / Node.js `fetch` API.
|
|
258
301
|
*
|
|
@@ -268,9 +311,20 @@ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor):
|
|
|
268
311
|
* @param baseURL - Absolute base URL prepended to every endpoint path.
|
|
269
312
|
* @param options.defaultHeaders - Async factory called on every request to
|
|
270
313
|
* produce headers (e.g. reading cookies in a Next.js server component).
|
|
271
|
-
* @param options.
|
|
314
|
+
* @param options.plugins - Plugins applied around the fetch call. Each retry
|
|
315
|
+
* attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)
|
|
316
|
+
* and `onError` hooks. Token-refresh-on-401 patterns work by updating an
|
|
317
|
+
* external token store in `onError` and letting `onRequest` pick it up on
|
|
318
|
+
* the next attempt.
|
|
319
|
+
* @param options.retry - Number of retries, or `{ count, shouldRetry? }`.
|
|
320
|
+
* @param options.timeout - Per-attempt timeout in milliseconds.
|
|
321
|
+
*
|
|
322
|
+
* @example Minimal — no options needed
|
|
323
|
+
* ```ts
|
|
324
|
+
* const executor = createFetchExecutor('https://api.example.com');
|
|
325
|
+
* ```
|
|
272
326
|
*
|
|
273
|
-
* @example
|
|
327
|
+
* @example SSR with bearer token
|
|
274
328
|
* ```ts
|
|
275
329
|
* const executor = createFetchExecutor('https://api.example.com', {
|
|
276
330
|
* defaultHeaders: async () => {
|
|
@@ -279,11 +333,16 @@ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor):
|
|
|
279
333
|
* },
|
|
280
334
|
* });
|
|
281
335
|
* ```
|
|
336
|
+
*
|
|
337
|
+
* @example With retry and timeout
|
|
338
|
+
* ```ts
|
|
339
|
+
* const executor = createFetchExecutor('https://api.example.com', {
|
|
340
|
+
* retry: 2,
|
|
341
|
+
* timeout: 8_000,
|
|
342
|
+
* });
|
|
343
|
+
* ```
|
|
282
344
|
*/
|
|
283
|
-
declare function createFetchExecutor(baseURL: string, options?:
|
|
284
|
-
defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
285
|
-
middlewares?: ExecutorMiddleware[];
|
|
286
|
-
}): Executor;
|
|
345
|
+
declare function createFetchExecutor(baseURL: string, options?: FetchExecutorOptions): Executor;
|
|
287
346
|
/**
|
|
288
347
|
* Thrown by {@link createFetchExecutor} when the server returns a non-2xx
|
|
289
348
|
* status code.
|
|
@@ -300,19 +359,10 @@ declare function createFetchExecutor(baseURL: string, options?: {
|
|
|
300
359
|
* ```
|
|
301
360
|
*/
|
|
302
361
|
declare class HttpError extends Error {
|
|
303
|
-
/** HTTP status code (e.g. 404, 500). */
|
|
304
362
|
readonly status: number;
|
|
305
|
-
/** HTTP status text (e.g. "Not Found"). */
|
|
306
363
|
readonly statusText: string;
|
|
307
|
-
/** Parsed response body, or `null` if the body was empty or not JSON. */
|
|
308
364
|
readonly body: unknown;
|
|
309
|
-
constructor(
|
|
310
|
-
/** HTTP status code (e.g. 404, 500). */
|
|
311
|
-
status: number,
|
|
312
|
-
/** HTTP status text (e.g. "Not Found"). */
|
|
313
|
-
statusText: string,
|
|
314
|
-
/** Parsed response body, or `null` if the body was empty or not JSON. */
|
|
315
|
-
body?: unknown);
|
|
365
|
+
constructor(status: number, statusText: string, body?: unknown);
|
|
316
366
|
}
|
|
317
367
|
|
|
318
368
|
/**
|
|
@@ -345,7 +395,43 @@ type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]
|
|
|
345
395
|
* that `request` includes a matching `path` field with those param names.
|
|
346
396
|
* A mismatch or missing key is a compile-time error.
|
|
347
397
|
*
|
|
348
|
-
* @example
|
|
398
|
+
* @example Basic GET with no params
|
|
399
|
+
* ```ts
|
|
400
|
+
* const getList = endpoint({ method: 'GET', path: '/', response: z.array(TodoSchema) });
|
|
401
|
+
* ```
|
|
402
|
+
*
|
|
403
|
+
* @example GET with query params
|
|
404
|
+
* ```ts
|
|
405
|
+
* const search = endpoint({
|
|
406
|
+
* method: 'GET',
|
|
407
|
+
* path: '/search',
|
|
408
|
+
* request: z.object({ query: z.object({ q: z.string(), limit: z.number().optional() }) }),
|
|
409
|
+
* response: z.array(TodoSchema),
|
|
410
|
+
* });
|
|
411
|
+
* ```
|
|
412
|
+
*
|
|
413
|
+
* @example POST with body
|
|
414
|
+
* ```ts
|
|
415
|
+
* const create = endpoint({
|
|
416
|
+
* method: 'POST',
|
|
417
|
+
* path: '/',
|
|
418
|
+
* request: z.object({ body: z.object({ title: z.string() }) }),
|
|
419
|
+
* response: TodoSchema,
|
|
420
|
+
* });
|
|
421
|
+
* ```
|
|
422
|
+
*
|
|
423
|
+
* @example Adapter — raw is inferred from the response schema, no cast needed
|
|
424
|
+
* ```ts
|
|
425
|
+
* const getDetail = endpoint({
|
|
426
|
+
* method: 'GET',
|
|
427
|
+
* path: '/:id',
|
|
428
|
+
* request: z.object({ path: z.object({ id: z.number() }) }),
|
|
429
|
+
* response: TodoRawSchema,
|
|
430
|
+
* adapter: (raw) => ({ ...raw, label: `#${raw.id} ${raw.title}` }),
|
|
431
|
+
* });
|
|
432
|
+
* ```
|
|
433
|
+
*
|
|
434
|
+
* @example Path param enforcement
|
|
349
435
|
* ```ts
|
|
350
436
|
* // ✅ path has ':id' → request.path.id is required
|
|
351
437
|
* const getDetail = endpoint({
|
|
@@ -353,7 +439,6 @@ type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]
|
|
|
353
439
|
* path: '/:id',
|
|
354
440
|
* request: z.object({ path: z.object({ id: z.number() }) }),
|
|
355
441
|
* response: TodoSchema,
|
|
356
|
-
* adapter: toTodoItem,
|
|
357
442
|
* });
|
|
358
443
|
*
|
|
359
444
|
* // ❌ compile error — 'id' is missing from request.path
|
|
@@ -445,57 +530,29 @@ declare function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints>
|
|
|
445
530
|
declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
|
|
446
531
|
|
|
447
532
|
/**
|
|
448
|
-
* Thrown by
|
|
449
|
-
* Distinguishable from a user-initiated
|
|
533
|
+
* Thrown by the built-in `timeout` option when a request exceeds the
|
|
534
|
+
* configured duration. Distinguishable from a user-initiated
|
|
535
|
+
* {@link AbortSignal} cancellation.
|
|
450
536
|
*/
|
|
451
537
|
declare class TimeoutError extends Error {
|
|
452
538
|
readonly ms: number;
|
|
453
539
|
constructor(ms: number);
|
|
454
540
|
}
|
|
455
541
|
/**
|
|
456
|
-
* Identity helper that returns the
|
|
457
|
-
*
|
|
458
|
-
* Wrap your middleware function with this to get full type inference on `opts`
|
|
459
|
-
* and `next` without having to annotate the type manually.
|
|
542
|
+
* Identity helper that returns the plugin as-is, providing full type inference.
|
|
460
543
|
*
|
|
461
544
|
* @example
|
|
462
545
|
* ```ts
|
|
463
|
-
* const
|
|
464
|
-
*
|
|
465
|
-
* )
|
|
466
|
-
*
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
* Retries a failed request up to `count` additional times.
|
|
471
|
-
*
|
|
472
|
-
* By default all errors trigger a retry. Pass `shouldRetry` to skip retries
|
|
473
|
-
* for non-transient errors (e.g. 4xx responses).
|
|
474
|
-
*
|
|
475
|
-
* @param count - Number of retries (not counting the initial attempt).
|
|
476
|
-
* @param options.shouldRetry - Return `false` to stop retrying early.
|
|
477
|
-
* Receives the error and a zero-based `attempt` index (0 = first failure,
|
|
478
|
-
* 1 = second failure, …) so you can limit retries by count or error type.
|
|
479
|
-
*
|
|
480
|
-
* @example
|
|
481
|
-
* ```ts
|
|
482
|
-
* withRetry(3, {
|
|
483
|
-
* shouldRetry: (err) => err instanceof HttpError && err.status >= 500,
|
|
484
|
-
* })
|
|
546
|
+
* const authPlugin = definePlugin({
|
|
547
|
+
* name: 'auth',
|
|
548
|
+
* onRequest: async (opts) => ({
|
|
549
|
+
* ...opts,
|
|
550
|
+
* headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
|
|
551
|
+
* }),
|
|
552
|
+
* });
|
|
485
553
|
* ```
|
|
486
554
|
*/
|
|
487
|
-
declare function
|
|
488
|
-
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
489
|
-
}): ExecutorMiddleware;
|
|
490
|
-
/**
|
|
491
|
-
* Aborts a request if it does not complete within `ms` milliseconds.
|
|
492
|
-
*
|
|
493
|
-
* Merges the timeout signal with any existing `AbortSignal` on the request,
|
|
494
|
-
* so whichever fires first wins.
|
|
495
|
-
*
|
|
496
|
-
* @param ms - Timeout in milliseconds.
|
|
497
|
-
*/
|
|
498
|
-
declare function withTimeout(ms: number): ExecutorMiddleware;
|
|
555
|
+
declare function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin;
|
|
499
556
|
/**
|
|
500
557
|
* Logs each request and its outcome (success duration or error).
|
|
501
558
|
*
|
|
@@ -503,12 +560,14 @@ declare function withTimeout(ms: number): ExecutorMiddleware;
|
|
|
503
560
|
*
|
|
504
561
|
* @example
|
|
505
562
|
* ```ts
|
|
506
|
-
*
|
|
563
|
+
* createExecutor(transport, {
|
|
564
|
+
* plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],
|
|
565
|
+
* })
|
|
507
566
|
* ```
|
|
508
567
|
*/
|
|
509
|
-
declare function
|
|
568
|
+
declare function logger(options?: {
|
|
510
569
|
log?: (message: string, data?: unknown) => void;
|
|
511
|
-
}):
|
|
570
|
+
}): ExecutorPlugin;
|
|
512
571
|
|
|
513
572
|
declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
|
|
514
573
|
|
|
@@ -527,4 +586,4 @@ declare class ValidationError extends Error {
|
|
|
527
586
|
constructor(message: string, cause?: unknown);
|
|
528
587
|
}
|
|
529
588
|
|
|
530
|
-
export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type
|
|
589
|
+
export { type ApiTypes, type CreateApiOptions, type CreateExecutorOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorPlugin, type FetchExecutorOptions, type FetchRetryOption, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, serializeParams };
|
package/dist/index.js
CHANGED
|
@@ -114,83 +114,35 @@ function buildEndpointFn(executor, prefix, spec, options) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// src/create-executor.ts
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// src/utils/params.ts
|
|
128
|
-
function serializeParams(params) {
|
|
129
|
-
const result = new URLSearchParams();
|
|
130
|
-
for (const [key, value] of Object.entries(params)) {
|
|
131
|
-
if (value == null) continue;
|
|
132
|
-
if (Array.isArray(value)) {
|
|
133
|
-
for (const item of value) {
|
|
134
|
-
if (item != null) result.append(key, String(item));
|
|
117
|
+
function pluginToMiddleware(plugin) {
|
|
118
|
+
return async (opts, next) => {
|
|
119
|
+
const resolvedOpts = plugin.onRequest ? await plugin.onRequest(opts) : opts;
|
|
120
|
+
try {
|
|
121
|
+
const response = await next(resolvedOpts);
|
|
122
|
+
return plugin.onResponse ? await plugin.onResponse(response, resolvedOpts) : response;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (plugin.onError) {
|
|
125
|
+
await plugin.onError(err, resolvedOpts);
|
|
126
|
+
throw err;
|
|
135
127
|
}
|
|
136
|
-
|
|
137
|
-
throw new TypeError(
|
|
138
|
-
`serializeParams: value for key "${key}" is a plain object. Serialize it to a string before passing as a query parameter.`
|
|
139
|
-
);
|
|
140
|
-
} else {
|
|
141
|
-
result.append(key, String(value));
|
|
128
|
+
throw err;
|
|
142
129
|
}
|
|
143
|
-
}
|
|
144
|
-
return result;
|
|
130
|
+
};
|
|
145
131
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
async ({ method, url, params, body, headers, signal }) => {
|
|
151
|
-
const fullURL = new URL(baseURL.replace(/\/$/, "") + url);
|
|
152
|
-
if (params) {
|
|
153
|
-
serializeParams(params).forEach((v, k) => {
|
|
154
|
-
fullURL.searchParams.set(k, v);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
const defaultHeaders = await options?.defaultHeaders?.() ?? {};
|
|
158
|
-
const res = await fetch(fullURL.toString(), {
|
|
159
|
-
method,
|
|
160
|
-
headers: {
|
|
161
|
-
...defaultHeaders,
|
|
162
|
-
...headers,
|
|
163
|
-
...body != null ? { "Content-Type": "application/json" } : {}
|
|
164
|
-
},
|
|
165
|
-
body: body != null ? JSON.stringify(body) : void 0,
|
|
166
|
-
signal
|
|
167
|
-
});
|
|
168
|
-
if (!res.ok) {
|
|
169
|
-
const errorBody = await res.json().catch(() => null);
|
|
170
|
-
throw new HttpError(res.status, res.statusText, errorBody);
|
|
171
|
-
}
|
|
172
|
-
if (res.status === 204 || res.status === 205 || res.status === 304) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
const text = await res.text();
|
|
176
|
-
return text === "" ? null : JSON.parse(text);
|
|
177
|
-
},
|
|
178
|
-
options?.middlewares
|
|
132
|
+
function buildChain(execute, middlewares) {
|
|
133
|
+
return middlewares.reduceRight(
|
|
134
|
+
(next, mw) => (opts) => mw(opts, next),
|
|
135
|
+
execute
|
|
179
136
|
);
|
|
180
137
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// src/define-endpoint.ts
|
|
192
|
-
function endpoint(spec) {
|
|
193
|
-
return spec;
|
|
138
|
+
function createExecutor(execute, options = {}) {
|
|
139
|
+
const middlewares = (options.plugins ?? []).map(pluginToMiddleware);
|
|
140
|
+
return { execute: buildChain(execute, middlewares) };
|
|
141
|
+
}
|
|
142
|
+
function dispatchExecutor(resolver) {
|
|
143
|
+
return {
|
|
144
|
+
execute: (opts) => resolver(opts).execute(opts)
|
|
145
|
+
};
|
|
194
146
|
}
|
|
195
147
|
|
|
196
148
|
// src/middleware.ts
|
|
@@ -201,11 +153,41 @@ var TimeoutError = class extends Error {
|
|
|
201
153
|
this.name = "TimeoutError";
|
|
202
154
|
}
|
|
203
155
|
};
|
|
204
|
-
function
|
|
205
|
-
return
|
|
156
|
+
function definePlugin(plugin) {
|
|
157
|
+
return plugin;
|
|
158
|
+
}
|
|
159
|
+
function logger(options) {
|
|
160
|
+
const log = options?.log ?? ((msg, data) => console.log(msg, data));
|
|
161
|
+
const timings = /* @__PURE__ */ new WeakMap();
|
|
162
|
+
return definePlugin({
|
|
163
|
+
name: "logger",
|
|
164
|
+
onRequest: (opts) => {
|
|
165
|
+
timings.set(opts, Date.now());
|
|
166
|
+
log(`[routar] ${opts.method} ${opts.url}`, {
|
|
167
|
+
params: opts.params,
|
|
168
|
+
body: opts.body
|
|
169
|
+
});
|
|
170
|
+
return opts;
|
|
171
|
+
},
|
|
172
|
+
onResponse: (res, opts) => {
|
|
173
|
+
log(
|
|
174
|
+
`[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - (timings.get(opts) ?? Date.now())}ms`
|
|
175
|
+
);
|
|
176
|
+
timings.delete(opts);
|
|
177
|
+
return res;
|
|
178
|
+
},
|
|
179
|
+
onError: (err, opts) => {
|
|
180
|
+
log(
|
|
181
|
+
`[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,
|
|
182
|
+
err
|
|
183
|
+
);
|
|
184
|
+
timings.delete(opts);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
206
188
|
}
|
|
207
189
|
function withRetry(count, options) {
|
|
208
|
-
return
|
|
190
|
+
return async (opts, next) => {
|
|
209
191
|
let lastError;
|
|
210
192
|
for (let attempt = 0; attempt <= count; attempt++) {
|
|
211
193
|
try {
|
|
@@ -217,10 +199,10 @@ function withRetry(count, options) {
|
|
|
217
199
|
}
|
|
218
200
|
}
|
|
219
201
|
throw lastError;
|
|
220
|
-
}
|
|
202
|
+
};
|
|
221
203
|
}
|
|
222
204
|
function withTimeout(ms) {
|
|
223
|
-
return
|
|
205
|
+
return async (opts, next) => {
|
|
224
206
|
const controller = new AbortController();
|
|
225
207
|
const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
|
|
226
208
|
const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
|
|
@@ -231,28 +213,7 @@ function withTimeout(ms) {
|
|
|
231
213
|
clearTimeout(timer);
|
|
232
214
|
cleanup();
|
|
233
215
|
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function withLogger(options) {
|
|
237
|
-
const log = options?.log ?? ((msg, data) => console.log(msg, data));
|
|
238
|
-
return defineMiddleware(async (opts, next) => {
|
|
239
|
-
const start = Date.now();
|
|
240
|
-
log(`[routar] ${opts.method} ${opts.url}`, {
|
|
241
|
-
params: opts.params,
|
|
242
|
-
body: opts.body
|
|
243
|
-
});
|
|
244
|
-
try {
|
|
245
|
-
const result = await next(opts);
|
|
246
|
-
log(`[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - start}ms`);
|
|
247
|
-
return result;
|
|
248
|
-
} catch (err) {
|
|
249
|
-
log(
|
|
250
|
-
`[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`,
|
|
251
|
-
err
|
|
252
|
-
);
|
|
253
|
-
throw err;
|
|
254
|
-
}
|
|
255
|
-
});
|
|
216
|
+
};
|
|
256
217
|
}
|
|
257
218
|
function anySignal(signals) {
|
|
258
219
|
const controller = new AbortController();
|
|
@@ -274,6 +235,91 @@ function anySignal(signals) {
|
|
|
274
235
|
};
|
|
275
236
|
}
|
|
276
237
|
|
|
277
|
-
|
|
238
|
+
// src/utils/params.ts
|
|
239
|
+
function serializeParams(params) {
|
|
240
|
+
const result = new URLSearchParams();
|
|
241
|
+
for (const [key, value] of Object.entries(params)) {
|
|
242
|
+
if (value == null) continue;
|
|
243
|
+
if (Array.isArray(value)) {
|
|
244
|
+
for (const item of value) {
|
|
245
|
+
if (item != null) result.append(key, String(item));
|
|
246
|
+
}
|
|
247
|
+
} else if (typeof value === "object") {
|
|
248
|
+
throw new TypeError(
|
|
249
|
+
`serializeParams: value for key "${key}" is a plain object. Serialize it to a string before passing as a query parameter.`
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
result.append(key, String(value));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/create-fetch-executor.ts
|
|
259
|
+
function buildFetchChain(transport, retry, timeout) {
|
|
260
|
+
const middlewares = [
|
|
261
|
+
...retry != null ? [
|
|
262
|
+
typeof retry === "number" ? withRetry(retry) : withRetry(retry.count, { shouldRetry: retry.shouldRetry })
|
|
263
|
+
] : [],
|
|
264
|
+
...timeout != null ? [withTimeout(timeout)] : []
|
|
265
|
+
];
|
|
266
|
+
if (middlewares.length === 0) return transport;
|
|
267
|
+
return buildChain(transport, middlewares);
|
|
268
|
+
}
|
|
269
|
+
function createFetchExecutor(baseURL, options) {
|
|
270
|
+
const transport = async ({
|
|
271
|
+
method,
|
|
272
|
+
url,
|
|
273
|
+
params,
|
|
274
|
+
body,
|
|
275
|
+
headers,
|
|
276
|
+
signal
|
|
277
|
+
}) => {
|
|
278
|
+
const fullURL = new URL(baseURL.replace(/\/$/, "") + url);
|
|
279
|
+
if (params) {
|
|
280
|
+
serializeParams(params).forEach((v, k) => {
|
|
281
|
+
fullURL.searchParams.set(k, v);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const defaultHeaders = await options?.defaultHeaders?.() ?? {};
|
|
285
|
+
const res = await fetch(fullURL.toString(), {
|
|
286
|
+
method,
|
|
287
|
+
headers: {
|
|
288
|
+
...defaultHeaders,
|
|
289
|
+
...headers,
|
|
290
|
+
...body != null ? { "Content-Type": "application/json" } : {}
|
|
291
|
+
},
|
|
292
|
+
body: body != null ? JSON.stringify(body) : void 0,
|
|
293
|
+
signal
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
const errorBody = await res.json().catch(() => null);
|
|
297
|
+
throw new HttpError(res.status, res.statusText, errorBody);
|
|
298
|
+
}
|
|
299
|
+
if (res.status === 204 || res.status === 205 || res.status === 304) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const text = await res.text();
|
|
303
|
+
return text === "" ? null : JSON.parse(text);
|
|
304
|
+
};
|
|
305
|
+
const executor = createExecutor(transport, { plugins: options?.plugins });
|
|
306
|
+
return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };
|
|
307
|
+
}
|
|
308
|
+
var HttpError = class extends Error {
|
|
309
|
+
constructor(status, statusText, body = null) {
|
|
310
|
+
super(`HTTP ${status}: ${statusText}`);
|
|
311
|
+
this.status = status;
|
|
312
|
+
this.statusText = statusText;
|
|
313
|
+
this.body = body;
|
|
314
|
+
this.name = "HttpError";
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/define-endpoint.ts
|
|
319
|
+
function endpoint(spec) {
|
|
320
|
+
return spec;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export { HttpError, TimeoutError, ValidationError, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, serializeParams };
|
|
278
324
|
//# sourceMappingURL=index.js.map
|
|
279
325
|
//# sourceMappingURL=index.js.map
|