@routar/core 1.0.0 → 1.2.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ // src/define-router.ts
4
+ function isRouterDef(entry) {
5
+ return "prefix" in entry && "endpoints" in entry;
6
+ }
7
+ function defineRouter(prefix, endpoints) {
8
+ return { prefix, endpoints };
9
+ }
10
+
3
11
  // src/utils/path.ts
4
12
  function joinPaths(...segments) {
5
13
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -9,7 +17,7 @@ function resolvePath(pathTemplate, params) {
9
17
  if (!params) return pathTemplate;
10
18
  return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {
11
19
  const value = params[key];
12
- if (value == null) throw new Error(`Missing path parameter: ${key}`);
20
+ if (value == null || value === "") throw new Error(`Missing path parameter: ${key}`);
13
21
  return encodeURIComponent(String(value));
14
22
  });
15
23
  }
@@ -18,13 +26,13 @@ function resolvePath(pathTemplate, params) {
18
26
  var ValidationError = class extends Error {
19
27
  constructor(message, cause) {
20
28
  super(message);
21
- this.cause = cause;
22
29
  this.name = "ValidationError";
23
30
  if (cause !== void 0) {
24
31
  Object.defineProperty(this, "cause", {
25
32
  value: cause,
26
- writable: true,
27
- enumerable: true
33
+ writable: false,
34
+ enumerable: false,
35
+ configurable: true
28
36
  });
29
37
  }
30
38
  }
@@ -32,25 +40,35 @@ var ValidationError = class extends Error {
32
40
 
33
41
  // src/create-api.ts
34
42
  function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
35
- let prefix;
36
- let endpoints;
37
- let options;
38
- if (typeof routerOrPrefixOrEndpoints === "string") {
39
- prefix = routerOrPrefixOrEndpoints;
40
- if (!endpointsArgOrOptions)
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)
41
53
  throw new Error("endpoints is required when prefix is provided");
42
- endpoints = endpointsArgOrOptions;
43
- options = optionsArg;
44
- } else if ("prefix" in routerOrPrefixOrEndpoints && "endpoints" in routerOrPrefixOrEndpoints) {
45
- prefix = routerOrPrefixOrEndpoints.prefix;
46
- endpoints = routerOrPrefixOrEndpoints.endpoints;
47
- options = endpointsArgOrOptions;
48
- } else {
49
- prefix = "";
50
- endpoints = routerOrPrefixOrEndpoints;
51
- options = endpointsArgOrOptions;
54
+ return {
55
+ prefix: second,
56
+ endpoints: third,
57
+ options: fourth
58
+ };
52
59
  }
53
- return buildClient(executor, prefix, endpoints, options);
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
+ };
54
72
  }
55
73
  function shouldValidate(options, kind) {
56
74
  const v = options?.validate;
@@ -61,55 +79,41 @@ function shouldValidate(options, kind) {
61
79
  function buildClient(executor, prefix, endpoints, options) {
62
80
  const client = {};
63
81
  for (const [key, entry] of Object.entries(endpoints)) {
64
- if ("prefix" in entry && "endpoints" in entry) {
65
- const nested = entry;
66
- client[key] = buildClient(
67
- executor,
68
- joinPaths(prefix, nested.prefix),
69
- nested.endpoints,
70
- options
71
- );
72
- } else {
73
- const spec = entry;
74
- client[key] = async (params = {}, signal) => {
75
- let validatedParams = params;
76
- if (spec.request && shouldValidate(options, "request")) {
77
- try {
78
- validatedParams = spec.request.parse(params);
79
- } catch (err) {
80
- throw new ValidationError("Request validation failed", err);
81
- }
82
- }
83
- const url = resolvePath(
84
- joinPaths(prefix, spec.path),
85
- validatedParams?.path
86
- );
87
- const raw = await executor.execute({
88
- method: spec.method,
89
- url,
90
- params: validatedParams?.query,
91
- body: validatedParams?.body,
92
- signal
93
- });
94
- let result;
95
- if (shouldValidate(options, "response")) {
96
- try {
97
- result = spec.response.parse(raw);
98
- } catch (err) {
99
- throw new ValidationError("Response validation failed", err);
100
- }
101
- } else {
102
- result = raw;
103
- }
104
- if (spec.adapter) {
105
- return spec.adapter(result);
106
- }
107
- return result;
108
- };
109
- }
82
+ client[key] = isRouterDef(entry) ? buildClient(executor, joinPaths(prefix, entry.prefix), entry.endpoints, options) : buildEndpointFn(executor, prefix, entry, options);
110
83
  }
111
84
  return client;
112
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
+ }
113
117
 
114
118
  // src/create-executor.ts
115
119
  function createExecutor(execute, middlewares = []) {
@@ -122,17 +126,83 @@ function dispatchExecutor(resolver) {
122
126
  };
123
127
  }
124
128
 
129
+ // src/utils/params.ts
130
+ function serializeParams(params) {
131
+ const result = new URLSearchParams();
132
+ for (const [key, value] of Object.entries(params)) {
133
+ if (value == null) continue;
134
+ if (Array.isArray(value)) {
135
+ for (const item of value) {
136
+ if (item != null) result.append(key, String(item));
137
+ }
138
+ } else if (typeof value === "object") {
139
+ throw new TypeError(
140
+ `serializeParams: value for key "${key}" is a plain object. Serialize it to a string before passing as a query parameter.`
141
+ );
142
+ } else {
143
+ result.append(key, String(value));
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+
149
+ // src/create-fetch-executor.ts
150
+ function createFetchExecutor(baseURL, options) {
151
+ return createExecutor(
152
+ async ({ method, url, params, body, headers, signal }) => {
153
+ const fullURL = new URL(baseURL.replace(/\/$/, "") + url);
154
+ if (params) {
155
+ serializeParams(params).forEach((v, k) => {
156
+ fullURL.searchParams.set(k, v);
157
+ });
158
+ }
159
+ const defaultHeaders = await options?.defaultHeaders?.() ?? {};
160
+ const res = await fetch(fullURL.toString(), {
161
+ method,
162
+ headers: {
163
+ ...defaultHeaders,
164
+ ...headers,
165
+ ...body != null ? { "Content-Type": "application/json" } : {}
166
+ },
167
+ body: body != null ? JSON.stringify(body) : void 0,
168
+ signal
169
+ });
170
+ if (!res.ok) {
171
+ const errorBody = await res.json().catch(() => null);
172
+ throw new HttpError(res.status, res.statusText, errorBody);
173
+ }
174
+ if (res.status === 204 || res.status === 205 || res.status === 304) {
175
+ return null;
176
+ }
177
+ const text = await res.text();
178
+ return text === "" ? null : JSON.parse(text);
179
+ },
180
+ options?.middlewares
181
+ );
182
+ }
183
+ var HttpError = class extends Error {
184
+ constructor(status, statusText, body = null) {
185
+ super(`HTTP ${status}: ${statusText}`);
186
+ this.status = status;
187
+ this.statusText = statusText;
188
+ this.body = body;
189
+ this.name = "HttpError";
190
+ }
191
+ };
192
+
125
193
  // src/define-endpoint.ts
126
194
  function endpoint(spec) {
127
195
  return spec;
128
196
  }
129
197
 
130
- // src/define-router.ts
131
- function defineRouter(prefix, endpoints) {
132
- return { prefix, endpoints };
133
- }
134
-
135
198
  // src/middleware.ts
199
+ var TimeoutError = class extends Error {
200
+ constructor(ms) {
201
+ super(`Request timed out after ${ms}ms`);
202
+ this.ms = ms;
203
+ this.name = "TimeoutError";
204
+ }
205
+ };
136
206
  function defineMiddleware(fn) {
137
207
  return fn;
138
208
  }
@@ -154,12 +224,14 @@ function withRetry(count, options) {
154
224
  function withTimeout(ms) {
155
225
  return defineMiddleware(async (opts, next) => {
156
226
  const controller = new AbortController();
157
- const timer = setTimeout(() => controller.abort(), ms);
158
- const signal = opts.signal ? anySignal([opts.signal, controller.signal]) : controller.signal;
227
+ const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
228
+ const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
229
+ } };
159
230
  try {
160
231
  return await next({ ...opts, signal });
161
232
  } finally {
162
233
  clearTimeout(timer);
234
+ cleanup();
163
235
  }
164
236
  });
165
237
  }
@@ -186,39 +258,35 @@ function withLogger(options) {
186
258
  }
187
259
  function anySignal(signals) {
188
260
  const controller = new AbortController();
189
- for (const signal of signals) {
190
- if (signal.aborted) {
261
+ const onAbort = () => controller.abort();
262
+ const attached = [];
263
+ for (const s of signals) {
264
+ if (s.aborted) {
191
265
  controller.abort();
192
- return controller.signal;
266
+ break;
193
267
  }
194
- signal.addEventListener("abort", () => controller.abort(), { once: true });
268
+ s.addEventListener("abort", onAbort, { once: true });
269
+ attached.push(s);
195
270
  }
196
- return controller.signal;
197
- }
198
-
199
- // src/utils/params.ts
200
- function serializeParams(params) {
201
- const result = new URLSearchParams();
202
- for (const [key, value] of Object.entries(params)) {
203
- if (value == null) continue;
204
- if (Array.isArray(value)) {
205
- for (const item of value) {
206
- if (item != null) result.append(key, String(item));
207
- }
208
- } else {
209
- result.append(key, String(value));
210
- }
211
- }
212
- return result;
271
+ return {
272
+ signal: controller.signal,
273
+ cleanup: () => attached.forEach((s) => {
274
+ s.removeEventListener("abort", onAbort);
275
+ })
276
+ };
213
277
  }
214
278
 
279
+ exports.HttpError = HttpError;
280
+ exports.TimeoutError = TimeoutError;
215
281
  exports.ValidationError = ValidationError;
216
282
  exports.createApi = createApi;
217
283
  exports.createExecutor = createExecutor;
284
+ exports.createFetchExecutor = createFetchExecutor;
218
285
  exports.defineMiddleware = defineMiddleware;
219
286
  exports.defineRouter = defineRouter;
220
287
  exports.dispatchExecutor = dispatchExecutor;
221
288
  exports.endpoint = endpoint;
289
+ exports.isRouterDef = isRouterDef;
222
290
  exports.joinPaths = joinPaths;
223
291
  exports.resolvePath = resolvePath;
224
292
  exports.serializeParams = serializeParams;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/define-endpoint.ts","../src/define-router.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["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 CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\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 }\n ? R\n : 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: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n let options: CreateApiOptions | undefined;\n\n if (typeof routerOrPrefixOrEndpoints === \"string\") {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArgOrOptions)\n throw new Error(\"endpoints is required when prefix is provided\");\n endpoints = endpointsArgOrOptions as RouterEndpoints;\n options = optionsArg;\n } else if (\n \"prefix\" in routerOrPrefixOrEndpoints &&\n \"endpoints\" in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).endpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n } else {\n prefix = \"\";\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n }\n\n return buildClient(executor, prefix, endpoints, options);\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 if (\"prefix\" in entry && \"endpoints\" in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<RouterEndpoints>;\n client[key] = buildClient(\n executor,\n joinPaths(prefix, nested.prefix),\n nested.endpoints,\n options,\n );\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 && 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(\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 result: unknown;\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 if (spec.adapter) {\n return spec.adapter(result);\n }\n return result;\n };\n }\n }\n\n return client;\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 { 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 { 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}`, {\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[]): 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(\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 {\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/utils/params.ts","../src/create-fetch-executor.ts","../src/define-endpoint.ts","../src/middleware.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","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","import type { Executor, ExecutorMiddleware } from \"./types.js\";\nimport { createExecutor } from \"./create-executor.js\";\nimport { serializeParams } from \"./utils/params.js\";\n\n/**\n * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.\n *\n * Suited for SSR environments where you need per-request dynamic headers\n * (e.g. forwarding auth cookies) without sharing state across requests.\n *\n * - Query params are serialized and appended to the URL.\n * - A `Content-Type: application/json` header is added automatically when\n * a request body is present.\n * - Responses with status 204 or `Content-Length: 0` resolve to `null`.\n * - Non-2xx responses throw an {@link HttpError}.\n *\n * @param baseURL - Absolute base URL prepended to every endpoint path.\n * @param options.defaultHeaders - Async factory called on every request to\n * produce headers (e.g. reading cookies in a Next.js server component).\n * @param options.middlewares - Middleware chain applied before the fetch call.\n *\n * @example\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * defaultHeaders: async () => {\n * const token = await getServerToken();\n * return token ? { Authorization: `Bearer ${token}` } : {};\n * },\n * });\n * ```\n */\nexport function createFetchExecutor(\n baseURL: string,\n options?: {\n defaultHeaders?: () =>\n | Record<string, string>\n | Promise<Record<string, string>>;\n middlewares?: ExecutorMiddleware[];\n },\n): Executor {\n return createExecutor(\n async ({ method, url, params, body, headers, signal }) => {\n const fullURL = new URL(baseURL.replace(/\\/$/, \"\") + url);\n if (params) {\n serializeParams(params).forEach((v, k) => {\n fullURL.searchParams.set(k, v);\n });\n }\n\n const defaultHeaders = (await options?.defaultHeaders?.()) ?? {};\n\n const res = await fetch(fullURL.toString(), {\n method,\n headers: {\n ...defaultHeaders,\n ...headers,\n ...(body != null ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new HttpError(res.status, res.statusText, errorBody);\n }\n if (res.status === 204 || res.status === 205 || res.status === 304) {\n return null;\n }\n const text = await res.text();\n return text === \"\" ? null : JSON.parse(text);\n },\n options?.middlewares,\n );\n}\n\n/**\n * Thrown by {@link createFetchExecutor} when the server returns a non-2xx\n * status code.\n *\n * @example\n * ```ts\n * try {\n * await api.getDetail({ path: { id: 999 } });\n * } catch (err) {\n * if (err instanceof HttpError && err.status === 404) {\n * // handle not-found\n * }\n * }\n * ```\n */\nexport class HttpError extends Error {\n constructor(\n /** HTTP status code (e.g. 404, 500). */\n public readonly status: number,\n /** HTTP status text (e.g. \"Not Found\"). */\n public readonly statusText: string,\n /** Parsed response body, or `null` if the body was empty or not JSON. */\n public readonly body: unknown = null,\n ) {\n super(`HTTP ${status}: ${statusText}`);\n this.name = \"HttpError\";\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"]}
package/dist/index.d.cts CHANGED
@@ -5,6 +5,11 @@ interface ExecuteOptions {
5
5
  url: string;
6
6
  params?: Record<string, unknown>;
7
7
  body?: unknown;
8
+ /**
9
+ * Per-request headers injected by middleware (e.g. `defineMiddleware`).
10
+ * Headers cannot be set from `createApi` call sites directly — use middleware
11
+ * to add dynamic headers such as `Authorization` or `X-Request-Id`.
12
+ */
8
13
  headers?: Record<string, string>;
9
14
  signal?: AbortSignal;
10
15
  }
@@ -143,9 +148,9 @@ interface CreateApiOptions {
143
148
  }
144
149
 
145
150
  /** Callable type for a single endpoint on the generated API client. */
146
- type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec["request"] extends {
151
+ type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = TSpec["request"] extends {
147
152
  parse: (data: unknown) => infer R;
148
- } ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
153
+ } ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>> : (params?: RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
149
154
  /**
150
155
  * Fully-typed API client produced by {@link createApi}.
151
156
  * Nested {@link RouterDef} entries become nested sub-client objects.
@@ -248,6 +253,68 @@ declare function createExecutor(execute: (options: ExecuteOptions) => Promise<un
248
253
  */
249
254
  declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
250
255
 
256
+ /**
257
+ * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.
258
+ *
259
+ * Suited for SSR environments where you need per-request dynamic headers
260
+ * (e.g. forwarding auth cookies) without sharing state across requests.
261
+ *
262
+ * - Query params are serialized and appended to the URL.
263
+ * - A `Content-Type: application/json` header is added automatically when
264
+ * a request body is present.
265
+ * - Responses with status 204 or `Content-Length: 0` resolve to `null`.
266
+ * - Non-2xx responses throw an {@link HttpError}.
267
+ *
268
+ * @param baseURL - Absolute base URL prepended to every endpoint path.
269
+ * @param options.defaultHeaders - Async factory called on every request to
270
+ * produce headers (e.g. reading cookies in a Next.js server component).
271
+ * @param options.middlewares - Middleware chain applied before the fetch call.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const executor = createFetchExecutor('https://api.example.com', {
276
+ * defaultHeaders: async () => {
277
+ * const token = await getServerToken();
278
+ * return token ? { Authorization: `Bearer ${token}` } : {};
279
+ * },
280
+ * });
281
+ * ```
282
+ */
283
+ declare function createFetchExecutor(baseURL: string, options?: {
284
+ defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
285
+ middlewares?: ExecutorMiddleware[];
286
+ }): Executor;
287
+ /**
288
+ * Thrown by {@link createFetchExecutor} when the server returns a non-2xx
289
+ * status code.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * try {
294
+ * await api.getDetail({ path: { id: 999 } });
295
+ * } catch (err) {
296
+ * if (err instanceof HttpError && err.status === 404) {
297
+ * // handle not-found
298
+ * }
299
+ * }
300
+ * ```
301
+ */
302
+ declare class HttpError extends Error {
303
+ /** HTTP status code (e.g. 404, 500). */
304
+ readonly status: number;
305
+ /** HTTP status text (e.g. "Not Found"). */
306
+ readonly statusText: string;
307
+ /** Parsed response body, or `null` if the body was empty or not JSON. */
308
+ 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);
316
+ }
317
+
251
318
  /**
252
319
  * Extracts `:param` segment names from a path template string as a union of
253
320
  * string literals.
@@ -373,8 +440,18 @@ declare function endpoint<TResponse extends Validator<unknown>>(spec: {
373
440
  * });
374
441
  * ```
375
442
  */
443
+ /** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */
444
+ declare function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints>;
376
445
  declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
377
446
 
447
+ /**
448
+ * Thrown by {@link withTimeout} when a request exceeds the configured duration.
449
+ * Distinguishable from a user-initiated {@link AbortSignal} cancellation.
450
+ */
451
+ declare class TimeoutError extends Error {
452
+ readonly ms: number;
453
+ constructor(ms: number);
454
+ }
378
455
  /**
379
456
  * Identity helper that returns the middleware as-is.
380
457
  *
@@ -397,6 +474,8 @@ declare function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware;
397
474
  *
398
475
  * @param count - Number of retries (not counting the initial attempt).
399
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.
400
479
  *
401
480
  * @example
402
481
  * ```ts
@@ -433,12 +512,19 @@ declare function withLogger(options?: {
433
512
 
434
513
  declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
435
514
 
515
+ /**
516
+ * Joins URL path segments, normalising repeated slashes and trailing slashes.
517
+ *
518
+ * **Note:** Intended for relative API paths only. Absolute URLs containing
519
+ * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs
520
+ * directly to the executor instead of through this helper.
521
+ */
436
522
  declare function joinPaths(...segments: string[]): string;
437
523
  declare function resolvePath(pathTemplate: string, params?: Record<string, unknown>): string;
438
524
 
439
525
  declare class ValidationError extends Error {
440
- readonly cause?: unknown | undefined;
441
- constructor(message: string, cause?: unknown | undefined);
526
+ readonly cause?: unknown;
527
+ constructor(message: string, cause?: unknown);
442
528
  }
443
529
 
444
- export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
530
+ export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,11 @@ interface ExecuteOptions {
5
5
  url: string;
6
6
  params?: Record<string, unknown>;
7
7
  body?: unknown;
8
+ /**
9
+ * Per-request headers injected by middleware (e.g. `defineMiddleware`).
10
+ * Headers cannot be set from `createApi` call sites directly — use middleware
11
+ * to add dynamic headers such as `Authorization` or `X-Request-Id`.
12
+ */
8
13
  headers?: Record<string, string>;
9
14
  signal?: AbortSignal;
10
15
  }
@@ -143,9 +148,9 @@ interface CreateApiOptions {
143
148
  }
144
149
 
145
150
  /** Callable type for a single endpoint on the generated API client. */
146
- type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec["request"] extends {
151
+ type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = TSpec["request"] extends {
147
152
  parse: (data: unknown) => infer R;
148
- } ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
153
+ } ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>> : (params?: RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
149
154
  /**
150
155
  * Fully-typed API client produced by {@link createApi}.
151
156
  * Nested {@link RouterDef} entries become nested sub-client objects.
@@ -248,6 +253,68 @@ declare function createExecutor(execute: (options: ExecuteOptions) => Promise<un
248
253
  */
249
254
  declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
250
255
 
256
+ /**
257
+ * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.
258
+ *
259
+ * Suited for SSR environments where you need per-request dynamic headers
260
+ * (e.g. forwarding auth cookies) without sharing state across requests.
261
+ *
262
+ * - Query params are serialized and appended to the URL.
263
+ * - A `Content-Type: application/json` header is added automatically when
264
+ * a request body is present.
265
+ * - Responses with status 204 or `Content-Length: 0` resolve to `null`.
266
+ * - Non-2xx responses throw an {@link HttpError}.
267
+ *
268
+ * @param baseURL - Absolute base URL prepended to every endpoint path.
269
+ * @param options.defaultHeaders - Async factory called on every request to
270
+ * produce headers (e.g. reading cookies in a Next.js server component).
271
+ * @param options.middlewares - Middleware chain applied before the fetch call.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const executor = createFetchExecutor('https://api.example.com', {
276
+ * defaultHeaders: async () => {
277
+ * const token = await getServerToken();
278
+ * return token ? { Authorization: `Bearer ${token}` } : {};
279
+ * },
280
+ * });
281
+ * ```
282
+ */
283
+ declare function createFetchExecutor(baseURL: string, options?: {
284
+ defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
285
+ middlewares?: ExecutorMiddleware[];
286
+ }): Executor;
287
+ /**
288
+ * Thrown by {@link createFetchExecutor} when the server returns a non-2xx
289
+ * status code.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * try {
294
+ * await api.getDetail({ path: { id: 999 } });
295
+ * } catch (err) {
296
+ * if (err instanceof HttpError && err.status === 404) {
297
+ * // handle not-found
298
+ * }
299
+ * }
300
+ * ```
301
+ */
302
+ declare class HttpError extends Error {
303
+ /** HTTP status code (e.g. 404, 500). */
304
+ readonly status: number;
305
+ /** HTTP status text (e.g. "Not Found"). */
306
+ readonly statusText: string;
307
+ /** Parsed response body, or `null` if the body was empty or not JSON. */
308
+ 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);
316
+ }
317
+
251
318
  /**
252
319
  * Extracts `:param` segment names from a path template string as a union of
253
320
  * string literals.
@@ -373,8 +440,18 @@ declare function endpoint<TResponse extends Validator<unknown>>(spec: {
373
440
  * });
374
441
  * ```
375
442
  */
443
+ /** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */
444
+ declare function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints>;
376
445
  declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
377
446
 
447
+ /**
448
+ * Thrown by {@link withTimeout} when a request exceeds the configured duration.
449
+ * Distinguishable from a user-initiated {@link AbortSignal} cancellation.
450
+ */
451
+ declare class TimeoutError extends Error {
452
+ readonly ms: number;
453
+ constructor(ms: number);
454
+ }
378
455
  /**
379
456
  * Identity helper that returns the middleware as-is.
380
457
  *
@@ -397,6 +474,8 @@ declare function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware;
397
474
  *
398
475
  * @param count - Number of retries (not counting the initial attempt).
399
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.
400
479
  *
401
480
  * @example
402
481
  * ```ts
@@ -433,12 +512,19 @@ declare function withLogger(options?: {
433
512
 
434
513
  declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
435
514
 
515
+ /**
516
+ * Joins URL path segments, normalising repeated slashes and trailing slashes.
517
+ *
518
+ * **Note:** Intended for relative API paths only. Absolute URLs containing
519
+ * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs
520
+ * directly to the executor instead of through this helper.
521
+ */
436
522
  declare function joinPaths(...segments: string[]): string;
437
523
  declare function resolvePath(pathTemplate: string, params?: Record<string, unknown>): string;
438
524
 
439
525
  declare class ValidationError extends Error {
440
- readonly cause?: unknown | undefined;
441
- constructor(message: string, cause?: unknown | undefined);
526
+ readonly cause?: unknown;
527
+ constructor(message: string, cause?: unknown);
442
528
  }
443
529
 
444
- export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
530
+ export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ // src/define-router.ts
2
+ function isRouterDef(entry) {
3
+ return "prefix" in entry && "endpoints" in entry;
4
+ }
5
+ function defineRouter(prefix, endpoints) {
6
+ return { prefix, endpoints };
7
+ }
8
+
1
9
  // src/utils/path.ts
2
10
  function joinPaths(...segments) {
3
11
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -7,7 +15,7 @@ function resolvePath(pathTemplate, params) {
7
15
  if (!params) return pathTemplate;
8
16
  return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {
9
17
  const value = params[key];
10
- if (value == null) throw new Error(`Missing path parameter: ${key}`);
18
+ if (value == null || value === "") throw new Error(`Missing path parameter: ${key}`);
11
19
  return encodeURIComponent(String(value));
12
20
  });
13
21
  }
@@ -16,13 +24,13 @@ function resolvePath(pathTemplate, params) {
16
24
  var ValidationError = class extends Error {
17
25
  constructor(message, cause) {
18
26
  super(message);
19
- this.cause = cause;
20
27
  this.name = "ValidationError";
21
28
  if (cause !== void 0) {
22
29
  Object.defineProperty(this, "cause", {
23
30
  value: cause,
24
- writable: true,
25
- enumerable: true
31
+ writable: false,
32
+ enumerable: false,
33
+ configurable: true
26
34
  });
27
35
  }
28
36
  }
@@ -30,25 +38,35 @@ var ValidationError = class extends Error {
30
38
 
31
39
  // src/create-api.ts
32
40
  function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
33
- let prefix;
34
- let endpoints;
35
- let options;
36
- if (typeof routerOrPrefixOrEndpoints === "string") {
37
- prefix = routerOrPrefixOrEndpoints;
38
- if (!endpointsArgOrOptions)
41
+ const { prefix, endpoints, options } = resolveArgs(
42
+ routerOrPrefixOrEndpoints,
43
+ endpointsArgOrOptions,
44
+ optionsArg
45
+ );
46
+ return buildClient(executor, prefix, endpoints, options);
47
+ }
48
+ function resolveArgs(second, third, fourth) {
49
+ if (typeof second === "string") {
50
+ if (!third)
39
51
  throw new Error("endpoints is required when prefix is provided");
40
- endpoints = endpointsArgOrOptions;
41
- options = optionsArg;
42
- } else if ("prefix" in routerOrPrefixOrEndpoints && "endpoints" in routerOrPrefixOrEndpoints) {
43
- prefix = routerOrPrefixOrEndpoints.prefix;
44
- endpoints = routerOrPrefixOrEndpoints.endpoints;
45
- options = endpointsArgOrOptions;
46
- } else {
47
- prefix = "";
48
- endpoints = routerOrPrefixOrEndpoints;
49
- options = endpointsArgOrOptions;
52
+ return {
53
+ prefix: second,
54
+ endpoints: third,
55
+ options: fourth
56
+ };
50
57
  }
51
- return buildClient(executor, prefix, endpoints, options);
58
+ if (isRouterDef(second)) {
59
+ return {
60
+ prefix: second.prefix,
61
+ endpoints: second.endpoints,
62
+ options: third
63
+ };
64
+ }
65
+ return {
66
+ prefix: "",
67
+ endpoints: second,
68
+ options: third
69
+ };
52
70
  }
53
71
  function shouldValidate(options, kind) {
54
72
  const v = options?.validate;
@@ -59,55 +77,41 @@ function shouldValidate(options, kind) {
59
77
  function buildClient(executor, prefix, endpoints, options) {
60
78
  const client = {};
61
79
  for (const [key, entry] of Object.entries(endpoints)) {
62
- if ("prefix" in entry && "endpoints" in entry) {
63
- const nested = entry;
64
- client[key] = buildClient(
65
- executor,
66
- joinPaths(prefix, nested.prefix),
67
- nested.endpoints,
68
- options
69
- );
70
- } else {
71
- const spec = entry;
72
- client[key] = async (params = {}, signal) => {
73
- let validatedParams = params;
74
- if (spec.request && shouldValidate(options, "request")) {
75
- try {
76
- validatedParams = spec.request.parse(params);
77
- } catch (err) {
78
- throw new ValidationError("Request validation failed", err);
79
- }
80
- }
81
- const url = resolvePath(
82
- joinPaths(prefix, spec.path),
83
- validatedParams?.path
84
- );
85
- const raw = await executor.execute({
86
- method: spec.method,
87
- url,
88
- params: validatedParams?.query,
89
- body: validatedParams?.body,
90
- signal
91
- });
92
- let result;
93
- if (shouldValidate(options, "response")) {
94
- try {
95
- result = spec.response.parse(raw);
96
- } catch (err) {
97
- throw new ValidationError("Response validation failed", err);
98
- }
99
- } else {
100
- result = raw;
101
- }
102
- if (spec.adapter) {
103
- return spec.adapter(result);
104
- }
105
- return result;
106
- };
107
- }
80
+ client[key] = isRouterDef(entry) ? buildClient(executor, joinPaths(prefix, entry.prefix), entry.endpoints, options) : buildEndpointFn(executor, prefix, entry, options);
108
81
  }
109
82
  return client;
110
83
  }
84
+ function buildEndpointFn(executor, prefix, spec, options) {
85
+ return async (params = {}, signal) => {
86
+ let validatedParams = params;
87
+ if (spec.request && shouldValidate(options, "request")) {
88
+ try {
89
+ validatedParams = spec.request.parse(params);
90
+ } catch (err) {
91
+ throw new ValidationError("Request validation failed", err);
92
+ }
93
+ }
94
+ const url = resolvePath(joinPaths(prefix, spec.path), validatedParams?.path);
95
+ const raw = await executor.execute({
96
+ method: spec.method,
97
+ url,
98
+ params: validatedParams?.query,
99
+ body: validatedParams?.body,
100
+ signal
101
+ });
102
+ let result;
103
+ if (shouldValidate(options, "response")) {
104
+ try {
105
+ result = spec.response.parse(raw);
106
+ } catch (err) {
107
+ throw new ValidationError("Response validation failed", err);
108
+ }
109
+ } else {
110
+ result = raw;
111
+ }
112
+ return spec.adapter ? spec.adapter(result) : result;
113
+ };
114
+ }
111
115
 
112
116
  // src/create-executor.ts
113
117
  function createExecutor(execute, middlewares = []) {
@@ -120,17 +124,83 @@ function dispatchExecutor(resolver) {
120
124
  };
121
125
  }
122
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));
135
+ }
136
+ } else if (typeof value === "object") {
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));
142
+ }
143
+ }
144
+ return result;
145
+ }
146
+
147
+ // src/create-fetch-executor.ts
148
+ function createFetchExecutor(baseURL, options) {
149
+ return createExecutor(
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
179
+ );
180
+ }
181
+ var HttpError = class extends Error {
182
+ constructor(status, statusText, body = null) {
183
+ super(`HTTP ${status}: ${statusText}`);
184
+ this.status = status;
185
+ this.statusText = statusText;
186
+ this.body = body;
187
+ this.name = "HttpError";
188
+ }
189
+ };
190
+
123
191
  // src/define-endpoint.ts
124
192
  function endpoint(spec) {
125
193
  return spec;
126
194
  }
127
195
 
128
- // src/define-router.ts
129
- function defineRouter(prefix, endpoints) {
130
- return { prefix, endpoints };
131
- }
132
-
133
196
  // src/middleware.ts
197
+ var TimeoutError = class extends Error {
198
+ constructor(ms) {
199
+ super(`Request timed out after ${ms}ms`);
200
+ this.ms = ms;
201
+ this.name = "TimeoutError";
202
+ }
203
+ };
134
204
  function defineMiddleware(fn) {
135
205
  return fn;
136
206
  }
@@ -152,12 +222,14 @@ function withRetry(count, options) {
152
222
  function withTimeout(ms) {
153
223
  return defineMiddleware(async (opts, next) => {
154
224
  const controller = new AbortController();
155
- const timer = setTimeout(() => controller.abort(), ms);
156
- const signal = opts.signal ? anySignal([opts.signal, controller.signal]) : controller.signal;
225
+ const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
226
+ const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
227
+ } };
157
228
  try {
158
229
  return await next({ ...opts, signal });
159
230
  } finally {
160
231
  clearTimeout(timer);
232
+ cleanup();
161
233
  }
162
234
  });
163
235
  }
@@ -184,32 +256,24 @@ function withLogger(options) {
184
256
  }
185
257
  function anySignal(signals) {
186
258
  const controller = new AbortController();
187
- for (const signal of signals) {
188
- if (signal.aborted) {
259
+ const onAbort = () => controller.abort();
260
+ const attached = [];
261
+ for (const s of signals) {
262
+ if (s.aborted) {
189
263
  controller.abort();
190
- return controller.signal;
264
+ break;
191
265
  }
192
- signal.addEventListener("abort", () => controller.abort(), { once: true });
266
+ s.addEventListener("abort", onAbort, { once: true });
267
+ attached.push(s);
193
268
  }
194
- return controller.signal;
195
- }
196
-
197
- // src/utils/params.ts
198
- function serializeParams(params) {
199
- const result = new URLSearchParams();
200
- for (const [key, value] of Object.entries(params)) {
201
- if (value == null) continue;
202
- if (Array.isArray(value)) {
203
- for (const item of value) {
204
- if (item != null) result.append(key, String(item));
205
- }
206
- } else {
207
- result.append(key, String(value));
208
- }
209
- }
210
- return result;
269
+ return {
270
+ signal: controller.signal,
271
+ cleanup: () => attached.forEach((s) => {
272
+ s.removeEventListener("abort", onAbort);
273
+ })
274
+ };
211
275
  }
212
276
 
213
- export { ValidationError, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
277
+ export { HttpError, TimeoutError, ValidationError, createApi, createExecutor, createFetchExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
214
278
  //# sourceMappingURL=index.js.map
215
279
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/define-endpoint.ts","../src/define-router.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["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 CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\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 }\n ? R\n : 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: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n let options: CreateApiOptions | undefined;\n\n if (typeof routerOrPrefixOrEndpoints === \"string\") {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArgOrOptions)\n throw new Error(\"endpoints is required when prefix is provided\");\n endpoints = endpointsArgOrOptions as RouterEndpoints;\n options = optionsArg;\n } else if (\n \"prefix\" in routerOrPrefixOrEndpoints &&\n \"endpoints\" in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).endpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n } else {\n prefix = \"\";\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n }\n\n return buildClient(executor, prefix, endpoints, options);\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 if (\"prefix\" in entry && \"endpoints\" in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<RouterEndpoints>;\n client[key] = buildClient(\n executor,\n joinPaths(prefix, nested.prefix),\n nested.endpoints,\n options,\n );\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 && 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(\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 result: unknown;\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 if (spec.adapter) {\n return spec.adapter(result);\n }\n return result;\n };\n }\n }\n\n return client;\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 { 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 { 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}`, {\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[]): 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(\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 {\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/utils/params.ts","../src/create-fetch-executor.ts","../src/define-endpoint.ts","../src/middleware.ts"],"names":[],"mappings":";AAiCO,SAAS,YAAY,KAAA,EAAoD;AAC9E,EAAA,OAAO,QAAA,IAAY,SAAS,WAAA,IAAe,KAAA;AAC7C;AAEO,SAAS,YAAA,CACd,QACA,SAAA,EACuB;AACvB,EAAA,OAAO,EAAE,QAAQ,SAAA,EAAU;AAC7B;;;ACnCO,SAAS,aAAa,QAAA,EAA4B;AACvD,EAAA,MAAM,MAAA,GAAS,QAAA,CACZ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,KAAM,EAAE,CAAA,CACtB,IAAA,CAAK,GAAG,CAAA,CACR,OAAA,CAAQ,QAAQ,GAAG,CAAA;AACtB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,GAAG,CAAA,IAAK,MAAA,CAAO,MAAA,GAAS,CAAA,GAC3C,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAClB,MAAA,IAAU,GAAA;AAChB;AAEO,SAAS,WAAA,CACd,cACA,MAAA,EACQ;AACR,EAAA,IAAI,CAAC,QAAQ,OAAO,YAAA;AACpB,EAAA,OAAO,YAAA,CAAa,OAAA,CAAQ,4BAAA,EAA8B,CAAC,GAAG,GAAA,KAAQ;AACpE,IAAA,MAAM,KAAA,GAAQ,OAAO,GAAG,CAAA;AACxB,IAAA,IAAI,KAAA,IAAS,QAAQ,KAAA,KAAU,EAAA,QAAU,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,GAAG,CAAA,CAAE,CAAA;AACnF,IAAA,OAAO,kBAAA,CAAmB,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,EACzC,CAAC,CAAA;AACH;;;AC3BO,IAAM,eAAA,GAAN,cAA8B,KAAA,CAAM;AAAA,EAGzC,WAAA,CAAY,SAAiB,KAAA,EAAiB;AAC5C,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AACZ,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,MAAA,CAAO,cAAA,CAAe,MAAM,OAAA,EAAS;AAAA,QACnC,KAAA,EAAO,KAAA;AAAA,QACP,QAAA,EAAU,KAAA;AAAA,QACV,UAAA,EAAY,KAAA;AAAA,QACZ,YAAA,EAAc;AAAA,OACf,CAAA;AAAA,IACH;AAAA,EACF;AACF;;;ACkFO,SAAS,SAAA,CACd,QAAA,EACA,yBAAA,EAIA,qBAAA,EACA,UAAA,EACyB;AACzB,EAAA,MAAM,EAAE,MAAA,EAAQ,SAAA,EAAW,OAAA,EAAQ,GAAI,WAAA;AAAA,IACrC,yBAAA;AAAA,IACA,qBAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,OAAO,WAAA,CAAY,QAAA,EAAU,MAAA,EAAQ,SAAA,EAAW,OAAO,CAAA;AACzD;AAEA,SAAS,WAAA,CACP,MAAA,EACA,KAAA,EACA,MAAA,EAKA;AACA,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAI,CAAC,KAAA;AACH,MAAA,MAAM,IAAI,MAAM,+CAA+C,CAAA;AACjE,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,MAAA;AAAA,MACR,SAAA,EAAW,KAAA;AAAA,MACX,OAAA,EAAS;AAAA,KACX;AAAA,EACF;AACA,EAAA,IAAI,WAAA,CAAY,MAAM,CAAA,EAAG;AACvB,IAAA,OAAO;AAAA,MACL,QAAQ,MAAA,CAAO,MAAA;AAAA,MACf,WAAW,MAAA,CAAO,SAAA;AAAA,MAClB,OAAA,EAAS;AAAA,KACX;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,EAAA;AAAA,IACR,SAAA,EAAW,MAAA;AAAA,IACX,OAAA,EAAS;AAAA,GACX;AACF;AAEA,SAAS,cAAA,CACP,SACA,IAAA,EACS;AACT,EAAA,MAAM,IAAI,OAAA,EAAS,QAAA;AACnB,EAAA,IAAI,CAAA,KAAM,MAAA,IAAa,CAAA,KAAM,IAAA,EAAM,OAAO,IAAA;AAC1C,EAAA,IAAI,CAAA,KAAM,OAAO,OAAO,KAAA;AACxB,EAAA,OAAO,CAAA,CAAE,IAAI,CAAA,IAAK,IAAA;AACpB;AAEA,SAAS,WAAA,CACP,QAAA,EACA,MAAA,EACA,SAAA,EACA,OAAA,EACyB;AACzB,EAAA,MAAM,SAAkC,EAAC;AAEzC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,SAAS,CAAA,EAAG;AACpD,IAAA,MAAA,CAAO,GAAG,IAAI,WAAA,CAAY,KAAK,IAC3B,WAAA,CAAY,QAAA,EAAU,UAAU,MAAA,EAAQ,KAAA,CAAM,MAAM,CAAA,EAAG,KAAA,CAAM,WAAW,OAAO,CAAA,GAC/E,gBAAgB,QAAA,EAAU,MAAA,EAAQ,OAAsC,OAAO,CAAA;AAAA,EACrF;AAEA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,eAAA,CACP,QAAA,EACA,MAAA,EACA,IAAA,EACA,OAAA,EACA;AACA,EAAA,OAAO,OAAO,MAAA,GAAuB,EAAC,EAAG,MAAA,KAAyB;AAChE,IAAA,IAAI,eAAA,GAAgC,MAAA;AACpC,IAAA,IAAI,IAAA,CAAK,OAAA,IAAW,cAAA,CAAe,OAAA,EAAS,SAAS,CAAA,EAAG;AACtD,MAAA,IAAI;AACF,QAAA,eAAA,GAAkB,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,MAAM,CAAA;AAAA,MAC7C,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,IAAI,eAAA,CAAgB,2BAAA,EAA6B,GAAG,CAAA;AAAA,MAC5D;AAAA,IACF;AAEA,IAAA,MAAM,GAAA,GAAM,YAAY,SAAA,CAAU,MAAA,EAAQ,KAAK,IAAI,CAAA,EAAG,iBAAiB,IAAI,CAAA;AAE3E,IAAA,MAAM,GAAA,GAAM,MAAM,QAAA,CAAS,OAAA,CAAQ;AAAA,MACjC,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,GAAA;AAAA,MACA,QAAQ,eAAA,EAAiB,KAAA;AAAA,MACzB,MAAM,eAAA,EAAiB,IAAA;AAAA,MACvB;AAAA,KACD,CAAA;AAED,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI,cAAA,CAAe,OAAA,EAAS,UAAU,CAAA,EAAG;AACvC,MAAA,IAAI;AACF,QAAA,MAAA,GAAS,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA;AAAA,MAClC,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,IAAI,eAAA,CAAgB,4BAAA,EAA8B,GAAG,CAAA;AAAA,MAC7D;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAA,GAAS,GAAA;AAAA,IACX;AAEA,IAAA,OAAO,IAAA,CAAK,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA;AAAA,EAC/C,CAAA;AACF;;;AC7LO,SAAS,cAAA,CACd,OAAA,EACA,WAAA,GAAoC,EAAC,EAC3B;AACV,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,WAAA,CAExB,CAAC,IAAA,EAAM,EAAA,KAAO,CAAC,IAAA,KAAS,EAAA,CAAG,IAAA,EAAM,IAAI,CAAA,EAAG,OAAO,CAAA;AACjD,EAAA,OAAO,EAAE,SAAS,KAAA,EAAM;AAC1B;AA4BO,SAAS,iBACd,QAAA,EACU;AACV,EAAA,OAAO;AAAA,IACL,SAAS,CAAC,IAAA,KAAS,SAAS,IAAI,CAAA,CAAE,QAAQ,IAAI;AAAA,GAChD;AACF;;;ACjEO,SAAS,gBACd,MAAA,EACiB;AACjB,EAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjD,IAAA,IAAI,SAAS,IAAA,EAAM;AACnB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,IAAI,QAAQ,IAAA,EAAM,MAAA,CAAO,OAAO,GAAA,EAAK,MAAA,CAAO,IAAI,CAAC,CAAA;AAAA,MACnD;AAAA,IACF,CAAA,MAAA,IAAW,OAAO,KAAA,KAAU,QAAA,EAAU;AACpC,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,mCAAmC,GAAG,CAAA,kFAAA;AAAA,OACxC;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IAClC;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;;;ACYO,SAAS,mBAAA,CACd,SACA,OAAA,EAMU;AACV,EAAA,OAAO,cAAA;AAAA,IACL,OAAO,EAAE,MAAA,EAAQ,GAAA,EAAK,QAAQ,IAAA,EAAM,OAAA,EAAS,QAAO,KAAM;AACxD,MAAA,MAAM,OAAA,GAAU,IAAI,GAAA,CAAI,OAAA,CAAQ,QAAQ,KAAA,EAAO,EAAE,IAAI,GAAG,CAAA;AACxD,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,eAAA,CAAgB,MAAM,CAAA,CAAE,OAAA,CAAQ,CAAC,GAAG,CAAA,KAAM;AACxC,UAAA,OAAA,CAAQ,YAAA,CAAa,GAAA,CAAI,CAAA,EAAG,CAAC,CAAA;AAAA,QAC/B,CAAC,CAAA;AAAA,MACH;AAEA,MAAA,MAAM,cAAA,GAAkB,MAAM,OAAA,EAAS,cAAA,QAAuB,EAAC;AAE/D,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAS,EAAG;AAAA,QAC1C,MAAA;AAAA,QACA,OAAA,EAAS;AAAA,UACP,GAAG,cAAA;AAAA,UACH,GAAG,OAAA;AAAA,UACH,GAAI,IAAA,IAAQ,IAAA,GAAO,EAAE,cAAA,EAAgB,kBAAA,KAAuB;AAAC,SAC/D;AAAA,QACA,MAAM,IAAA,IAAQ,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,GAAI,MAAA;AAAA,QAC5C;AAAA,OACD,CAAA;AAED,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,MAAM,YAAY,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,IAAI,CAAA;AACnD,QAAA,MAAM,IAAI,SAAA,CAAU,GAAA,CAAI,MAAA,EAAQ,GAAA,CAAI,YAAY,SAAS,CAAA;AAAA,MAC3D;AACA,MAAA,IAAI,GAAA,CAAI,WAAW,GAAA,IAAO,GAAA,CAAI,WAAW,GAAA,IAAO,GAAA,CAAI,WAAW,GAAA,EAAK;AAClE,QAAA,OAAO,IAAA;AAAA,MACT;AACA,MAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,MAAA,OAAO,IAAA,KAAS,EAAA,GAAK,IAAA,GAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,IAC7C,CAAA;AAAA,IACA,OAAA,EAAS;AAAA,GACX;AACF;AAiBO,IAAM,SAAA,GAAN,cAAwB,KAAA,CAAM;AAAA,EACnC,WAAA,CAEkB,MAAA,EAEA,UAAA,EAEA,IAAA,GAAgB,IAAA,EAChC;AACA,IAAA,KAAA,CAAM,CAAA,KAAA,EAAQ,MAAM,CAAA,EAAA,EAAK,UAAU,CAAA,CAAE,CAAA;AANrB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAEA,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AAEA,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAGhB,IAAA,IAAA,CAAK,IAAA,GAAO,WAAA;AAAA,EACd;AACF;;;ACsBO,SAAS,SAAS,IAAA,EAAwB;AAC/C,EAAA,OAAO,IAAA;AACT;;;ACzHO,IAAM,YAAA,GAAN,cAA2B,KAAA,CAAM;AAAA,EACtC,YAA4B,EAAA,EAAY;AACtC,IAAA,KAAA,CAAM,CAAA,wBAAA,EAA2B,EAAE,CAAA,EAAA,CAAI,CAAA;AADb,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAE1B,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AAAA,EACd;AACF;AAeO,SAAS,iBAAiB,EAAA,EAA4C;AAC3E,EAAA,OAAO,EAAA;AACT;AAoBO,SAAS,SAAA,CACd,OACA,OAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,OAAO,IAAA,EAAM,IAAA,KAAS;AAC5C,IAAA,IAAI,SAAA;AACJ,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,KAAA,EAAO,OAAA,EAAA,EAAW;AACjD,MAAA,IAAI;AACF,QAAA,OAAO,MAAM,KAAK,IAAI,CAAA;AAAA,MACxB,SAAS,GAAA,EAAK;AACZ,QAAA,SAAA,GAAY,GAAA;AACZ,QAAA,IAAI,YAAY,KAAA,EAAO;AACvB,QAAA,IAAI,SAAS,WAAA,IAAe,CAAC,QAAQ,WAAA,CAAY,GAAA,EAAK,OAAO,CAAA,EAAG;AAAA,MAClE;AAAA,IACF;AACA,IAAA,MAAM,SAAA;AAAA,EACR,CAAC,CAAA;AACH;AAUO,SAAS,YAAY,EAAA,EAAgC;AAC1D,EAAA,OAAO,gBAAA,CAAiB,OAAO,IAAA,EAAM,IAAA,KAAS;AAC5C,IAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,CAAM,IAAI,YAAA,CAAa,EAAE,CAAC,CAAA,EAAG,EAAE,CAAA;AAEzE,IAAA,MAAM,EAAE,MAAA,EAAQ,OAAA,KAAY,IAAA,CAAK,MAAA,GAC7B,UAAU,CAAC,IAAA,CAAK,QAAQ,UAAA,CAAW,MAAM,CAAC,CAAA,GAC1C,EAAE,QAAQ,UAAA,CAAW,MAAA,EAAQ,SAAS,MAAM;AAAA,IAAC,CAAA,EAAE;AAEnD,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,IAAA,CAAK,EAAE,GAAG,IAAA,EAAM,QAAQ,CAAA;AAAA,IACvC,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,KAAK,CAAA;AAClB,MAAA,OAAA,EAAQ;AAAA,IACV;AAAA,EACF,CAAC,CAAA;AACH;AAYO,SAAS,WAAW,OAAA,EAEJ;AACrB,EAAA,MAAM,GAAA,GAAM,SAAS,GAAA,KAAQ,CAAC,KAAK,IAAA,KAAS,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,IAAI,CAAA,CAAA;AACjE,EAAA,OAAO,gBAAA,CAAiB,OAAO,IAAA,EAAM,IAAA,KAAS;AAC5C,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AACvB,IAAA,GAAA,CAAI,YAAY,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,GAAG,CAAA,CAAA,EAAI;AAAA,MACzC,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,MAAM,IAAA,CAAK;AAAA,KACZ,CAAA;AACD,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,IAAI,CAAA;AAC9B,MAAA,GAAA,CAAI,CAAA,SAAA,EAAY,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,GAAG,CAAA,QAAA,EAAM,IAAA,CAAK,GAAA,EAAI,GAAI,KAAK,CAAA,EAAA,CAAI,CAAA;AACnE,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,GAAA;AAAA,QACE,CAAA,SAAA,EAAY,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,GAAG,CAAA,oBAAA,EAAkB,IAAA,CAAK,GAAA,EAAI,GAAI,KAAK,CAAA,EAAA,CAAA;AAAA,QACvE;AAAA,OACF;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAC,CAAA;AACH;AAGA,SAAS,UAAU,OAAA,EAGjB;AACA,EAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,KAAA,EAAM;AACvC,EAAA,MAAM,WAA0B,EAAC;AACjC,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,IAAA,IAAI,EAAE,OAAA,EAAS;AACb,MAAA,UAAA,CAAW,KAAA,EAAM;AACjB,MAAA;AAAA,IACF;AACA,IAAA,CAAA,CAAE,iBAAiB,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AACnD,IAAA,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,EACjB;AACA,EAAA,OAAO;AAAA,IACL,QAAQ,UAAA,CAAW,MAAA;AAAA,IACnB,OAAA,EAAS,MACP,QAAA,CAAS,OAAA,CAAQ,CAAC,CAAA,KAAM;AACtB,MAAA,CAAA,CAAE,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAAA,IACxC,CAAC;AAAA,GACL;AACF","file":"index.js","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","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","import type { Executor, ExecutorMiddleware } from \"./types.js\";\nimport { createExecutor } from \"./create-executor.js\";\nimport { serializeParams } from \"./utils/params.js\";\n\n/**\n * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.\n *\n * Suited for SSR environments where you need per-request dynamic headers\n * (e.g. forwarding auth cookies) without sharing state across requests.\n *\n * - Query params are serialized and appended to the URL.\n * - A `Content-Type: application/json` header is added automatically when\n * a request body is present.\n * - Responses with status 204 or `Content-Length: 0` resolve to `null`.\n * - Non-2xx responses throw an {@link HttpError}.\n *\n * @param baseURL - Absolute base URL prepended to every endpoint path.\n * @param options.defaultHeaders - Async factory called on every request to\n * produce headers (e.g. reading cookies in a Next.js server component).\n * @param options.middlewares - Middleware chain applied before the fetch call.\n *\n * @example\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * defaultHeaders: async () => {\n * const token = await getServerToken();\n * return token ? { Authorization: `Bearer ${token}` } : {};\n * },\n * });\n * ```\n */\nexport function createFetchExecutor(\n baseURL: string,\n options?: {\n defaultHeaders?: () =>\n | Record<string, string>\n | Promise<Record<string, string>>;\n middlewares?: ExecutorMiddleware[];\n },\n): Executor {\n return createExecutor(\n async ({ method, url, params, body, headers, signal }) => {\n const fullURL = new URL(baseURL.replace(/\\/$/, \"\") + url);\n if (params) {\n serializeParams(params).forEach((v, k) => {\n fullURL.searchParams.set(k, v);\n });\n }\n\n const defaultHeaders = (await options?.defaultHeaders?.()) ?? {};\n\n const res = await fetch(fullURL.toString(), {\n method,\n headers: {\n ...defaultHeaders,\n ...headers,\n ...(body != null ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new HttpError(res.status, res.statusText, errorBody);\n }\n if (res.status === 204 || res.status === 205 || res.status === 304) {\n return null;\n }\n const text = await res.text();\n return text === \"\" ? null : JSON.parse(text);\n },\n options?.middlewares,\n );\n}\n\n/**\n * Thrown by {@link createFetchExecutor} when the server returns a non-2xx\n * status code.\n *\n * @example\n * ```ts\n * try {\n * await api.getDetail({ path: { id: 999 } });\n * } catch (err) {\n * if (err instanceof HttpError && err.status === 404) {\n * // handle not-found\n * }\n * }\n * ```\n */\nexport class HttpError extends Error {\n constructor(\n /** HTTP status code (e.g. 404, 500). */\n public readonly status: number,\n /** HTTP status text (e.g. \"Not Found\"). */\n public readonly statusText: string,\n /** Parsed response body, or `null` if the body was empty or not JSON. */\n public readonly body: unknown = null,\n ) {\n super(`HTTP ${status}: ${statusText}`);\n this.name = \"HttpError\";\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routar/core",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Schema-first HTTP API client — endpoint definitions, typed router, API client factory, and middleware system",
5
5
  "keywords": [
6
6
  "api",