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