@routar/core 1.5.0 → 1.7.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.js CHANGED
@@ -6,6 +6,96 @@ function defineRouter(prefix, endpoints) {
6
6
  return { prefix, endpoints };
7
7
  }
8
8
 
9
+ // src/middleware.ts
10
+ var TimeoutError = class extends Error {
11
+ constructor(ms) {
12
+ super(`Request timed out after ${ms}ms`);
13
+ this.ms = ms;
14
+ this.name = "TimeoutError";
15
+ }
16
+ };
17
+ function definePlugin(plugin) {
18
+ return plugin;
19
+ }
20
+ function logger(options) {
21
+ const log = options?.log ?? ((msg, data) => console.log(msg, data));
22
+ const timings = /* @__PURE__ */ new WeakMap();
23
+ return definePlugin({
24
+ name: "logger",
25
+ onRequest: (opts) => {
26
+ timings.set(opts, Date.now());
27
+ log(`[routar] ${opts.method} ${opts.url}`, {
28
+ params: opts.params,
29
+ body: opts.body
30
+ });
31
+ return opts;
32
+ },
33
+ onResponse: (res, opts) => {
34
+ log(
35
+ `[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - (timings.get(opts) ?? Date.now())}ms`
36
+ );
37
+ timings.delete(opts);
38
+ return res;
39
+ },
40
+ onError: (err, opts) => {
41
+ log(
42
+ `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,
43
+ err
44
+ );
45
+ timings.delete(opts);
46
+ throw err;
47
+ }
48
+ });
49
+ }
50
+ function withRetry(count, options) {
51
+ return async (opts, next) => {
52
+ let lastError;
53
+ for (let attempt = 0; attempt <= count; attempt++) {
54
+ try {
55
+ return await next(opts);
56
+ } catch (err) {
57
+ lastError = err;
58
+ if (attempt === count) break;
59
+ if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;
60
+ }
61
+ }
62
+ throw lastError;
63
+ };
64
+ }
65
+ function withTimeout(ms) {
66
+ return async (opts, next) => {
67
+ const controller = new AbortController();
68
+ const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
69
+ const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
70
+ } };
71
+ try {
72
+ return await next({ ...opts, signal });
73
+ } finally {
74
+ clearTimeout(timer);
75
+ cleanup();
76
+ }
77
+ };
78
+ }
79
+ function anySignal(signals) {
80
+ const controller = new AbortController();
81
+ const onAbort = () => controller.abort();
82
+ const attached = [];
83
+ for (const s of signals) {
84
+ if (s.aborted) {
85
+ controller.abort();
86
+ break;
87
+ }
88
+ s.addEventListener("abort", onAbort, { once: true });
89
+ attached.push(s);
90
+ }
91
+ return {
92
+ signal: controller.signal,
93
+ cleanup: () => attached.forEach((s) => {
94
+ s.removeEventListener("abort", onAbort);
95
+ })
96
+ };
97
+ }
98
+
9
99
  // src/utils/path.ts
10
100
  function joinPaths(...segments) {
11
101
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -35,6 +125,26 @@ var ValidationError = class extends Error {
35
125
  }
36
126
  }
37
127
  };
128
+ var StandardSchemaError = class extends Error {
129
+ constructor(issues) {
130
+ super(
131
+ issues.map((issue) => issue.message).join("; ") || "Standard Schema validation failed"
132
+ );
133
+ this.name = "StandardSchemaError";
134
+ this.issues = issues;
135
+ }
136
+ };
137
+
138
+ // src/utils/run-validator.ts
139
+ async function runValidator(validator, data) {
140
+ if (typeof validator.parse === "function") {
141
+ return validator.parse(data);
142
+ }
143
+ const standard = validator["~standard"];
144
+ const result = await standard.validate(data);
145
+ if (result.issues) throw new StandardSchemaError(result.issues);
146
+ return result.value;
147
+ }
38
148
 
39
149
  // src/create-api.ts
40
150
  function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
@@ -73,11 +183,10 @@ function resolveArgs(second, third, fourth) {
73
183
  options: third
74
184
  };
75
185
  }
76
- function shouldValidate(options, kind) {
186
+ function validateMode(options, kind) {
77
187
  const v = options?.validate;
78
- if (v === void 0 || v === true) return true;
79
- if (v === false) return false;
80
- return v[kind] ?? true;
188
+ const mode = v === void 0 ? true : typeof v === "boolean" || v === "warn" ? v : v[kind] ?? true;
189
+ return mode === "warn" ? "warn" : mode ? "on" : "off";
81
190
  }
82
191
  function buildClient(executor, prefix, endpoints, options) {
83
192
  const client = {};
@@ -97,39 +206,117 @@ function buildClient(executor, prefix, endpoints, options) {
97
206
  return client;
98
207
  }
99
208
  function buildEndpointFn(executor, prefix, spec, options) {
100
- return async (params = {}, signal) => {
209
+ return async (params = {}, signalOrOptions) => {
210
+ const call = normalizeCallOptions(signalOrOptions);
211
+ const reqMode = validateMode(options, "request");
101
212
  let validatedParams = params;
102
- if (spec.request && shouldValidate(options, "request")) {
213
+ let requestError = null;
214
+ if (spec.request && reqMode !== "off") {
103
215
  try {
104
- validatedParams = spec.request.parse(params);
216
+ validatedParams = await runValidator(
217
+ spec.request,
218
+ params
219
+ );
105
220
  } catch (err) {
106
- throw new ValidationError("Request validation failed", err);
221
+ requestError = new ValidationError("Request validation failed", err);
222
+ if (reqMode !== "warn") throw requestError;
223
+ validatedParams = params;
107
224
  }
108
225
  }
109
226
  const url = resolvePath(
110
227
  joinPaths(prefix, spec.path),
111
228
  validatedParams?.path
112
229
  );
113
- const raw = await executor.execute({
230
+ if (requestError) {
231
+ options?.onValidationError?.(requestError, {
232
+ kind: "request",
233
+ method: spec.method,
234
+ url,
235
+ data: params
236
+ });
237
+ }
238
+ const raw = await executeWithTimeout(executor, call.timeout, {
114
239
  method: spec.method,
115
240
  url,
116
241
  params: validatedParams?.query,
117
242
  body: validatedParams?.body,
118
- signal
243
+ headers: call.headers,
244
+ signal: call.signal
119
245
  });
246
+ const resMode = validateMode(options, "response");
120
247
  let result;
121
- if (shouldValidate(options, "response")) {
248
+ if (resMode === "off") {
249
+ result = raw;
250
+ } else {
122
251
  try {
123
- result = spec.response.parse(raw);
252
+ result = await runValidator(
253
+ spec.response,
254
+ raw
255
+ );
124
256
  } catch (err) {
125
- throw new ValidationError("Response validation failed", err);
257
+ const responseError = new ValidationError(
258
+ "Response validation failed",
259
+ err
260
+ );
261
+ if (resMode !== "warn") throw responseError;
262
+ options?.onValidationError?.(responseError, {
263
+ kind: "response",
264
+ method: spec.method,
265
+ url,
266
+ data: raw
267
+ });
268
+ result = raw;
126
269
  }
127
- } else {
128
- result = raw;
129
270
  }
130
271
  return spec.adapter ? spec.adapter(result) : result;
131
272
  };
132
273
  }
274
+ function normalizeCallOptions(arg) {
275
+ if (arg == null) return {};
276
+ if (isAbortSignal(arg)) return { signal: arg };
277
+ return arg;
278
+ }
279
+ function isAbortSignal(value) {
280
+ if (typeof AbortSignal !== "undefined" && value instanceof AbortSignal) {
281
+ return true;
282
+ }
283
+ return typeof value === "object" && value !== null && typeof value.aborted === "boolean" && typeof value.addEventListener === "function";
284
+ }
285
+ function executeWithTimeout(executor, timeout, opts) {
286
+ if (timeout == null) return executor.execute(opts);
287
+ const controller = new AbortController();
288
+ const timer = setTimeout(
289
+ () => controller.abort(new TimeoutError(timeout)),
290
+ timeout
291
+ );
292
+ const { signal, cleanup } = combineSignals(opts.signal, controller.signal);
293
+ return (async () => {
294
+ try {
295
+ return await executor.execute({ ...opts, signal });
296
+ } finally {
297
+ clearTimeout(timer);
298
+ cleanup();
299
+ }
300
+ })();
301
+ }
302
+ function combineSignals(caller, timeoutSignal) {
303
+ if (!caller) return { signal: timeoutSignal, cleanup: () => {
304
+ } };
305
+ if (caller.aborted) return { signal: caller, cleanup: () => {
306
+ } };
307
+ const controller = new AbortController();
308
+ const onCallerAbort = () => controller.abort(caller.reason);
309
+ const onTimeoutAbort = () => controller.abort(timeoutSignal.reason);
310
+ caller.addEventListener("abort", onCallerAbort, { once: true });
311
+ timeoutSignal.addEventListener("abort", onTimeoutAbort, { once: true });
312
+ return {
313
+ signal: controller.signal,
314
+ cleanup: () => {
315
+ caller.removeEventListener("abort", onCallerAbort);
316
+ timeoutSignal.removeEventListener("abort", onTimeoutAbort);
317
+ }
318
+ };
319
+ }
133
320
 
134
321
  // src/create-executor.ts
135
322
  function pluginToMiddleware(plugin) {
@@ -154,7 +341,12 @@ function buildChain(execute, middlewares) {
154
341
  );
155
342
  }
156
343
  function createExecutor(execute, options = {}) {
157
- const middlewares = (options.plugins ?? []).map(pluginToMiddleware);
344
+ const plugins = [...options.plugins ?? []];
345
+ if (options.unwrap) {
346
+ const unwrapFn = options.unwrap;
347
+ plugins.push({ onResponse: (raw) => unwrapFn(raw) });
348
+ }
349
+ const middlewares = plugins.map(pluginToMiddleware);
158
350
  return { execute: buildChain(execute, middlewares) };
159
351
  }
160
352
  function dispatchExecutor(resolver) {
@@ -163,96 +355,6 @@ function dispatchExecutor(resolver) {
163
355
  };
164
356
  }
165
357
 
166
- // src/middleware.ts
167
- var TimeoutError = class extends Error {
168
- constructor(ms) {
169
- super(`Request timed out after ${ms}ms`);
170
- this.ms = ms;
171
- this.name = "TimeoutError";
172
- }
173
- };
174
- function definePlugin(plugin) {
175
- return plugin;
176
- }
177
- function logger(options) {
178
- const log = options?.log ?? ((msg, data) => console.log(msg, data));
179
- const timings = /* @__PURE__ */ new WeakMap();
180
- return definePlugin({
181
- name: "logger",
182
- onRequest: (opts) => {
183
- timings.set(opts, Date.now());
184
- log(`[routar] ${opts.method} ${opts.url}`, {
185
- params: opts.params,
186
- body: opts.body
187
- });
188
- return opts;
189
- },
190
- onResponse: (res, opts) => {
191
- log(
192
- `[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - (timings.get(opts) ?? Date.now())}ms`
193
- );
194
- timings.delete(opts);
195
- return res;
196
- },
197
- onError: (err, opts) => {
198
- log(
199
- `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,
200
- err
201
- );
202
- timings.delete(opts);
203
- throw err;
204
- }
205
- });
206
- }
207
- function withRetry(count, options) {
208
- return async (opts, next) => {
209
- let lastError;
210
- for (let attempt = 0; attempt <= count; attempt++) {
211
- try {
212
- return await next(opts);
213
- } catch (err) {
214
- lastError = err;
215
- if (attempt === count) break;
216
- if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;
217
- }
218
- }
219
- throw lastError;
220
- };
221
- }
222
- function withTimeout(ms) {
223
- return async (opts, next) => {
224
- const controller = new AbortController();
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
- } };
228
- try {
229
- return await next({ ...opts, signal });
230
- } finally {
231
- clearTimeout(timer);
232
- cleanup();
233
- }
234
- };
235
- }
236
- function anySignal(signals) {
237
- const controller = new AbortController();
238
- const onAbort = () => controller.abort();
239
- const attached = [];
240
- for (const s of signals) {
241
- if (s.aborted) {
242
- controller.abort();
243
- break;
244
- }
245
- s.addEventListener("abort", onAbort, { once: true });
246
- attached.push(s);
247
- }
248
- return {
249
- signal: controller.signal,
250
- cleanup: () => attached.forEach((s) => {
251
- s.removeEventListener("abort", onAbort);
252
- })
253
- };
254
- }
255
-
256
358
  // src/utils/params.ts
257
359
  function serializeParams(params) {
258
360
  const result = new URLSearchParams();
@@ -293,7 +395,8 @@ function createFetchExecutor(baseURL, options) {
293
395
  headers,
294
396
  signal
295
397
  }) => {
296
- const fullURL = new URL(baseURL.replace(/\/$/, "") + url);
398
+ const resolvedBase = typeof baseURL === "function" ? await baseURL() : baseURL;
399
+ const fullURL = new URL(resolvedBase.replace(/\/$/, "") + url);
297
400
  if (params) {
298
401
  serializeParams(params).forEach((v, k) => {
299
402
  fullURL.searchParams.set(k, v);
@@ -312,7 +415,7 @@ function createFetchExecutor(baseURL, options) {
312
415
  });
313
416
  if (!res.ok) {
314
417
  const errorBody = await res.json().catch(() => null);
315
- throw new HttpError(res.status, res.statusText, errorBody);
418
+ throw new HttpError(res.status, res.statusText, errorBody, { url: fullURL.toString(), method });
316
419
  }
317
420
  if (res.status === 204 || res.status === 205 || res.status === 304) {
318
421
  return null;
@@ -320,24 +423,95 @@ function createFetchExecutor(baseURL, options) {
320
423
  const text = await res.text();
321
424
  return text === "" ? null : JSON.parse(text);
322
425
  };
323
- const executor = createExecutor(transport, { plugins: options?.plugins });
426
+ const executor = createExecutor(transport, { plugins: options?.plugins, unwrap: options?.unwrap });
324
427
  return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };
325
428
  }
326
429
  var HttpError = class extends Error {
327
- constructor(status, statusText, body = null) {
328
- super(`HTTP ${status}: ${statusText}`);
430
+ constructor(status, statusText, body = null, options) {
431
+ super(`HTTP ${status}: ${statusText}`, { cause: options?.cause });
329
432
  this.status = status;
330
433
  this.statusText = statusText;
331
434
  this.body = body;
332
435
  this.name = "HttpError";
436
+ this.url = options?.url;
437
+ this.method = options?.method;
333
438
  }
334
439
  };
335
440
 
441
+ // src/utils/compose-request.ts
442
+ var BUCKET_KEYS = ["path", "query", "body"];
443
+ function hasParse(v) {
444
+ return typeof v.parse === "function";
445
+ }
446
+ async function validateBucket(validator, value) {
447
+ if (hasParse(validator)) {
448
+ try {
449
+ return { value: validator.parse(value) };
450
+ } catch (err) {
451
+ const message = err instanceof Error ? err.message : String(err);
452
+ return { issues: [{ message }] };
453
+ }
454
+ }
455
+ return validator["~standard"].validate(value);
456
+ }
457
+ function composeRequest(buckets) {
458
+ const entries = BUCKET_KEYS.filter(
459
+ (k) => buckets[k] !== void 0
460
+ ).map((k) => [k, buckets[k]]);
461
+ const shape = {};
462
+ for (const [k, v] of entries) shape[k] = v;
463
+ const standard = {
464
+ "~standard": {
465
+ version: 1,
466
+ vendor: "routar",
467
+ validate: async (data) => {
468
+ const input = data ?? {};
469
+ const out = {};
470
+ const issues = [];
471
+ for (const [k, v] of entries) {
472
+ const result = await validateBucket(v, input[k]);
473
+ if (result.issues) {
474
+ for (const issue of result.issues) {
475
+ issues.push({ message: issue.message, path: [k, ...issue.path ?? []] });
476
+ }
477
+ } else {
478
+ out[k] = result.value;
479
+ }
480
+ }
481
+ return issues.length > 0 ? { issues } : { value: out };
482
+ }
483
+ }
484
+ };
485
+ if (entries.every(([, v]) => hasParse(v))) {
486
+ const parse = (data) => {
487
+ const input = data ?? {};
488
+ const out = {};
489
+ for (const [k, v] of entries) {
490
+ out[k] = v.parse(input[k]);
491
+ }
492
+ return out;
493
+ };
494
+ return Object.assign(standard, { parse, shape });
495
+ }
496
+ return Object.assign(standard, { shape });
497
+ }
498
+
336
499
  // src/define-endpoint.ts
337
500
  function endpoint(spec) {
501
+ if (spec.request === void 0 && (spec.pathParams !== void 0 || spec.query !== void 0 || spec.body !== void 0)) {
502
+ const { pathParams, query, body, ...rest } = spec;
503
+ return {
504
+ ...rest,
505
+ request: composeRequest({
506
+ path: pathParams,
507
+ query,
508
+ body
509
+ })
510
+ };
511
+ }
338
512
  return spec;
339
513
  }
340
514
 
341
- export { HttpError, TimeoutError, ValidationError, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, serializeParams };
515
+ export { HttpError, StandardSchemaError, TimeoutError, ValidationError, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, runValidator, serializeParams };
342
516
  //# sourceMappingURL=index.js.map
343
517
  //# sourceMappingURL=index.js.map