@routar/core 0.1.0 → 1.1.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/README.md CHANGED
@@ -8,4 +8,4 @@ Core package for [routar](https://github.com/kbmin1129/routar) — endpoint defi
8
8
  npm install @routar/core
9
9
  ```
10
10
 
11
- See the [main README](https://github.com/kbmin1129/routar) for full documentation.
11
+ See the [documentation](https://routar.vercel.app) or the [main README](https://github.com/minr2kb/routar) for full documentation.
package/dist/index.cjs CHANGED
@@ -1,15 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  // src/define-router.ts
4
+ function isRouterDef(entry) {
5
+ return "prefix" in entry && "endpoints" in entry;
6
+ }
4
7
  function defineRouter(prefix, endpoints) {
5
8
  return { prefix, endpoints };
6
9
  }
7
10
 
8
- // src/define-endpoint.ts
9
- function endpoint(spec) {
10
- return spec;
11
- }
12
-
13
11
  // src/utils/path.ts
14
12
  function joinPaths(...segments) {
15
13
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -19,7 +17,7 @@ function resolvePath(pathTemplate, params) {
19
17
  if (!params) return pathTemplate;
20
18
  return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {
21
19
  const value = params[key];
22
- if (value == null) throw new Error(`Missing path parameter: ${key}`);
20
+ if (value == null || value === "") throw new Error(`Missing path parameter: ${key}`);
23
21
  return encodeURIComponent(String(value));
24
22
  });
25
23
  }
@@ -28,89 +26,119 @@ function resolvePath(pathTemplate, params) {
28
26
  var ValidationError = class extends Error {
29
27
  constructor(message, cause) {
30
28
  super(message);
31
- this.cause = cause;
32
29
  this.name = "ValidationError";
33
30
  if (cause !== void 0) {
34
31
  Object.defineProperty(this, "cause", {
35
32
  value: cause,
36
- writable: true,
37
- enumerable: true
33
+ writable: false,
34
+ enumerable: false,
35
+ configurable: true
38
36
  });
39
37
  }
40
38
  }
41
39
  };
42
40
 
43
41
  // src/create-api.ts
44
- function createApi(executor, routerOrPrefixOrEndpoints, endpointsArg) {
45
- let prefix;
46
- let endpoints;
47
- if (typeof routerOrPrefixOrEndpoints === "string") {
48
- prefix = routerOrPrefixOrEndpoints;
49
- if (!endpointsArg) throw new Error("endpoints is required when prefix is provided");
50
- endpoints = endpointsArg;
51
- } else if ("prefix" in routerOrPrefixOrEndpoints && "endpoints" in routerOrPrefixOrEndpoints) {
52
- prefix = routerOrPrefixOrEndpoints.prefix;
53
- endpoints = routerOrPrefixOrEndpoints.endpoints;
54
- } else {
55
- prefix = "";
56
- endpoints = routerOrPrefixOrEndpoints;
42
+ function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
43
+ const { prefix, endpoints, options } = resolveArgs(
44
+ routerOrPrefixOrEndpoints,
45
+ endpointsArgOrOptions,
46
+ optionsArg
47
+ );
48
+ return buildClient(executor, prefix, endpoints, options);
49
+ }
50
+ function resolveArgs(second, third, fourth) {
51
+ if (typeof second === "string") {
52
+ if (!third)
53
+ throw new Error("endpoints is required when prefix is provided");
54
+ return {
55
+ prefix: second,
56
+ endpoints: third,
57
+ options: fourth
58
+ };
57
59
  }
58
- return buildClient(executor, prefix, endpoints);
60
+ if (isRouterDef(second)) {
61
+ return {
62
+ prefix: second.prefix,
63
+ endpoints: second.endpoints,
64
+ options: third
65
+ };
66
+ }
67
+ return {
68
+ prefix: "",
69
+ endpoints: second,
70
+ options: third
71
+ };
72
+ }
73
+ function shouldValidate(options, kind) {
74
+ const v = options?.validate;
75
+ if (v === void 0 || v === true) return true;
76
+ if (v === false) return false;
77
+ return v[kind] ?? true;
59
78
  }
60
- function buildClient(executor, prefix, endpoints) {
79
+ function buildClient(executor, prefix, endpoints, options) {
61
80
  const client = {};
62
81
  for (const [key, entry] of Object.entries(endpoints)) {
63
- if ("prefix" in entry && "endpoints" in entry) {
64
- const nested = entry;
65
- client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);
66
- } else {
67
- const spec = entry;
68
- client[key] = async (params = {}, signal) => {
69
- let validatedParams = params;
70
- if (spec.request) {
71
- try {
72
- validatedParams = spec.request.parse(params);
73
- } catch (err) {
74
- throw new ValidationError("Request validation failed", err);
75
- }
76
- }
77
- const url = resolvePath(
78
- joinPaths(prefix, spec.path),
79
- validatedParams?.path
80
- );
81
- const raw = await executor.execute({
82
- method: spec.method,
83
- url,
84
- params: validatedParams?.query,
85
- body: validatedParams?.body,
86
- signal
87
- });
88
- let validated;
89
- try {
90
- validated = spec.response.parse(raw);
91
- } catch (err) {
92
- throw new ValidationError("Response validation failed", err);
93
- }
94
- if (spec.adapter) {
95
- return spec.adapter(validated);
96
- }
97
- return validated;
98
- };
99
- }
82
+ client[key] = isRouterDef(entry) ? buildClient(executor, joinPaths(prefix, entry.prefix), entry.endpoints, options) : buildEndpointFn(executor, prefix, entry, options);
100
83
  }
101
84
  return client;
102
85
  }
86
+ function buildEndpointFn(executor, prefix, spec, options) {
87
+ return async (params = {}, signal) => {
88
+ let validatedParams = params;
89
+ if (spec.request && shouldValidate(options, "request")) {
90
+ try {
91
+ validatedParams = spec.request.parse(params);
92
+ } catch (err) {
93
+ throw new ValidationError("Request validation failed", err);
94
+ }
95
+ }
96
+ const url = resolvePath(joinPaths(prefix, spec.path), validatedParams?.path);
97
+ const raw = await executor.execute({
98
+ method: spec.method,
99
+ url,
100
+ params: validatedParams?.query,
101
+ body: validatedParams?.body,
102
+ signal
103
+ });
104
+ let result;
105
+ if (shouldValidate(options, "response")) {
106
+ try {
107
+ result = spec.response.parse(raw);
108
+ } catch (err) {
109
+ throw new ValidationError("Response validation failed", err);
110
+ }
111
+ } else {
112
+ result = raw;
113
+ }
114
+ return spec.adapter ? spec.adapter(result) : result;
115
+ };
116
+ }
103
117
 
104
118
  // src/create-executor.ts
105
119
  function createExecutor(execute, middlewares = []) {
106
- const chain = middlewares.reduceRight(
107
- (next, mw) => (opts) => mw(opts, next),
108
- execute
109
- );
120
+ const chain = middlewares.reduceRight((next, mw) => (opts) => mw(opts, next), execute);
110
121
  return { execute: chain };
111
122
  }
123
+ function dispatchExecutor(resolver) {
124
+ return {
125
+ execute: (opts) => resolver(opts).execute(opts)
126
+ };
127
+ }
128
+
129
+ // src/define-endpoint.ts
130
+ function endpoint(spec) {
131
+ return spec;
132
+ }
112
133
 
113
134
  // src/middleware.ts
135
+ var TimeoutError = class extends Error {
136
+ constructor(ms) {
137
+ super(`Request timed out after ${ms}ms`);
138
+ this.ms = ms;
139
+ this.name = "TimeoutError";
140
+ }
141
+ };
114
142
  function defineMiddleware(fn) {
115
143
  return fn;
116
144
  }
@@ -132,12 +160,14 @@ function withRetry(count, options) {
132
160
  function withTimeout(ms) {
133
161
  return defineMiddleware(async (opts, next) => {
134
162
  const controller = new AbortController();
135
- const timer = setTimeout(() => controller.abort(), ms);
136
- const signal = opts.signal ? anySignal([opts.signal, controller.signal]) : controller.signal;
163
+ const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
164
+ const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
165
+ } };
137
166
  try {
138
167
  return await next({ ...opts, signal });
139
168
  } finally {
140
169
  clearTimeout(timer);
170
+ cleanup();
141
171
  }
142
172
  });
143
173
  }
@@ -145,27 +175,41 @@ function withLogger(options) {
145
175
  const log = options?.log ?? ((msg, data) => console.log(msg, data));
146
176
  return defineMiddleware(async (opts, next) => {
147
177
  const start = Date.now();
148
- log(`[routar] ${opts.method} ${opts.url}`, { params: opts.params, body: opts.body });
178
+ log(`[routar] ${opts.method} ${opts.url}`, {
179
+ params: opts.params,
180
+ body: opts.body
181
+ });
149
182
  try {
150
183
  const result = await next(opts);
151
184
  log(`[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - start}ms`);
152
185
  return result;
153
186
  } catch (err) {
154
- log(`[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`, err);
187
+ log(
188
+ `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`,
189
+ err
190
+ );
155
191
  throw err;
156
192
  }
157
193
  });
158
194
  }
159
195
  function anySignal(signals) {
160
196
  const controller = new AbortController();
161
- for (const signal of signals) {
162
- if (signal.aborted) {
197
+ const onAbort = () => controller.abort();
198
+ const attached = [];
199
+ for (const s of signals) {
200
+ if (s.aborted) {
163
201
  controller.abort();
164
- return controller.signal;
202
+ break;
165
203
  }
166
- signal.addEventListener("abort", () => controller.abort(), { once: true });
204
+ s.addEventListener("abort", onAbort, { once: true });
205
+ attached.push(s);
167
206
  }
168
- return controller.signal;
207
+ return {
208
+ signal: controller.signal,
209
+ cleanup: () => attached.forEach((s) => {
210
+ s.removeEventListener("abort", onAbort);
211
+ })
212
+ };
169
213
  }
170
214
 
171
215
  // src/utils/params.ts
@@ -177,6 +221,10 @@ function serializeParams(params) {
177
221
  for (const item of value) {
178
222
  if (item != null) result.append(key, String(item));
179
223
  }
224
+ } else if (typeof value === "object") {
225
+ throw new TypeError(
226
+ `serializeParams: value for key "${key}" is a plain object. Serialize it to a string before passing as a query parameter.`
227
+ );
180
228
  } else {
181
229
  result.append(key, String(value));
182
230
  }
@@ -184,11 +232,13 @@ function serializeParams(params) {
184
232
  return result;
185
233
  }
186
234
 
235
+ exports.TimeoutError = TimeoutError;
187
236
  exports.ValidationError = ValidationError;
188
237
  exports.createApi = createApi;
189
238
  exports.createExecutor = createExecutor;
190
239
  exports.defineMiddleware = defineMiddleware;
191
240
  exports.defineRouter = defineRouter;
241
+ exports.dispatchExecutor = dispatchExecutor;
192
242
  exports.endpoint = endpoint;
193
243
  exports.joinPaths = joinPaths;
194
244
  exports.resolvePath = resolvePath;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/define-router.ts","../src/define-endpoint.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { RouterDef, RouterEndpoints } from './types.js';\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from './types.js';\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> =\n [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * @example\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * adapter: toTodoItem,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n","export function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter(s => s !== '')\n .join('/')\n .replace(/\\/+/g, '/');\n return joined.endsWith('/') && joined.length > 1\n ? joined.slice(0, -1)\n : joined || '/';\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null) throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = 'ValidationError';\n if (cause !== undefined) {\n Object.defineProperty(this, 'cause', {\n value: cause,\n writable: true,\n enumerable: true,\n });\n }\n }\n}\n","import type {\n Executor,\n RouterDef,\n RouterEndpoints,\n EndpointSpec,\n InferResponse,\n RequestShape,\n} from './types.js';\nimport { joinPaths, resolvePath } from './utils/path.js';\nimport { ValidationError } from './utils/validate.js';\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (\n params: TSpec['request'] extends { parse: (data: unknown) => infer R } ? R : RequestShape,\n signal?: AbortSignal,\n) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\ntype ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints>\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n *\n * @example\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n): ApiClient<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints: RouterDef<any> | RouterEndpoints | string,\n endpointsArg?: RouterEndpoints,\n): Record<string, any> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n\n if (typeof routerOrPrefixOrEndpoints === 'string') {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArg) throw new Error('endpoints is required when prefix is provided');\n endpoints = endpointsArg;\n } else if (\n 'prefix' in routerOrPrefixOrEndpoints &&\n 'endpoints' in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<any>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<any>).endpoints;\n } else {\n prefix = '';\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n }\n\n return buildClient(executor, prefix, endpoints);\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n): Record<string, any> {\n const client: Record<string, any> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n if ('prefix' in entry && 'endpoints' in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<any>;\n client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);\n } else {\n // Leaf EndpointSpec\n const spec = entry as EndpointSpec<any, any, any>;\n client[key] = async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError('Request validation failed', err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let validated: unknown;\n try {\n validated = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError('Response validation failed', err);\n }\n\n if (spec.adapter) {\n return spec.adapter(validated as any);\n }\n return validated;\n };\n }\n }\n\n return client;\n}\n","import type { Executor, ExecuteOptions, ExecutorMiddleware } from './types.js';\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with an\n * optional middleware chain.\n *\n * Middlewares are applied in declaration order — the first middleware is the\n * outermost wrapper and runs first on each request.\n *\n * @param execute - The underlying transport function (fetch, axios, etc.).\n * @param middlewares - Ordered list of middlewares to apply.\n *\n * @example\n * ```ts\n * const executor = createExecutor(\n * async ({ method, url, body }) => {\n * const res = await fetch(url, { method, body: JSON.stringify(body) });\n * return res.json();\n * },\n * [withTimeout(5000), withRetry(3), withLogger()],\n * );\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[] = [],\n): Executor {\n const chain = middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n return { execute: chain };\n}\n","import type { ExecutorMiddleware } from './types.js';\n\n/**\n * Identity helper that returns the middleware as-is.\n *\n * Wrap your middleware function with this to get full type inference on `opts`\n * and `next` without having to annotate the type manually.\n *\n * @example\n * ```ts\n * const withCorrelationId = defineMiddleware((opts, next) =>\n * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })\n * );\n * ```\n */\nexport function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware {\n return fn;\n}\n\n/**\n * Retries a failed request up to `count` additional times.\n *\n * By default all errors trigger a retry. Pass `shouldRetry` to skip retries\n * for non-transient errors (e.g. 4xx responses).\n *\n * @param count - Number of retries (not counting the initial attempt).\n * @param options.shouldRetry - Return `false` to stop retrying early.\n *\n * @example\n * ```ts\n * withRetry(3, {\n * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,\n * })\n * ```\n */\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n });\n}\n\n/**\n * Aborts a request if it does not complete within `ms` milliseconds.\n *\n * Merges the timeout signal with any existing `AbortSignal` on the request,\n * so whichever fires first wins.\n *\n * @param ms - Timeout in milliseconds.\n */\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n const signal = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : controller.signal;\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * withLogger({ log: (msg, data) => logger.debug(msg, data) })\n * ```\n */\nexport function withLogger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorMiddleware {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n return defineMiddleware(async (opts, next) => {\n const start = Date.now();\n log(`[routar] ${opts.method} ${opts.url}`, { params: opts.params, body: opts.body });\n try {\n const result = await next(opts);\n log(`[routar] ${opts.method} ${opts.url} — ${Date.now() - start}ms`);\n return result;\n } catch (err) {\n log(`[routar] ${opts.method} ${opts.url} — error after ${Date.now() - start}ms`, err);\n throw err;\n }\n });\n}\n\n/** Combines multiple AbortSignals into one that aborts when any of them fire. */\nfunction anySignal(signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","export function serializeParams(params: Record<string, unknown>): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n"]}
1
+ {"version":3,"sources":["../src/define-router.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/define-endpoint.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\n/** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */\nexport function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints> {\n return \"prefix\" in entry && \"endpoints\" in entry;\n}\n\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","/**\n * Joins URL path segments, normalising repeated slashes and trailing slashes.\n *\n * **Note:** Intended for relative API paths only. Absolute URLs containing\n * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs\n * directly to the executor instead of through this helper.\n */\nexport function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null || value === \"\") throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n readonly cause?: unknown;\n\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: false,\n enumerable: false,\n configurable: true,\n });\n }\n }\n}\n","import { isRouterDef } from \"./define-router.js\";\nimport type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n ValidatorOutput,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> =\n TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>>\n : (\n params?: RequestShape,\n signal?: AbortSignal,\n ) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\ntype ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n *\n * // Skip response validation in production\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints:\n | RouterDef<RouterEndpoints>\n | RouterEndpoints\n | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n const { prefix, endpoints, options } = resolveArgs(\n routerOrPrefixOrEndpoints,\n endpointsArgOrOptions,\n optionsArg,\n );\n return buildClient(executor, prefix, endpoints, options);\n}\n\nfunction resolveArgs(\n second: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n third: RouterEndpoints | CreateApiOptions | undefined,\n fourth: CreateApiOptions | undefined,\n): {\n prefix: string;\n endpoints: RouterEndpoints;\n options: CreateApiOptions | undefined;\n} {\n if (typeof second === \"string\") {\n if (!third)\n throw new Error(\"endpoints is required when prefix is provided\");\n return {\n prefix: second,\n endpoints: third as RouterEndpoints,\n options: fourth,\n };\n }\n if (isRouterDef(second)) {\n return {\n prefix: second.prefix,\n endpoints: second.endpoints,\n options: third as CreateApiOptions | undefined,\n };\n }\n return {\n prefix: \"\",\n endpoints: second as RouterEndpoints,\n options: third as CreateApiOptions | undefined,\n };\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n client[key] = isRouterDef(entry)\n ? buildClient(executor, joinPaths(prefix, entry.prefix), entry.endpoints, options)\n : buildEndpointFn(executor, prefix, entry as EndpointSpec<any, any, any>, options);\n }\n\n return client;\n}\n\nfunction buildEndpointFn(\n executor: Executor,\n prefix: string,\n spec: EndpointSpec<any, any, any>,\n options: CreateApiOptions | undefined,\n) {\n return async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(joinPaths(prefix, spec.path), validatedParams?.path);\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: ValidatorOutput<typeof spec.response>;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n return spec.adapter ? spec.adapter(result) : result;\n };\n}\n","import type { ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with an\n * optional middleware chain.\n *\n * Middlewares are applied in declaration order — the first middleware is the\n * outermost wrapper and runs first on each request.\n *\n * @param execute - The underlying transport function (fetch, axios, etc.).\n * @param middlewares - Ordered list of middlewares to apply.\n *\n * @example\n * ```ts\n * const executor = createExecutor(\n * async ({ method, url, body }) => {\n * const res = await fetch(url, { method, body: JSON.stringify(body) });\n * return res.json();\n * },\n * [withTimeout(5000), withRetry(3), withLogger()],\n * );\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[] = [],\n): Executor {\n const chain = middlewares.reduceRight<\n (options: ExecuteOptions) => Promise<unknown>\n >((next, mw) => (opts) => mw(opts, next), execute);\n return { execute: chain };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * Use this to unify SSR and CSR behind a single API client — the resolver\n * picks the right executor per request, so `createApi` is called once and\n * works in both environments without duplicate `*ServerApi` instances.\n *\n * The resolver receives the full {@link ExecuteOptions} so it can branch on\n * environment, URL prefix, auth context, or any runtime condition.\n *\n * @param resolver - Called on every request; returns the executor to delegate to.\n *\n * @example\n * ```ts\n * // SSR vs CSR — pick transport based on environment\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n *\n * // Route by URL prefix — internal routes use a different transport\n * const apiExecutor = dispatchExecutor((opts) =>\n * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * @example\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * adapter: toTodoItem,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<TResponse extends Validator<unknown>, TOut>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<TResponse extends Validator<unknown>>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n","import type { ExecutorMiddleware } from \"./types.js\";\n\n/**\n * Thrown by {@link withTimeout} when a request exceeds the configured duration.\n * Distinguishable from a user-initiated {@link AbortSignal} cancellation.\n */\nexport class TimeoutError extends Error {\n constructor(public readonly ms: number) {\n super(`Request timed out after ${ms}ms`);\n this.name = \"TimeoutError\";\n }\n}\n\n/**\n * Identity helper that returns the middleware as-is.\n *\n * Wrap your middleware function with this to get full type inference on `opts`\n * and `next` without having to annotate the type manually.\n *\n * @example\n * ```ts\n * const withCorrelationId = defineMiddleware((opts, next) =>\n * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })\n * );\n * ```\n */\nexport function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware {\n return fn;\n}\n\n/**\n * Retries a failed request up to `count` additional times.\n *\n * By default all errors trigger a retry. Pass `shouldRetry` to skip retries\n * for non-transient errors (e.g. 4xx responses).\n *\n * @param count - Number of retries (not counting the initial attempt).\n * @param options.shouldRetry - Return `false` to stop retrying early.\n * Receives the error and a zero-based `attempt` index (0 = first failure,\n * 1 = second failure, …) so you can limit retries by count or error type.\n *\n * @example\n * ```ts\n * withRetry(3, {\n * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,\n * })\n * ```\n */\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n });\n}\n\n/**\n * Aborts a request if it does not complete within `ms` milliseconds.\n *\n * Merges the timeout signal with any existing `AbortSignal` on the request,\n * so whichever fires first wins.\n *\n * @param ms - Timeout in milliseconds.\n */\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);\n\n const { signal, cleanup } = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : { signal: controller.signal, cleanup: () => {} };\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n cleanup();\n }\n });\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * withLogger({ log: (msg, data) => logger.debug(msg, data) })\n * ```\n */\nexport function withLogger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorMiddleware {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n return defineMiddleware(async (opts, next) => {\n const start = Date.now();\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n try {\n const result = await next(opts);\n log(`[routar] ${opts.method} ${opts.url} — ${Date.now() - start}ms`);\n return result;\n } catch (err) {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - start}ms`,\n err,\n );\n throw err;\n }\n });\n}\n\n/** Combines multiple AbortSignals into one that aborts when any of them fire. */\nfunction anySignal(signals: AbortSignal[]): {\n signal: AbortSignal;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n const onAbort = () => controller.abort();\n const attached: AbortSignal[] = [];\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener(\"abort\", onAbort, { once: true });\n attached.push(s);\n }\n return {\n signal: controller.signal,\n cleanup: () =>\n attached.forEach((s) => {\n s.removeEventListener(\"abort\", onAbort);\n }),\n };\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else if (typeof value === \"object\") {\n throw new TypeError(\n `serializeParams: value for key \"${key}\" is a plain object. Serialize it to a string before passing as a query parameter.`,\n );\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n"]}