arthron 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -49,12 +49,18 @@ function serializeBody(body, headers) {
49
49
  */
50
50
  var ArthronError = class extends Error {
51
51
  name = "ArthronError";
52
+ /** HTTP status of the failed response; `0` when fetch itself failed (network). */
52
53
  status;
53
54
  statusText;
55
+ /** Response headers; empty for network failures. */
54
56
  headers;
57
+ /** Parsed body: JSON if it parses, else the text, else `undefined`. */
55
58
  data;
59
+ /** The context that was sent — url and init after request interceptors. */
56
60
  request;
61
+ /** The original Response, still readable (the body was parsed from a clone). Absent for network failures. */
57
62
  response;
63
+ /** The underlying error for network failures. */
58
64
  cause;
59
65
  constructor(init) {
60
66
  super(init.message);
@@ -67,6 +73,7 @@ var ArthronError = class extends Error {
67
73
  this.cause = init.cause;
68
74
  }
69
75
  };
76
+ /** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
70
77
  function isArthronError(error) {
71
78
  return error instanceof ArthronError;
72
79
  }
@@ -95,7 +102,7 @@ async function readBody(response) {
95
102
  }
96
103
  }
97
104
  async function responseError(response, request, resolve = defaultErrorMessage) {
98
- const data = await readBody(response);
105
+ const data = await readBody(response.clone());
99
106
  return new ArthronError({
100
107
  message: resolve(data, response) ?? `Request failed with status ${response.status}`,
101
108
  status: response.status,
@@ -196,7 +203,101 @@ async function runPipeline(plugins, ctx, send) {
196
203
  return dispatch(0, ctx);
197
204
  }
198
205
  //#endregion
206
+ //#region src/stream.ts
207
+ /** Yield the body line by line, reassembling lines split across chunks. */
208
+ async function* lines(response) {
209
+ if (!response.body) return;
210
+ const reader = response.body.getReader();
211
+ const decoder = new TextDecoder();
212
+ let buffer = "";
213
+ try {
214
+ while (true) {
215
+ const { value, done } = await reader.read();
216
+ buffer += decoder.decode(value, { stream: !done });
217
+ const parts = buffer.split(/\r\n|\r|\n/);
218
+ buffer = parts.pop() ?? "";
219
+ for (const line of parts) yield line;
220
+ if (done) break;
221
+ }
222
+ if (buffer) yield buffer;
223
+ } finally {
224
+ reader.releaseLock();
225
+ }
226
+ }
227
+ /**
228
+ * Parse a `text/event-stream` body into events. Follows the spec: a blank
229
+ * line dispatches, `data:` lines are joined with `\n`, one leading space
230
+ * after the colon is dropped, lines starting with `:` are comments, and a
231
+ * block without `data` is not dispatched. Provider sentinels like
232
+ * `[DONE]` are passed through untouched — they are not part of the spec.
233
+ */
234
+ async function* parseSse(response) {
235
+ let data = [];
236
+ let event;
237
+ let id;
238
+ let retry;
239
+ const flush = () => {
240
+ if (data.length === 0) {
241
+ event = id = retry = void 0;
242
+ return;
243
+ }
244
+ const out = { data: data.join("\n") };
245
+ if (event !== void 0) out.event = event;
246
+ if (id !== void 0) out.id = id;
247
+ if (retry !== void 0) out.retry = retry;
248
+ data = [];
249
+ event = id = retry = void 0;
250
+ return out;
251
+ };
252
+ for await (const line of lines(response)) {
253
+ if (line === "") {
254
+ const out = flush();
255
+ if (out) yield out;
256
+ continue;
257
+ }
258
+ if (line.startsWith(":")) continue;
259
+ const colon = line.indexOf(":");
260
+ const field = colon === -1 ? line : line.slice(0, colon);
261
+ let value = colon === -1 ? "" : line.slice(colon + 1);
262
+ if (value.startsWith(" ")) value = value.slice(1);
263
+ switch (field) {
264
+ case "data":
265
+ data.push(value);
266
+ break;
267
+ case "event":
268
+ event = value;
269
+ break;
270
+ case "id":
271
+ id = value;
272
+ break;
273
+ case "retry": {
274
+ const ms = Number(value);
275
+ if (Number.isInteger(ms) && ms >= 0) retry = ms;
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ const out = flush();
281
+ if (out) yield out;
282
+ }
283
+ /**
284
+ * Parse newline-delimited JSON. Blank lines are skipped; a malformed line
285
+ * rejects the iteration with the `SyntaxError` from `JSON.parse`.
286
+ */
287
+ async function* parseNdjson(response) {
288
+ for await (const line of lines(response)) {
289
+ if (line.trim() === "") continue;
290
+ yield JSON.parse(line);
291
+ }
292
+ }
293
+ //#endregion
199
294
  //#region src/promise.ts
295
+ /** Defer the Response until iteration starts, so the helper itself never rejects. */
296
+ function iterate(promise, parse) {
297
+ return { async *[Symbol.asyncIterator]() {
298
+ yield* parse(await promise);
299
+ } };
300
+ }
200
301
  /**
201
302
  * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting
202
303
  * without a helper still yields the untouched `Response`.
@@ -207,7 +308,9 @@ function toArthronPromise(promise) {
207
308
  text: () => promise.then((res) => res.text()),
208
309
  blob: () => promise.then((res) => res.blob()),
209
310
  arrayBuffer: () => promise.then((res) => res.arrayBuffer()),
210
- formData: () => promise.then((res) => res.formData())
311
+ formData: () => promise.then((res) => res.formData()),
312
+ sse: () => iterate(promise, parseSse),
313
+ ndjson: () => iterate(promise, parseNdjson)
211
314
  });
212
315
  }
213
316
  //#endregion
@@ -234,15 +337,18 @@ function resolveUrl(baseURL, path, params) {
234
337
  let url;
235
338
  if (isAbsoluteUrl(path)) url = path;
236
339
  else {
237
- if (baseURL === void 0) throw new TypeError(`arthron: relative path "${path}" requires a baseURL`);
340
+ if (baseURL === void 0) throw new TypeError(`arthron: relative path "${path}" needs a baseURL — pass baseURL in arthron({ baseURL }) or use an absolute URL`);
238
341
  url = `${baseURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
239
342
  }
240
343
  const query = toQueryString(params);
241
344
  if (!query) return url;
242
- return `${url}${url.includes("?") ? "&" : "?"}${query}`;
345
+ const hash = url.indexOf("#");
346
+ const [base, fragment] = hash === -1 ? [url, ""] : [url.slice(0, hash), url.slice(hash)];
347
+ return `${base}${base.includes("?") ? "&" : "?"}${query}${fragment}`;
243
348
  }
244
349
  //#endregion
245
350
  //#region src/client.ts
351
+ const BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
246
352
  /** Normalize once so `extend` can merge headers and append plugins cheaply. */
247
353
  function resolveConfig(config) {
248
354
  const { headers, use, ...rest } = config;
@@ -258,9 +364,9 @@ function resolveConfig(config) {
258
364
  */
259
365
  function arthron(config = {}) {
260
366
  const resolved = resolveConfig(config);
261
- const fetchImpl = resolved.fetch ?? globalThis.fetch;
262
367
  /** The one place a failure becomes an ArthronError. Cancellation passes through. */
263
368
  const send = async (ctx) => {
369
+ const fetchImpl = resolved.fetch ?? globalThis.fetch;
264
370
  let res;
265
371
  try {
266
372
  res = await fetchImpl(ctx.url, ctx.init);
@@ -273,6 +379,7 @@ function arthron(config = {}) {
273
379
  };
274
380
  const dispatch = (path, method, rawBody, { params, headers: callHeaders, ...init }) => {
275
381
  try {
382
+ if (rawBody !== void 0 && BODYLESS_METHODS.has(method.toUpperCase())) throw new TypeError(`arthron: ${method.toUpperCase()} requests cannot carry a body — send it as params (query) or switch to POST`);
276
383
  const url = resolveUrl(resolved.baseURL, path, params);
277
384
  const { body, headers } = serializeBody(rawBody, mergeHeaders(resolved.headers, callHeaders));
278
385
  const requestInit = {
@@ -307,7 +414,7 @@ function arthron(config = {}) {
307
414
  patch: (path, body, init = {}) => dispatch(path, "PATCH", body, init),
308
415
  request: (path, init) => {
309
416
  const { method, body, ...rest } = init;
310
- if (!method) return toArthronPromise(Promise.reject(/* @__PURE__ */ new TypeError("arthron: request() requires init.method")));
417
+ if (!method) return toArthronPromise(Promise.reject(/* @__PURE__ */ new TypeError("arthron: request() needs init.method — e.g. request(path, { method: \"OPTIONS\" }); use get/post/... for the common verbs")));
311
418
  return dispatch(path, method, body, rest);
312
419
  },
313
420
  use: (...plugins) => derive({ use: plugins }),
@@ -316,41 +423,79 @@ function arthron(config = {}) {
316
423
  }
317
424
  //#endregion
318
425
  //#region src/define.ts
426
+ /** Identity helper: gives a middleware written in its own file the right parameter types. */
319
427
  const defineMiddleware = (middleware) => middleware;
428
+ /** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */
320
429
  const defineInterceptor = {
321
430
  request: (interceptor) => interceptor,
322
431
  response: (interceptor) => interceptor,
323
432
  error: (interceptor) => interceptor
324
433
  };
434
+ /** Identity helper: type-checks a plugin object literal without an annotation. */
325
435
  const definePlugin = (plugin) => plugin;
326
436
  //#endregion
327
437
  //#region src/plugins/logging.ts
438
+ /** Origin + pathname only — the query and userinfo tend to carry secrets. */
439
+ function displayPath(url) {
440
+ try {
441
+ const parsed = new URL(url);
442
+ return `${parsed.origin}${parsed.pathname}`;
443
+ } catch {
444
+ return url.split("?")[0] ?? url;
445
+ }
446
+ }
447
+ function consoleSink(logger) {
448
+ return (e) => {
449
+ if (e.phase === "start") logger.log(`[arthron] → ${e.method} ${e.path}`);
450
+ else if (e.phase === "success") logger.log(`[arthron] ← ${e.method} ${e.path} ${e.status} ${e.ms}ms`);
451
+ else logger.error(`[arthron] ✕ ${e.method} ${e.path} ${e.status ?? "-"} ${e.ms}ms`);
452
+ };
453
+ }
328
454
  /**
329
- * One line when a request starts, one when it settles, with status and
455
+ * One event when a request starts, one when it settles, with status and
330
456
  * elapsed time. Wraps the whole call, so retries are included in the time.
331
- * The query string is dropped — it tends to carry tokens.
457
+ * Default output is a console line per event; pass `onEvent` to get the
458
+ * structured event instead. Cancellations are not reported.
332
459
  */
333
460
  function logging(options = {}) {
334
- const { enabled = true, logger = console, now = () => performance.now() } = options;
461
+ const { enabled = true, logger = console, onEvent, now = () => performance.now() } = options;
462
+ const emit = onEvent ?? consoleSink(logger);
335
463
  return {
336
464
  name: "logging",
337
465
  middleware: async (ctx, next) => {
338
466
  if (!enabled) return next(ctx);
339
467
  const method = ctx.init.method ?? "GET";
340
- const path = ctx.url.split("?")[0];
468
+ const path = displayPath(ctx.url);
341
469
  const started = now();
342
- const elapsed = () => `${Math.round(now() - started)}ms`;
343
- logger.log(`[arthron] → ${method} ${path}`);
470
+ const ms = () => Math.round(now() - started);
471
+ emit({
472
+ phase: "start",
473
+ method,
474
+ path
475
+ });
344
476
  try {
345
477
  const res = await next(ctx);
346
- logger.log(`[arthron] ← ${method} ${path} ${res.status} ${elapsed()}`);
478
+ emit({
479
+ phase: "success",
480
+ method,
481
+ path,
482
+ status: res.status,
483
+ ms: ms()
484
+ });
347
485
  return res;
348
- } catch (thrown) {
349
- if (!isCancellation(thrown)) {
350
- const status = isArthronError(thrown) ? thrown.status : "-";
351
- logger.error(`[arthron] ✕ ${method} ${path} ${status} ${elapsed()}`);
486
+ } catch (error) {
487
+ if (!isCancellation(error)) {
488
+ const status = isArthronError(error) ? error.status : void 0;
489
+ emit({
490
+ phase: "error",
491
+ method,
492
+ path,
493
+ status,
494
+ ms: ms(),
495
+ error
496
+ });
352
497
  }
353
- throw thrown;
498
+ throw error;
354
499
  }
355
500
  }
356
501
  };
@@ -428,6 +573,85 @@ function retry(options = {}) {
428
573
  };
429
574
  }
430
575
  //#endregion
576
+ //#region src/plugins/timeout.ts
577
+ /**
578
+ * Wrap `body` so the deadline keeps running until it is fully read, and so an
579
+ * abort while reading surfaces as a stream error instead of a silent stall.
580
+ */
581
+ function guardBody(body, signal, finish) {
582
+ const reader = body.getReader();
583
+ return new ReadableStream({
584
+ start(controller) {
585
+ signal.addEventListener("abort", () => {
586
+ controller.error(signal.reason);
587
+ reader.cancel(signal.reason).catch(() => {});
588
+ }, { once: true });
589
+ },
590
+ async pull(controller) {
591
+ const { value, done } = await reader.read();
592
+ if (done) {
593
+ finish();
594
+ controller.close();
595
+ return;
596
+ }
597
+ controller.enqueue(value);
598
+ },
599
+ cancel(reason) {
600
+ finish();
601
+ return reader.cancel(reason);
602
+ }
603
+ });
604
+ }
605
+ /**
606
+ * Abort the request when it takes longer than `ms` — counted from the start
607
+ * of the attempt until the body has been fully read, so a server that sends
608
+ * headers and then stalls still trips it. Rejects with a `TimeoutError` (a
609
+ * `DOMException`, like `AbortSignal.timeout()` would give); it is not an
610
+ * `ArthronError`, so error interceptors and `retry` leave it alone.
611
+ *
612
+ * Built on `AbortController` + `setTimeout` rather than `AbortSignal.any` /
613
+ * `AbortSignal.timeout` so it runs on Node 18.
614
+ */
615
+ function timeout(ms) {
616
+ return {
617
+ name: "timeout",
618
+ middleware: async (ctx, next) => {
619
+ const controller = new AbortController();
620
+ const outer = ctx.init.signal;
621
+ const forward = () => controller.abort(outer?.reason);
622
+ if (outer?.aborted) forward();
623
+ else outer?.addEventListener("abort", forward, { once: true });
624
+ const timer = setTimeout(() => controller.abort(new DOMException(`arthron: request timed out after ${ms}ms`, "TimeoutError")), ms);
625
+ const finish = () => {
626
+ clearTimeout(timer);
627
+ outer?.removeEventListener("abort", forward);
628
+ };
629
+ let res;
630
+ try {
631
+ res = await next({
632
+ ...ctx,
633
+ init: {
634
+ ...ctx.init,
635
+ signal: controller.signal
636
+ }
637
+ });
638
+ } catch (thrown) {
639
+ finish();
640
+ throw thrown;
641
+ }
642
+ if (!res.body) {
643
+ finish();
644
+ return res;
645
+ }
646
+ return new Response(guardBody(res.body, controller.signal, finish), {
647
+ status: res.status,
648
+ statusText: res.statusText,
649
+ headers: res.headers
650
+ });
651
+ }
652
+ };
653
+ }
654
+ //#endregion
431
655
  exports.ArthronError = ArthronError;
432
656
  exports.arthron = arthron;
433
657
  exports.defineInterceptor = defineInterceptor;
@@ -436,5 +660,6 @@ exports.definePlugin = definePlugin;
436
660
  exports.isArthronError = isArthronError;
437
661
  exports.logging = logging;
438
662
  exports.retry = retry;
663
+ exports.timeout = timeout;
439
664
 
440
665
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/body.ts","../src/error.ts","../src/headers.ts","../src/pipeline.ts","../src/promise.ts","../src/url.ts","../src/client.ts","../src/define.ts","../src/plugins/logging.ts","../src/plugins/retry.ts"],"sourcesContent":["type Serialized = {\n body: BodyInit | undefined;\n headers: Record<string, string>;\n};\n\nconst RAW_CTORS = [\n \"FormData\",\n \"Blob\",\n \"ArrayBuffer\",\n \"URLSearchParams\",\n \"ReadableStream\",\n] as const;\n\n/** Bodies fetch already knows how to send. Content-Type is left to the caller / runtime. */\nfunction isRawBody(body: unknown): body is BodyInit {\n if (typeof body === \"string\") return true;\n if (ArrayBuffer.isView(body)) return true;\n return RAW_CTORS.some((name) => {\n const ctor = (globalThis as Record<string, unknown>)[name];\n return typeof ctor === \"function\" && body instanceof ctor;\n });\n}\n\n/**\n * Turn a caller-supplied body into something fetch accepts. Raw bodies pass\n * through untouched; anything else is JSON with `content-type` set — unless\n * the caller already set one (headers arrive lowercased, so a plain lookup is\n * enough).\n */\nexport function serializeBody(\n body: unknown,\n headers: Record<string, string>,\n): Serialized {\n if (body === undefined) return { body: undefined, headers };\n if (isRawBody(body)) return { body, headers };\n const withType =\n \"content-type\" in headers\n ? headers\n : { ...headers, \"content-type\": \"application/json\" };\n return { body: JSON.stringify(body), headers: withType };\n}\n","import type { RequestContext } from \"./types\";\n\nexport type ErrorMessageResolver = (\n data: unknown,\n response: Response,\n) => string | undefined;\n\ntype ArthronErrorInit = {\n message: string;\n status: number;\n statusText: string;\n headers: Headers;\n data: unknown;\n request: RequestContext;\n response?: Response;\n cause?: unknown;\n};\n\n/**\n * The only error arthron itself produces. Two sources: a response with\n * `ok === false`, or fetch rejecting outright (network failure, `status` 0).\n * Everything else — cancellation, interceptor bugs — is rethrown untouched.\n */\nexport class ArthronError extends Error {\n override readonly name = \"ArthronError\";\n readonly status: number;\n readonly statusText: string;\n readonly headers: Headers;\n readonly data: unknown;\n readonly request: RequestContext;\n readonly response: Response | undefined;\n override readonly cause: unknown;\n\n constructor(init: ArthronErrorInit) {\n super(init.message);\n this.status = init.status;\n this.statusText = init.statusText;\n this.headers = init.headers;\n this.data = init.data;\n this.request = init.request;\n this.response = init.response;\n this.cause = init.cause;\n }\n}\n\nexport function isArthronError(error: unknown): error is ArthronError {\n return error instanceof ArthronError;\n}\n\n/** Cancellation is not an HTTP failure. Matched by name so it works across realms. */\nexport function isCancellation(error: unknown): boolean {\n const name = (error as { name?: unknown } | null | undefined)?.name;\n return name === \"AbortError\" || name === \"TimeoutError\";\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\n/** `message` → `error` (string) → `error.message` → `detail`. */\nexport function defaultErrorMessage(data: unknown): string | undefined {\n if (!isRecord(data)) return undefined;\n if (typeof data.message === \"string\") return data.message;\n if (typeof data.error === \"string\") return data.error;\n if (isRecord(data.error) && typeof data.error.message === \"string\") {\n return data.error.message;\n }\n if (typeof data.detail === \"string\") return data.detail;\n return undefined;\n}\n\n/** Body as JSON if it parses, else as text, else `undefined` when empty. */\nasync function readBody(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => \"\");\n if (text === \"\") return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nexport async function responseError(\n response: Response,\n request: RequestContext,\n resolve: ErrorMessageResolver = defaultErrorMessage,\n): Promise<ArthronError> {\n const data = await readBody(response);\n return new ArthronError({\n message:\n resolve(data, response) ??\n `Request failed with status ${response.status}`,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n data,\n request,\n response,\n });\n}\n\nexport function networkError(\n cause: unknown,\n request: RequestContext,\n): ArthronError {\n return new ArthronError({\n message: cause instanceof Error ? cause.message : \"Network error\",\n status: 0,\n statusText: \"\",\n headers: new Headers(),\n data: undefined,\n request,\n cause,\n });\n}\n","/**\n * Normalize any `HeadersInit` (plain object, `Headers`, tuple array) into a\n * plain record. Keys come back lowercased — `Headers` does that for us.\n */\nexport function toHeaderRecord(\n headers: HeadersInit | undefined,\n): Record<string, string> {\n if (!headers) return {};\n return Object.fromEntries(new Headers(headers).entries());\n}\n\n/**\n * Layer `override` on top of `base`. Names are compared case-insensitively and\n * the override wins, so a caller's `content-type` replaces a default\n * `Content-Type` instead of fetch sending both values joined.\n */\nexport function mergeHeaders(\n base: HeadersInit | undefined,\n override: HeadersInit | undefined,\n): Record<string, string> {\n return { ...toHeaderRecord(base), ...toHeaderRecord(override) };\n}\n","import { isArthronError } from \"./error\";\nimport { toHeaderRecord } from \"./headers\";\nimport type {\n ErrorInterceptor,\n Middleware,\n Plugin,\n RequestContext,\n RequestInterceptor,\n ResponseInterceptor,\n} from \"./types\";\n\n/**\n * Interceptors mutate the context in place. A middleware that calls `next`\n * again (retry) must not see the previous attempt's changes, so every attempt\n * starts from a fresh copy.\n */\nfunction cloneContext({ url, init }: RequestContext): RequestContext {\n return { url, init: { ...init, headers: toHeaderRecord(init.headers) } };\n}\n\n/**\n * Owns the order of one request.\n *\n * Middleware wraps the whole call, outermost first in registration order.\n * Each attempt below it runs request interceptors → `send` → response\n * interceptors. Only an `ArthronError` reaches the error interceptors; whatever\n * they return is thrown. Anything else that is thrown — cancellation, a bug\n * in a plugin — propagates untouched.\n */\nexport async function runPipeline(\n plugins: readonly Plugin[],\n ctx: RequestContext,\n send: (ctx: RequestContext) => Promise<Response>,\n): Promise<Response> {\n const middlewares: Middleware[] = [];\n const request: RequestInterceptor[] = [];\n const response: ResponseInterceptor[] = [];\n const error: ErrorInterceptor[] = [];\n for (const plugin of plugins) {\n if (plugin.middleware) middlewares.push(plugin.middleware);\n if (plugin.request) request.push(plugin.request);\n if (plugin.response) response.push(plugin.response);\n if (plugin.error) error.push(plugin.error);\n }\n\n const attempt = async (input: RequestContext): Promise<Response> => {\n let current = cloneContext(input);\n try {\n for (const interceptor of request) current = await interceptor(current);\n let res = await send(current);\n for (const interceptor of response) res = await interceptor(res, current);\n return res;\n } catch (thrown) {\n if (!isArthronError(thrown)) throw thrown;\n let err = thrown;\n for (const interceptor of error) err = await interceptor(err, current);\n throw err;\n }\n };\n\n const dispatch = (\n index: number,\n input: RequestContext,\n ): Promise<Response> => {\n const middleware = middlewares[index];\n if (!middleware) return attempt(input);\n return middleware(input, (next) => dispatch(index + 1, next));\n };\n return dispatch(0, ctx);\n}\n","import type { ArthronPromise } from \"./types\";\n\n/**\n * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting\n * without a helper still yields the untouched `Response`.\n */\nexport function toArthronPromise(promise: Promise<Response>): ArthronPromise {\n return Object.assign(promise, {\n json: <T = unknown>() => promise.then((res) => res.json() as Promise<T>),\n text: () => promise.then((res) => res.text()),\n blob: () => promise.then((res) => res.blob()),\n arrayBuffer: () => promise.then((res) => res.arrayBuffer()),\n formData: () => promise.then((res) => res.formData()),\n });\n}\n","export type QueryParams = Record<\n string,\n string | number | boolean | null | undefined\n>;\n\nconst ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n\nexport function isAbsoluteUrl(path: string): boolean {\n return ABSOLUTE_URL.test(path);\n}\n\nfunction toQueryString(params: QueryParams | undefined): string {\n if (!params) return \"\";\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) continue;\n search.append(key, String(value));\n }\n return search.toString();\n}\n\n/**\n * Join `baseURL` and `path` by plain string concatenation so a path prefix in\n * the base (`/v1`) survives a leading slash in the path. WHATWG `new URL()`\n * would drop it. Absolute paths skip the base entirely.\n */\nexport function resolveUrl(\n baseURL: string | undefined,\n path: string,\n params?: QueryParams,\n): string {\n let url: string;\n if (isAbsoluteUrl(path)) {\n url = path;\n } else {\n if (baseURL === undefined) {\n throw new TypeError(\n `arthron: relative path \"${path}\" requires a baseURL`,\n );\n }\n url = `${baseURL.replace(/\\/+$/, \"\")}/${path.replace(/^\\/+/, \"\")}`;\n }\n const query = toQueryString(params);\n if (!query) return url;\n return `${url}${url.includes(\"?\") ? \"&\" : \"?\"}${query}`;\n}\n","import { serializeBody } from \"./body\";\nimport { isCancellation, networkError, responseError } from \"./error\";\nimport { mergeHeaders, toHeaderRecord } from \"./headers\";\nimport { runPipeline } from \"./pipeline\";\nimport { toArthronPromise } from \"./promise\";\nimport type {\n Arthron,\n ArthronConfig,\n ArthronPromise,\n Plugin,\n RequestContext,\n RequestInitWithBody,\n RequestOptions,\n} from \"./types\";\nimport { resolveUrl } from \"./url\";\n\ntype Resolved = Omit<ArthronConfig, \"headers\" | \"use\"> & {\n headers: Record<string, string>;\n use: readonly Plugin[];\n};\n\n/** Normalize once so `extend` can merge headers and append plugins cheaply. */\nfunction resolveConfig(config: ArthronConfig): Resolved {\n const { headers, use, ...rest } = config;\n return { ...rest, headers: toHeaderRecord(headers), use: use ?? [] };\n}\n\n/**\n * Build a client from config. The result is immutable — `use` and `extend`\n * return new clients and never touch the one they were called on.\n */\nexport function arthron(config: ArthronConfig = {}): Arthron {\n const resolved = resolveConfig(config);\n const fetchImpl = resolved.fetch ?? globalThis.fetch;\n\n /** The one place a failure becomes an ArthronError. Cancellation passes through. */\n const send = async (ctx: RequestContext): Promise<Response> => {\n let res: Response;\n try {\n res = await fetchImpl(ctx.url, ctx.init);\n } catch (thrown) {\n if (isCancellation(thrown)) throw thrown;\n throw networkError(thrown, ctx);\n }\n if (!res.ok) throw await responseError(res, ctx, resolved.errorMessage);\n return res;\n };\n\n const dispatch = (\n path: string,\n method: string,\n rawBody: unknown,\n { params, headers: callHeaders, ...init }: RequestOptions,\n ): ArthronPromise => {\n try {\n const url = resolveUrl(resolved.baseURL, path, params);\n const { body, headers } = serializeBody(\n rawBody,\n mergeHeaders(resolved.headers, callHeaders),\n );\n // Config credentials sit underneath so a per-call value wins, like every other option.\n const requestInit: RequestInit = {\n ...(resolved.credentials !== undefined && {\n credentials: resolved.credentials,\n }),\n ...init,\n method,\n headers,\n };\n if (body !== undefined) requestInit.body = body;\n return toArthronPromise(\n runPipeline(resolved.use, { url, init: requestInit }, send),\n );\n } catch (thrown) {\n // Argument errors (no baseURL, unserializable body) reject like everything else.\n return toArthronPromise(Promise.reject(thrown));\n }\n };\n\n const derive = (next: Partial<ArthronConfig>): Arthron => {\n const merged: ArthronConfig = {\n ...resolved,\n ...next,\n headers: mergeHeaders(resolved.headers, next.headers),\n use: [...resolved.use, ...(next.use ?? [])],\n };\n return arthron(merged);\n };\n\n return {\n get: (path, init = {}) => dispatch(path, \"GET\", undefined, init),\n delete: (path, init = {}) => dispatch(path, \"DELETE\", undefined, init),\n head: (path, init = {}) => dispatch(path, \"HEAD\", undefined, init),\n post: (path, body, init = {}) => dispatch(path, \"POST\", body, init),\n put: (path, body, init = {}) => dispatch(path, \"PUT\", body, init),\n patch: (path, body, init = {}) => dispatch(path, \"PATCH\", body, init),\n request: (path, init: RequestInitWithBody) => {\n const { method, body, ...rest } = init;\n if (!method) {\n return toArthronPromise(\n Promise.reject(\n new TypeError(\"arthron: request() requires init.method\"),\n ),\n );\n }\n return dispatch(path, method, body, rest);\n },\n use: (...plugins) => derive({ use: plugins }),\n extend: (next) => derive(next),\n };\n}\n","import type {\n ErrorInterceptor,\n Middleware,\n Plugin,\n RequestInterceptor,\n ResponseInterceptor,\n} from \"./types\";\n\n/*\n * Identity helpers. They do nothing at runtime; they exist so a plugin written\n * in its own file gets contextual typing without an explicit annotation.\n */\n\nexport const defineMiddleware = (middleware: Middleware): Middleware =>\n middleware;\n\nexport const defineInterceptor = {\n request: (interceptor: RequestInterceptor): RequestInterceptor => interceptor,\n response: (interceptor: ResponseInterceptor): ResponseInterceptor =>\n interceptor,\n error: (interceptor: ErrorInterceptor): ErrorInterceptor => interceptor,\n};\n\nexport const definePlugin = (plugin: Plugin): Plugin => plugin;\n","import { isArthronError, isCancellation } from \"../error\";\nimport type { Plugin } from \"../types\";\n\nexport type LoggingOptions = {\n /** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */\n enabled?: boolean;\n logger?: Pick<Console, \"log\" | \"error\">;\n /** Clock in ms. Injectable for tests. */\n now?: () => number;\n};\n\n/**\n * One line when a request starts, one when it settles, with status and\n * elapsed time. Wraps the whole call, so retries are included in the time.\n * The query string is dropped — it tends to carry tokens.\n */\nexport function logging(options: LoggingOptions = {}): Plugin {\n const {\n enabled = true,\n logger = console,\n now = () => performance.now(),\n } = options;\n return {\n name: \"logging\",\n middleware: async (ctx, next) => {\n if (!enabled) return next(ctx);\n const method = ctx.init.method ?? \"GET\";\n const path = ctx.url.split(\"?\")[0];\n const started = now();\n const elapsed = () => `${Math.round(now() - started)}ms`;\n\n logger.log(`[arthron] → ${method} ${path}`);\n try {\n const res = await next(ctx);\n logger.log(`[arthron] ← ${method} ${path} ${res.status} ${elapsed()}`);\n return res;\n } catch (thrown) {\n if (!isCancellation(thrown)) {\n const status = isArthronError(thrown) ? thrown.status : \"-\";\n logger.error(`[arthron] ✕ ${method} ${path} ${status} ${elapsed()}`);\n }\n throw thrown;\n }\n },\n };\n}\n","import { type ArthronError, isArthronError } from \"../error\";\nimport type { Plugin, RequestContext } from \"../types\";\n\nexport type RetryOptions = {\n /** Number of retries after the first attempt. Default 2 (3 requests total). */\n retries?: number;\n /** Statuses worth retrying. Network errors (status 0) always are. */\n retryOn?: readonly number[];\n /** Methods safe to repeat. Compared case-insensitively. */\n methods?: readonly string[];\n /** Backoff in ms for retry number `retry` (0-based). Default 300 · 2^retry. */\n delay?: (retry: number, error: ArthronError) => number;\n /** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */\n sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;\n /**\n * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not\n * retried at all. Default 60 000. Also guards `setTimeout`'s 2^31-1 ms cap,\n * past which timers fire immediately.\n */\n maxRetryAfter?: number;\n};\n\nconst DEFAULT_RETRY_ON = [408, 429, 500, 502, 503, 504] as const;\nconst DEFAULT_METHODS = [\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"] as const;\nconst DEFAULT_MAX_RETRY_AFTER = 60_000;\n\nfunction defaultSleep(ms: number, signal?: AbortSignal | null): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal?.reason);\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** `Retry-After` as ms — seconds or an HTTP-date. `undefined` when absent or unparsable. */\nfunction retryAfterMs(error: ArthronError): number | undefined {\n const value = error.headers.get(\"retry-after\")?.trim();\n if (!value) return undefined;\n const seconds = Number(value);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const at = Date.parse(value);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, at - Date.now());\n}\n\nfunction isStreamBody(ctx: RequestContext): boolean {\n return (\n typeof ReadableStream !== \"undefined\" &&\n ctx.init.body instanceof ReadableStream\n );\n}\n\n/**\n * Retry on network errors and the configured statuses by calling `next`\n * again — the whole attempt (request interceptors included) is re-run.\n * Only `ArthronError` is considered; cancellation and plugin bugs pass through.\n */\nexport function retry(options: RetryOptions = {}): Plugin {\n const {\n retries = 2,\n retryOn = DEFAULT_RETRY_ON,\n methods = DEFAULT_METHODS,\n delay = (retry) => 300 * 2 ** retry,\n sleep = defaultSleep,\n maxRetryAfter = DEFAULT_MAX_RETRY_AFTER,\n } = options;\n const allowed = new Set(methods.map((m) => m.toUpperCase()));\n\n return {\n name: \"retry\",\n middleware: async (ctx, next) => {\n const method = (ctx.init.method ?? \"GET\").toUpperCase();\n const eligible = allowed.has(method) && !isStreamBody(ctx);\n\n for (let attempt = 0; ; attempt += 1) {\n try {\n return await next(ctx);\n } catch (thrown) {\n if (!eligible || attempt >= retries) throw thrown;\n if (!isArthronError(thrown)) throw thrown;\n if (thrown.status !== 0 && !retryOn.includes(thrown.status)) {\n throw thrown;\n }\n if (ctx.init.signal?.aborted) throw thrown;\n const retryAfter = retryAfterMs(thrown);\n if (retryAfter !== undefined && retryAfter > maxRetryAfter) {\n throw thrown;\n }\n await sleep(retryAfter ?? delay(attempt, thrown), ctx.init.signal);\n }\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/body.ts","../src/error.ts","../src/headers.ts","../src/pipeline.ts","../src/stream.ts","../src/promise.ts","../src/url.ts","../src/client.ts","../src/define.ts","../src/plugins/logging.ts","../src/plugins/retry.ts","../src/plugins/timeout.ts"],"sourcesContent":["type Serialized = {\n body: BodyInit | undefined;\n headers: Record<string, string>;\n};\n\nconst RAW_CTORS = [\n \"FormData\",\n \"Blob\",\n \"ArrayBuffer\",\n \"URLSearchParams\",\n \"ReadableStream\",\n] as const;\n\n/** Bodies fetch already knows how to send. Content-Type is left to the caller / runtime. */\nfunction isRawBody(body: unknown): body is BodyInit {\n if (typeof body === \"string\") return true;\n if (ArrayBuffer.isView(body)) return true;\n return RAW_CTORS.some((name) => {\n const ctor = (globalThis as Record<string, unknown>)[name];\n return typeof ctor === \"function\" && body instanceof ctor;\n });\n}\n\n/**\n * Turn a caller-supplied body into something fetch accepts. Raw bodies pass\n * through untouched; anything else is JSON with `content-type` set — unless\n * the caller already set one (headers arrive lowercased, so a plain lookup is\n * enough).\n */\nexport function serializeBody(\n body: unknown,\n headers: Record<string, string>,\n): Serialized {\n if (body === undefined) return { body: undefined, headers };\n if (isRawBody(body)) return { body, headers };\n const withType =\n \"content-type\" in headers\n ? headers\n : { ...headers, \"content-type\": \"application/json\" };\n return { body: JSON.stringify(body), headers: withType };\n}\n","import type { RequestContext } from \"./types\";\n\nexport type ErrorMessageResolver = (\n data: unknown,\n response: Response,\n) => string | undefined;\n\ntype ArthronErrorInit = {\n message: string;\n status: number;\n statusText: string;\n headers: Headers;\n data: unknown;\n request: RequestContext;\n response?: Response;\n cause?: unknown;\n};\n\n/**\n * The only error arthron itself produces. Two sources: a response with\n * `ok === false`, or fetch rejecting outright (network failure, `status` 0).\n * Everything else — cancellation, interceptor bugs — is rethrown untouched.\n */\nexport class ArthronError extends Error {\n override readonly name = \"ArthronError\";\n /** HTTP status of the failed response; `0` when fetch itself failed (network). */\n readonly status: number;\n readonly statusText: string;\n /** Response headers; empty for network failures. */\n readonly headers: Headers;\n /** Parsed body: JSON if it parses, else the text, else `undefined`. */\n readonly data: unknown;\n /** The context that was sent — url and init after request interceptors. */\n readonly request: RequestContext;\n /** The original Response, still readable (the body was parsed from a clone). Absent for network failures. */\n readonly response: Response | undefined;\n /** The underlying error for network failures. */\n override readonly cause: unknown;\n\n constructor(init: ArthronErrorInit) {\n super(init.message);\n this.status = init.status;\n this.statusText = init.statusText;\n this.headers = init.headers;\n this.data = init.data;\n this.request = init.request;\n this.response = init.response;\n this.cause = init.cause;\n }\n}\n\n/** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */\nexport function isArthronError(error: unknown): error is ArthronError {\n return error instanceof ArthronError;\n}\n\n/** Cancellation is not an HTTP failure. Matched by name so it works across realms. */\nexport function isCancellation(error: unknown): boolean {\n const name = (error as { name?: unknown } | null | undefined)?.name;\n return name === \"AbortError\" || name === \"TimeoutError\";\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\n/** `message` → `error` (string) → `error.message` → `detail`. */\nexport function defaultErrorMessage(data: unknown): string | undefined {\n if (!isRecord(data)) return undefined;\n if (typeof data.message === \"string\") return data.message;\n if (typeof data.error === \"string\") return data.error;\n if (isRecord(data.error) && typeof data.error.message === \"string\") {\n return data.error.message;\n }\n if (typeof data.detail === \"string\") return data.detail;\n return undefined;\n}\n\n/** Body as JSON if it parses, else as text, else `undefined` when empty. */\nasync function readBody(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => \"\");\n if (text === \"\") return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nexport async function responseError(\n response: Response,\n request: RequestContext,\n resolve: ErrorMessageResolver = defaultErrorMessage,\n): Promise<ArthronError> {\n // Read from a clone so `error.response` stays usable by the caller.\n const data = await readBody(response.clone());\n return new ArthronError({\n message:\n resolve(data, response) ??\n `Request failed with status ${response.status}`,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n data,\n request,\n response,\n });\n}\n\nexport function networkError(\n cause: unknown,\n request: RequestContext,\n): ArthronError {\n return new ArthronError({\n message: cause instanceof Error ? cause.message : \"Network error\",\n status: 0,\n statusText: \"\",\n headers: new Headers(),\n data: undefined,\n request,\n cause,\n });\n}\n","/**\n * Normalize any `HeadersInit` (plain object, `Headers`, tuple array) into a\n * plain record. Keys come back lowercased — `Headers` does that for us.\n */\nexport function toHeaderRecord(\n headers: HeadersInit | undefined,\n): Record<string, string> {\n if (!headers) return {};\n return Object.fromEntries(new Headers(headers).entries());\n}\n\n/**\n * Layer `override` on top of `base`. Names are compared case-insensitively and\n * the override wins, so a caller's `content-type` replaces a default\n * `Content-Type` instead of fetch sending both values joined.\n */\nexport function mergeHeaders(\n base: HeadersInit | undefined,\n override: HeadersInit | undefined,\n): Record<string, string> {\n return { ...toHeaderRecord(base), ...toHeaderRecord(override) };\n}\n","import { isArthronError } from \"./error\";\nimport { toHeaderRecord } from \"./headers\";\nimport type {\n ErrorInterceptor,\n Middleware,\n Plugin,\n RequestContext,\n RequestInterceptor,\n ResponseInterceptor,\n} from \"./types\";\n\n/**\n * Interceptors mutate the context in place. A middleware that calls `next`\n * again (retry) must not see the previous attempt's changes, so every attempt\n * starts from a fresh copy.\n */\nfunction cloneContext({ url, init }: RequestContext): RequestContext {\n return { url, init: { ...init, headers: toHeaderRecord(init.headers) } };\n}\n\n/**\n * Owns the order of one request.\n *\n * Middleware wraps the whole call, outermost first in registration order.\n * Each attempt below it runs request interceptors → `send` → response\n * interceptors. Only an `ArthronError` reaches the error interceptors; whatever\n * they return is thrown. Anything else that is thrown — cancellation, a bug\n * in a plugin — propagates untouched.\n */\nexport async function runPipeline(\n plugins: readonly Plugin[],\n ctx: RequestContext,\n send: (ctx: RequestContext) => Promise<Response>,\n): Promise<Response> {\n const middlewares: Middleware[] = [];\n const request: RequestInterceptor[] = [];\n const response: ResponseInterceptor[] = [];\n const error: ErrorInterceptor[] = [];\n for (const plugin of plugins) {\n if (plugin.middleware) middlewares.push(plugin.middleware);\n if (plugin.request) request.push(plugin.request);\n if (plugin.response) response.push(plugin.response);\n if (plugin.error) error.push(plugin.error);\n }\n\n const attempt = async (input: RequestContext): Promise<Response> => {\n let current = cloneContext(input);\n try {\n for (const interceptor of request) current = await interceptor(current);\n let res = await send(current);\n for (const interceptor of response) res = await interceptor(res, current);\n return res;\n } catch (thrown) {\n if (!isArthronError(thrown)) throw thrown;\n let err = thrown;\n for (const interceptor of error) err = await interceptor(err, current);\n throw err;\n }\n };\n\n const dispatch = (\n index: number,\n input: RequestContext,\n ): Promise<Response> => {\n const middleware = middlewares[index];\n if (!middleware) return attempt(input);\n return middleware(input, (next) => dispatch(index + 1, next));\n };\n return dispatch(0, ctx);\n}\n","/** One server-sent event, as described by the WHATWG EventSource spec. */\nexport type SseEvent = {\n /** The `event:` field, when the server sent one. */\n event?: string;\n /** All `data:` lines of the block, joined with `\\n`. */\n data: string;\n /** The `id:` field, when present. */\n id?: string;\n /** The `retry:` field in ms, when present and numeric. */\n retry?: number;\n};\n\n/** Yield the body line by line, reassembling lines split across chunks. */\nasync function* lines(response: Response): AsyncGenerator<string> {\n if (!response.body) return;\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n while (true) {\n const { value, done } = await reader.read();\n buffer += decoder.decode(value, { stream: !done });\n const parts = buffer.split(/\\r\\n|\\r|\\n/);\n buffer = parts.pop() ?? \"\";\n for (const line of parts) yield line;\n if (done) break;\n }\n if (buffer) yield buffer;\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Parse a `text/event-stream` body into events. Follows the spec: a blank\n * line dispatches, `data:` lines are joined with `\\n`, one leading space\n * after the colon is dropped, lines starting with `:` are comments, and a\n * block without `data` is not dispatched. Provider sentinels like\n * `[DONE]` are passed through untouched — they are not part of the spec.\n */\nexport async function* parseSse(response: Response): AsyncGenerator<SseEvent> {\n let data: string[] = [];\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n\n const flush = (): SseEvent | undefined => {\n if (data.length === 0) {\n event = id = retry = undefined;\n return undefined;\n }\n const out: SseEvent = { data: data.join(\"\\n\") };\n if (event !== undefined) out.event = event;\n if (id !== undefined) out.id = id;\n if (retry !== undefined) out.retry = retry;\n data = [];\n event = id = retry = undefined;\n return out;\n };\n\n for await (const line of lines(response)) {\n if (line === \"\") {\n const out = flush();\n if (out) yield out;\n continue;\n }\n if (line.startsWith(\":\")) continue;\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n let value = colon === -1 ? \"\" : line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n switch (field) {\n case \"data\":\n data.push(value);\n break;\n case \"event\":\n event = value;\n break;\n case \"id\":\n id = value;\n break;\n case \"retry\": {\n const ms = Number(value);\n if (Number.isInteger(ms) && ms >= 0) retry = ms;\n break;\n }\n default:\n break; // unknown fields are ignored, per spec\n }\n }\n const out = flush();\n if (out) yield out;\n}\n\n/**\n * Parse newline-delimited JSON. Blank lines are skipped; a malformed line\n * rejects the iteration with the `SyntaxError` from `JSON.parse`.\n */\nexport async function* parseNdjson<T = unknown>(\n response: Response,\n): AsyncGenerator<T> {\n for await (const line of lines(response)) {\n if (line.trim() === \"\") continue;\n yield JSON.parse(line) as T;\n }\n}\n","import { parseNdjson, parseSse } from \"./stream\";\nimport type { ArthronPromise } from \"./types\";\n\n/** Defer the Response until iteration starts, so the helper itself never rejects. */\nfunction iterate<T>(\n promise: Promise<Response>,\n parse: (res: Response) => AsyncGenerator<T>,\n): AsyncIterable<T> {\n return {\n async *[Symbol.asyncIterator]() {\n yield* parse(await promise);\n },\n };\n}\n\n/**\n * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting\n * without a helper still yields the untouched `Response`.\n */\nexport function toArthronPromise(promise: Promise<Response>): ArthronPromise {\n return Object.assign(promise, {\n json: <T = unknown>() => promise.then((res) => res.json() as Promise<T>),\n text: () => promise.then((res) => res.text()),\n blob: () => promise.then((res) => res.blob()),\n arrayBuffer: () => promise.then((res) => res.arrayBuffer()),\n formData: () => promise.then((res) => res.formData()),\n sse: () => iterate(promise, parseSse),\n ndjson: <T = unknown>() => iterate(promise, parseNdjson<T>),\n });\n}\n","export type QueryParams = Record<\n string,\n string | number | boolean | null | undefined\n>;\n\nconst ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n\nexport function isAbsoluteUrl(path: string): boolean {\n return ABSOLUTE_URL.test(path);\n}\n\nfunction toQueryString(params: QueryParams | undefined): string {\n if (!params) return \"\";\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === null || value === undefined) continue;\n search.append(key, String(value));\n }\n return search.toString();\n}\n\n/**\n * Join `baseURL` and `path` by plain string concatenation so a path prefix in\n * the base (`/v1`) survives a leading slash in the path. WHATWG `new URL()`\n * would drop it. Absolute paths skip the base entirely.\n */\nexport function resolveUrl(\n baseURL: string | undefined,\n path: string,\n params?: QueryParams,\n): string {\n let url: string;\n if (isAbsoluteUrl(path)) {\n url = path;\n } else {\n if (baseURL === undefined) {\n throw new TypeError(\n `arthron: relative path \"${path}\" needs a baseURL — pass baseURL in arthron({ baseURL }) or use an absolute URL`,\n );\n }\n url = `${baseURL.replace(/\\/+$/, \"\")}/${path.replace(/^\\/+/, \"\")}`;\n }\n const query = toQueryString(params);\n if (!query) return url;\n // The query goes before any #fragment, or the server never sees it.\n const hash = url.indexOf(\"#\");\n const [base, fragment] =\n hash === -1 ? [url, \"\"] : [url.slice(0, hash), url.slice(hash)];\n return `${base}${base.includes(\"?\") ? \"&\" : \"?\"}${query}${fragment}`;\n}\n","import { serializeBody } from \"./body\";\nimport { isCancellation, networkError, responseError } from \"./error\";\nimport { mergeHeaders, toHeaderRecord } from \"./headers\";\nimport { runPipeline } from \"./pipeline\";\nimport { toArthronPromise } from \"./promise\";\nimport type {\n Arthron,\n ArthronConfig,\n ArthronPromise,\n Plugin,\n RequestContext,\n RequestInitWithBody,\n RequestOptions,\n} from \"./types\";\nimport { resolveUrl } from \"./url\";\n\nconst BODYLESS_METHODS = new Set([\"GET\", \"HEAD\"]);\n\ntype Resolved = Omit<ArthronConfig, \"headers\" | \"use\"> & {\n headers: Record<string, string>;\n use: readonly Plugin[];\n};\n\n/** Normalize once so `extend` can merge headers and append plugins cheaply. */\nfunction resolveConfig(config: ArthronConfig): Resolved {\n const { headers, use, ...rest } = config;\n return { ...rest, headers: toHeaderRecord(headers), use: use ?? [] };\n}\n\n/**\n * Build a client from config. The result is immutable — `use` and `extend`\n * return new clients and never touch the one they were called on.\n */\nexport function arthron(config: ArthronConfig = {}): Arthron {\n const resolved = resolveConfig(config);\n\n /** The one place a failure becomes an ArthronError. Cancellation passes through. */\n const send = async (ctx: RequestContext): Promise<Response> => {\n // Resolved per call, not per client: a global patched after the client was\n // created (msw's `server.listen()`, for one) must still be honoured.\n const fetchImpl = resolved.fetch ?? globalThis.fetch;\n let res: Response;\n try {\n res = await fetchImpl(ctx.url, ctx.init);\n } catch (thrown) {\n if (isCancellation(thrown)) throw thrown;\n throw networkError(thrown, ctx);\n }\n if (!res.ok) throw await responseError(res, ctx, resolved.errorMessage);\n return res;\n };\n\n const dispatch = (\n path: string,\n method: string,\n rawBody: unknown,\n { params, headers: callHeaders, ...init }: RequestOptions,\n ): ArthronPromise => {\n try {\n if (rawBody !== undefined && BODYLESS_METHODS.has(method.toUpperCase())) {\n // fetch rejects these with a TypeError that would otherwise be mistaken for a network failure and retried.\n throw new TypeError(\n `arthron: ${method.toUpperCase()} requests cannot carry a body — send it as params (query) or switch to POST`,\n );\n }\n const url = resolveUrl(resolved.baseURL, path, params);\n const { body, headers } = serializeBody(\n rawBody,\n mergeHeaders(resolved.headers, callHeaders),\n );\n // Config credentials sit underneath so a per-call value wins, like every other option.\n const requestInit: RequestInit = {\n ...(resolved.credentials !== undefined && {\n credentials: resolved.credentials,\n }),\n ...init,\n method,\n headers,\n };\n if (body !== undefined) requestInit.body = body;\n return toArthronPromise(\n runPipeline(resolved.use, { url, init: requestInit }, send),\n );\n } catch (thrown) {\n // Argument errors (no baseURL, unserializable body) reject like everything else.\n return toArthronPromise(Promise.reject(thrown));\n }\n };\n\n const derive = (next: Partial<ArthronConfig>): Arthron => {\n const merged: ArthronConfig = {\n ...resolved,\n ...next,\n headers: mergeHeaders(resolved.headers, next.headers),\n use: [...resolved.use, ...(next.use ?? [])],\n };\n return arthron(merged);\n };\n\n return {\n get: (path, init = {}) => dispatch(path, \"GET\", undefined, init),\n delete: (path, init = {}) => dispatch(path, \"DELETE\", undefined, init),\n head: (path, init = {}) => dispatch(path, \"HEAD\", undefined, init),\n post: (path, body, init = {}) => dispatch(path, \"POST\", body, init),\n put: (path, body, init = {}) => dispatch(path, \"PUT\", body, init),\n patch: (path, body, init = {}) => dispatch(path, \"PATCH\", body, init),\n request: (path, init: RequestInitWithBody) => {\n const { method, body, ...rest } = init;\n if (!method) {\n return toArthronPromise(\n Promise.reject(\n new TypeError(\n 'arthron: request() needs init.method — e.g. request(path, { method: \"OPTIONS\" }); use get/post/... for the common verbs',\n ),\n ),\n );\n }\n return dispatch(path, method, body, rest);\n },\n use: (...plugins) => derive({ use: plugins }),\n extend: (next) => derive(next),\n };\n}\n","import type {\n ErrorInterceptor,\n Middleware,\n Plugin,\n RequestInterceptor,\n ResponseInterceptor,\n} from \"./types\";\n\n/*\n * Identity helpers. They do nothing at runtime; they exist so a plugin written\n * in its own file gets contextual typing without an explicit annotation.\n */\n\n/** Identity helper: gives a middleware written in its own file the right parameter types. */\nexport const defineMiddleware = (middleware: Middleware): Middleware =>\n middleware;\n\n/** Identity helpers for each interceptor kind — `defineInterceptor.request(fn)` etc. */\nexport const defineInterceptor = {\n request: (interceptor: RequestInterceptor): RequestInterceptor => interceptor,\n response: (interceptor: ResponseInterceptor): ResponseInterceptor =>\n interceptor,\n error: (interceptor: ErrorInterceptor): ErrorInterceptor => interceptor,\n};\n\n/** Identity helper: type-checks a plugin object literal without an annotation. */\nexport const definePlugin = (plugin: Plugin): Plugin => plugin;\n","import { isArthronError, isCancellation } from \"../error\";\nimport type { Plugin } from \"../types\";\n\ntype LogEventBase = { method: string; path: string };\nexport type LogEvent =\n | ({ phase: \"start\" } & LogEventBase)\n | ({ phase: \"success\"; status: number; ms: number } & LogEventBase)\n | ({\n phase: \"error\";\n /** HTTP status for an ArthronError (0 = network); undefined for anything else. */\n status: number | undefined;\n ms: number;\n error: unknown;\n } & LogEventBase);\n\nexport type LoggingOptions = {\n /** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */\n enabled?: boolean;\n logger?: Pick<Console, \"log\" | \"error\">;\n /** Receive structured events instead of the default console lines. */\n onEvent?: (event: LogEvent) => void;\n /** Clock in ms. Injectable for tests. */\n now?: () => number;\n};\n\n/** Origin + pathname only — the query and userinfo tend to carry secrets. */\nfunction displayPath(url: string): string {\n try {\n const parsed = new URL(url);\n return `${parsed.origin}${parsed.pathname}`;\n } catch {\n return url.split(\"?\")[0] ?? url;\n }\n}\n\nfunction consoleSink(logger: Pick<Console, \"log\" | \"error\">) {\n return (e: LogEvent) => {\n if (e.phase === \"start\") {\n logger.log(`[arthron] → ${e.method} ${e.path}`);\n } else if (e.phase === \"success\") {\n logger.log(`[arthron] ← ${e.method} ${e.path} ${e.status} ${e.ms}ms`);\n } else {\n logger.error(\n `[arthron] ✕ ${e.method} ${e.path} ${e.status ?? \"-\"} ${e.ms}ms`,\n );\n }\n };\n}\n\n/**\n * One event when a request starts, one when it settles, with status and\n * elapsed time. Wraps the whole call, so retries are included in the time.\n * Default output is a console line per event; pass `onEvent` to get the\n * structured event instead. Cancellations are not reported.\n */\nexport function logging(options: LoggingOptions = {}): Plugin {\n const {\n enabled = true,\n logger = console,\n onEvent,\n now = () => performance.now(),\n } = options;\n const emit = onEvent ?? consoleSink(logger);\n return {\n name: \"logging\",\n middleware: async (ctx, next) => {\n if (!enabled) return next(ctx);\n const method = ctx.init.method ?? \"GET\";\n const path = displayPath(ctx.url);\n const started = now();\n const ms = () => Math.round(now() - started);\n\n emit({ phase: \"start\", method, path });\n try {\n const res = await next(ctx);\n emit({ phase: \"success\", method, path, status: res.status, ms: ms() });\n return res;\n } catch (error) {\n if (!isCancellation(error)) {\n const status = isArthronError(error) ? error.status : undefined;\n emit({ phase: \"error\", method, path, status, ms: ms(), error });\n }\n throw error;\n }\n },\n };\n}\n","import { type ArthronError, isArthronError } from \"../error\";\nimport type { Plugin, RequestContext } from \"../types\";\n\nexport type RetryOptions = {\n /** Number of retries after the first attempt. Default 2 (3 requests total). */\n retries?: number;\n /** Statuses worth retrying. Network errors (status 0) always are. */\n retryOn?: readonly number[];\n /** Methods safe to repeat. Compared case-insensitively. */\n methods?: readonly string[];\n /** Backoff in ms for retry number `retry` (0-based). Default 300 · 2^retry. */\n delay?: (retry: number, error: ArthronError) => number;\n /** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */\n sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;\n /**\n * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not\n * retried at all. Default 60 000. Also guards `setTimeout`'s 2^31-1 ms cap,\n * past which timers fire immediately.\n */\n maxRetryAfter?: number;\n};\n\nconst DEFAULT_RETRY_ON = [408, 429, 500, 502, 503, 504] as const;\nconst DEFAULT_METHODS = [\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"] as const;\nconst DEFAULT_MAX_RETRY_AFTER = 60_000;\n\nfunction defaultSleep(ms: number, signal?: AbortSignal | null): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal?.reason);\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** `Retry-After` as ms — seconds or an HTTP-date. `undefined` when absent or unparsable. */\nfunction retryAfterMs(error: ArthronError): number | undefined {\n const value = error.headers.get(\"retry-after\")?.trim();\n if (!value) return undefined;\n const seconds = Number(value);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const at = Date.parse(value);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, at - Date.now());\n}\n\nfunction isStreamBody(ctx: RequestContext): boolean {\n return (\n typeof ReadableStream !== \"undefined\" &&\n ctx.init.body instanceof ReadableStream\n );\n}\n\n/**\n * Retry on network errors and the configured statuses by calling `next`\n * again — the whole attempt (request interceptors included) is re-run.\n * Only `ArthronError` is considered; cancellation and plugin bugs pass through.\n */\nexport function retry(options: RetryOptions = {}): Plugin {\n const {\n retries = 2,\n retryOn = DEFAULT_RETRY_ON,\n methods = DEFAULT_METHODS,\n delay = (retry) => 300 * 2 ** retry,\n sleep = defaultSleep,\n maxRetryAfter = DEFAULT_MAX_RETRY_AFTER,\n } = options;\n const allowed = new Set(methods.map((m) => m.toUpperCase()));\n\n return {\n name: \"retry\",\n middleware: async (ctx, next) => {\n const method = (ctx.init.method ?? \"GET\").toUpperCase();\n const eligible = allowed.has(method) && !isStreamBody(ctx);\n\n for (let attempt = 0; ; attempt += 1) {\n try {\n return await next(ctx);\n } catch (thrown) {\n if (!eligible || attempt >= retries) throw thrown;\n if (!isArthronError(thrown)) throw thrown;\n if (thrown.status !== 0 && !retryOn.includes(thrown.status)) {\n throw thrown;\n }\n if (ctx.init.signal?.aborted) throw thrown;\n const retryAfter = retryAfterMs(thrown);\n if (retryAfter !== undefined && retryAfter > maxRetryAfter) {\n throw thrown;\n }\n await sleep(retryAfter ?? delay(attempt, thrown), ctx.init.signal);\n }\n }\n },\n };\n}\n","import type { Plugin } from \"../types\";\n\n/**\n * Wrap `body` so the deadline keeps running until it is fully read, and so an\n * abort while reading surfaces as a stream error instead of a silent stall.\n */\nfunction guardBody(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n finish: () => void,\n): ReadableStream<Uint8Array> {\n const reader = body.getReader();\n return new ReadableStream<Uint8Array>({\n start(controller) {\n signal.addEventListener(\n \"abort\",\n () => {\n controller.error(signal.reason);\n reader.cancel(signal.reason).catch(() => {});\n },\n { once: true },\n );\n },\n async pull(controller) {\n const { value, done } = await reader.read();\n if (done) {\n finish();\n controller.close();\n return;\n }\n controller.enqueue(value);\n },\n cancel(reason) {\n finish();\n return reader.cancel(reason);\n },\n });\n}\n\n/**\n * Abort the request when it takes longer than `ms` — counted from the start\n * of the attempt until the body has been fully read, so a server that sends\n * headers and then stalls still trips it. Rejects with a `TimeoutError` (a\n * `DOMException`, like `AbortSignal.timeout()` would give); it is not an\n * `ArthronError`, so error interceptors and `retry` leave it alone.\n *\n * Built on `AbortController` + `setTimeout` rather than `AbortSignal.any` /\n * `AbortSignal.timeout` so it runs on Node 18.\n */\nexport function timeout(ms: number): Plugin {\n return {\n name: \"timeout\",\n middleware: async (ctx, next) => {\n const controller = new AbortController();\n const outer = ctx.init.signal;\n const forward = () => controller.abort(outer?.reason);\n if (outer?.aborted) forward();\n else outer?.addEventListener(\"abort\", forward, { once: true });\n\n const timer = setTimeout(\n () =>\n controller.abort(\n new DOMException(\n `arthron: request timed out after ${ms}ms`,\n \"TimeoutError\",\n ),\n ),\n ms,\n );\n const finish = () => {\n clearTimeout(timer);\n outer?.removeEventListener(\"abort\", forward);\n };\n\n let res: Response;\n try {\n res = await next({\n ...ctx,\n init: { ...ctx.init, signal: controller.signal },\n });\n } catch (thrown) {\n finish();\n throw thrown;\n }\n if (!res.body) {\n finish();\n return res;\n }\n return new Response(guardBody(res.body, controller.signal, finish), {\n status: res.status,\n statusText: res.statusText,\n headers: res.headers,\n });\n },\n };\n}\n"],"mappings":"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"}