arthron 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1,159 @@
1
+ //#region src/error.d.ts
2
+ type ErrorMessageResolver = (data: unknown, response: Response) => string | undefined;
3
+ type ArthronErrorInit = {
4
+ message: string;
5
+ status: number;
6
+ statusText: string;
7
+ headers: Headers;
8
+ data: unknown;
9
+ request: RequestContext;
10
+ response?: Response;
11
+ cause?: unknown;
12
+ };
13
+ /**
14
+ * The only error arthron itself produces. Two sources: a response with
15
+ * `ok === false`, or fetch rejecting outright (network failure, `status` 0).
16
+ * Everything else — cancellation, interceptor bugs — is rethrown untouched.
17
+ */
18
+ export declare class ArthronError extends Error {
19
+ readonly name = "ArthronError";
20
+ readonly status: number;
21
+ readonly statusText: string;
22
+ readonly headers: Headers;
23
+ readonly data: unknown;
24
+ readonly request: RequestContext;
25
+ readonly response: Response | undefined;
26
+ readonly cause: unknown;
27
+ constructor(init: ArthronErrorInit);
28
+ }
29
+ export declare function isArthronError(error: unknown): error is ArthronError;
30
+ //#endregion
31
+ //#region src/url.d.ts
32
+ type QueryParams = Record<string, string | number | boolean | null | undefined>;
33
+ //#endregion
34
+ //#region src/types.d.ts
35
+ type RequestContext = {
36
+ url: string;
37
+ init: RequestInit;
38
+ };
39
+ /** Runs before each attempt. Returns the context to send (may be a new object). */
40
+ type RequestInterceptor = (ctx: RequestContext) => RequestContext | Promise<RequestContext>;
41
+ /** Runs after a successful attempt. The body is single-use — `clone()` to peek. */
42
+ type ResponseInterceptor = (res: Response, ctx: RequestContext) => Response | Promise<Response>;
43
+ /** Runs after a failed attempt. Returns the error to throw; it is always thrown. */
44
+ type ErrorInterceptor = (err: ArthronError, ctx: RequestContext) => ArthronError | Promise<ArthronError>;
45
+ /**
46
+ * Wraps the whole call. `next` runs everything below it (request interceptors →
47
+ * fetch → response/error interceptors) and may be called again to retry.
48
+ * `next` rejects with an `ArthronError`, or with the untouched original when the
49
+ * failure was a cancellation or a bug in a plugin.
50
+ */
51
+ type Middleware = (ctx: RequestContext, next: (ctx: RequestContext) => Promise<Response>) => Promise<Response>;
52
+ type Plugin = {
53
+ /** For logs and debugging only. */
54
+ name?: string;
55
+ middleware?: Middleware;
56
+ request?: RequestInterceptor;
57
+ response?: ResponseInterceptor;
58
+ error?: ErrorInterceptor;
59
+ };
60
+ type RequestOptions = Omit<RequestInit, "body" | "method"> & {
61
+ params?: QueryParams;
62
+ };
63
+ type RequestInitWithBody = RequestOptions & {
64
+ method: string;
65
+ body?: unknown;
66
+ };
67
+ type ArthronConfig = {
68
+ /** Prefix for relative paths. Without it only absolute URLs are accepted. */
69
+ baseURL?: string;
70
+ /** Default headers. Per-call headers win, compared case-insensitively. */
71
+ headers?: HeadersInit;
72
+ /** Forwarded to fetch as-is. Not set unless you set it. */
73
+ credentials?: RequestCredentials;
74
+ /** Injection point for tests and polyfills. Defaults to `globalThis.fetch`. */
75
+ fetch?: typeof fetch;
76
+ use?: Plugin[];
77
+ /** How to pick a message out of an error body. */
78
+ errorMessage?: ErrorMessageResolver;
79
+ };
80
+ type ArthronPromise = Promise<Response> & {
81
+ json<T = unknown>(): Promise<T>;
82
+ text(): Promise<string>;
83
+ blob(): Promise<Blob>;
84
+ arrayBuffer(): Promise<ArrayBuffer>;
85
+ formData(): Promise<FormData>;
86
+ };
87
+ type Arthron = {
88
+ get(path: string, init?: RequestOptions): ArthronPromise;
89
+ delete(path: string, init?: RequestOptions): ArthronPromise;
90
+ head(path: string, init?: RequestOptions): ArthronPromise;
91
+ post(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
92
+ put(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
93
+ patch(path: string, body?: unknown, init?: RequestOptions): ArthronPromise;
94
+ request(path: string, init: RequestInitWithBody): ArthronPromise;
95
+ /** New client with these plugins appended. The receiver is untouched. */
96
+ use(...plugins: Plugin[]): Arthron;
97
+ /** New client with config shallow-merged, headers merged, plugins appended. */
98
+ extend(config: Partial<ArthronConfig>): Arthron;
99
+ };
100
+ //#endregion
101
+ //#region src/client.d.ts
102
+ /**
103
+ * Build a client from config. The result is immutable — `use` and `extend`
104
+ * return new clients and never touch the one they were called on.
105
+ */
106
+ export declare function arthron(config?: ArthronConfig): Arthron;
107
+ //#endregion
108
+ //#region src/define.d.ts
109
+ export declare const defineMiddleware: (middleware: Middleware) => Middleware;
110
+ export declare const defineInterceptor: {
111
+ request: (interceptor: RequestInterceptor) => RequestInterceptor;
112
+ response: (interceptor: ResponseInterceptor) => ResponseInterceptor;
113
+ error: (interceptor: ErrorInterceptor) => ErrorInterceptor;
114
+ };
115
+ export declare const definePlugin: (plugin: Plugin) => Plugin;
116
+ //#endregion
117
+ //#region src/plugins/logging.d.ts
118
+ type LoggingOptions = {
119
+ /** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */
120
+ enabled?: boolean;
121
+ logger?: Pick<Console, "log" | "error">;
122
+ /** Clock in ms. Injectable for tests. */
123
+ now?: () => number;
124
+ };
125
+ /**
126
+ * One line when a request starts, one when it settles, with status and
127
+ * elapsed time. Wraps the whole call, so retries are included in the time.
128
+ * The query string is dropped — it tends to carry tokens.
129
+ */
130
+ export declare function logging(options?: LoggingOptions): Plugin;
131
+ //#endregion
132
+ //#region src/plugins/retry.d.ts
133
+ type RetryOptions = {
134
+ /** Number of retries after the first attempt. Default 2 (3 requests total). */
135
+ retries?: number;
136
+ /** Statuses worth retrying. Network errors (status 0) always are. */
137
+ retryOn?: readonly number[];
138
+ /** Methods safe to repeat. Compared case-insensitively. */
139
+ methods?: readonly string[];
140
+ /** Backoff in ms for retry number `retry` (0-based). Default 300 · 2^retry. */
141
+ delay?: (retry: number, error: ArthronError) => number;
142
+ /** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */
143
+ sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;
144
+ /**
145
+ * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not
146
+ * retried at all. Default 60 000. Also guards `setTimeout`'s 2^31-1 ms cap,
147
+ * past which timers fire immediately.
148
+ */
149
+ maxRetryAfter?: number;
150
+ };
151
+ /**
152
+ * Retry on network errors and the configured statuses by calling `next`
153
+ * again — the whole attempt (request interceptors included) is re-run.
154
+ * Only `ArthronError` is considered; cancellation and plugin bugs pass through.
155
+ */
156
+ export declare function retry(options?: RetryOptions): Plugin;
157
+ //#endregion
158
+ export type { Arthron, ArthronConfig, ArthronPromise, ErrorInterceptor, ErrorMessageResolver, LoggingOptions, Middleware, Plugin, QueryParams, RequestContext, RequestInterceptor, RequestOptions, ResponseInterceptor, RetryOptions };
159
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,432 @@
1
+ //#region src/body.ts
2
+ const RAW_CTORS = [
3
+ "FormData",
4
+ "Blob",
5
+ "ArrayBuffer",
6
+ "URLSearchParams",
7
+ "ReadableStream"
8
+ ];
9
+ /** Bodies fetch already knows how to send. Content-Type is left to the caller / runtime. */
10
+ function isRawBody(body) {
11
+ if (typeof body === "string") return true;
12
+ if (ArrayBuffer.isView(body)) return true;
13
+ return RAW_CTORS.some((name) => {
14
+ const ctor = globalThis[name];
15
+ return typeof ctor === "function" && body instanceof ctor;
16
+ });
17
+ }
18
+ /**
19
+ * Turn a caller-supplied body into something fetch accepts. Raw bodies pass
20
+ * through untouched; anything else is JSON with `content-type` set — unless
21
+ * the caller already set one (headers arrive lowercased, so a plain lookup is
22
+ * enough).
23
+ */
24
+ function serializeBody(body, headers) {
25
+ if (body === void 0) return {
26
+ body: void 0,
27
+ headers
28
+ };
29
+ if (isRawBody(body)) return {
30
+ body,
31
+ headers
32
+ };
33
+ const withType = "content-type" in headers ? headers : {
34
+ ...headers,
35
+ "content-type": "application/json"
36
+ };
37
+ return {
38
+ body: JSON.stringify(body),
39
+ headers: withType
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/error.ts
44
+ /**
45
+ * The only error arthron itself produces. Two sources: a response with
46
+ * `ok === false`, or fetch rejecting outright (network failure, `status` 0).
47
+ * Everything else — cancellation, interceptor bugs — is rethrown untouched.
48
+ */
49
+ var ArthronError = class extends Error {
50
+ name = "ArthronError";
51
+ status;
52
+ statusText;
53
+ headers;
54
+ data;
55
+ request;
56
+ response;
57
+ cause;
58
+ constructor(init) {
59
+ super(init.message);
60
+ this.status = init.status;
61
+ this.statusText = init.statusText;
62
+ this.headers = init.headers;
63
+ this.data = init.data;
64
+ this.request = init.request;
65
+ this.response = init.response;
66
+ this.cause = init.cause;
67
+ }
68
+ };
69
+ function isArthronError(error) {
70
+ return error instanceof ArthronError;
71
+ }
72
+ /** Cancellation is not an HTTP failure. Matched by name so it works across realms. */
73
+ function isCancellation(error) {
74
+ const name = error?.name;
75
+ return name === "AbortError" || name === "TimeoutError";
76
+ }
77
+ const isRecord = (value) => typeof value === "object" && value !== null;
78
+ /** `message` → `error` (string) → `error.message` → `detail`. */
79
+ function defaultErrorMessage(data) {
80
+ if (!isRecord(data)) return void 0;
81
+ if (typeof data.message === "string") return data.message;
82
+ if (typeof data.error === "string") return data.error;
83
+ if (isRecord(data.error) && typeof data.error.message === "string") return data.error.message;
84
+ if (typeof data.detail === "string") return data.detail;
85
+ }
86
+ /** Body as JSON if it parses, else as text, else `undefined` when empty. */
87
+ async function readBody(response) {
88
+ const text = await response.text().catch(() => "");
89
+ if (text === "") return void 0;
90
+ try {
91
+ return JSON.parse(text);
92
+ } catch {
93
+ return text;
94
+ }
95
+ }
96
+ async function responseError(response, request, resolve = defaultErrorMessage) {
97
+ const data = await readBody(response);
98
+ return new ArthronError({
99
+ message: resolve(data, response) ?? `Request failed with status ${response.status}`,
100
+ status: response.status,
101
+ statusText: response.statusText,
102
+ headers: response.headers,
103
+ data,
104
+ request,
105
+ response
106
+ });
107
+ }
108
+ function networkError(cause, request) {
109
+ return new ArthronError({
110
+ message: cause instanceof Error ? cause.message : "Network error",
111
+ status: 0,
112
+ statusText: "",
113
+ headers: new Headers(),
114
+ data: void 0,
115
+ request,
116
+ cause
117
+ });
118
+ }
119
+ //#endregion
120
+ //#region src/headers.ts
121
+ /**
122
+ * Normalize any `HeadersInit` (plain object, `Headers`, tuple array) into a
123
+ * plain record. Keys come back lowercased — `Headers` does that for us.
124
+ */
125
+ function toHeaderRecord(headers) {
126
+ if (!headers) return {};
127
+ return Object.fromEntries(new Headers(headers).entries());
128
+ }
129
+ /**
130
+ * Layer `override` on top of `base`. Names are compared case-insensitively and
131
+ * the override wins, so a caller's `content-type` replaces a default
132
+ * `Content-Type` instead of fetch sending both values joined.
133
+ */
134
+ function mergeHeaders(base, override) {
135
+ return {
136
+ ...toHeaderRecord(base),
137
+ ...toHeaderRecord(override)
138
+ };
139
+ }
140
+ //#endregion
141
+ //#region src/pipeline.ts
142
+ /**
143
+ * Interceptors mutate the context in place. A middleware that calls `next`
144
+ * again (retry) must not see the previous attempt's changes, so every attempt
145
+ * starts from a fresh copy.
146
+ */
147
+ function cloneContext({ url, init }) {
148
+ return {
149
+ url,
150
+ init: {
151
+ ...init,
152
+ headers: toHeaderRecord(init.headers)
153
+ }
154
+ };
155
+ }
156
+ /**
157
+ * Owns the order of one request.
158
+ *
159
+ * Middleware wraps the whole call, outermost first in registration order.
160
+ * Each attempt below it runs request interceptors → `send` → response
161
+ * interceptors. Only an `ArthronError` reaches the error interceptors; whatever
162
+ * they return is thrown. Anything else that is thrown — cancellation, a bug
163
+ * in a plugin — propagates untouched.
164
+ */
165
+ async function runPipeline(plugins, ctx, send) {
166
+ const middlewares = [];
167
+ const request = [];
168
+ const response = [];
169
+ const error = [];
170
+ for (const plugin of plugins) {
171
+ if (plugin.middleware) middlewares.push(plugin.middleware);
172
+ if (plugin.request) request.push(plugin.request);
173
+ if (plugin.response) response.push(plugin.response);
174
+ if (plugin.error) error.push(plugin.error);
175
+ }
176
+ const attempt = async (input) => {
177
+ let current = cloneContext(input);
178
+ try {
179
+ for (const interceptor of request) current = await interceptor(current);
180
+ let res = await send(current);
181
+ for (const interceptor of response) res = await interceptor(res, current);
182
+ return res;
183
+ } catch (thrown) {
184
+ if (!isArthronError(thrown)) throw thrown;
185
+ let err = thrown;
186
+ for (const interceptor of error) err = await interceptor(err, current);
187
+ throw err;
188
+ }
189
+ };
190
+ const dispatch = (index, input) => {
191
+ const middleware = middlewares[index];
192
+ if (!middleware) return attempt(input);
193
+ return middleware(input, (next) => dispatch(index + 1, next));
194
+ };
195
+ return dispatch(0, ctx);
196
+ }
197
+ //#endregion
198
+ //#region src/promise.ts
199
+ /**
200
+ * Sugar over `Promise<Response>`: `.json()` is `(await p).json()`. Awaiting
201
+ * without a helper still yields the untouched `Response`.
202
+ */
203
+ function toArthronPromise(promise) {
204
+ return Object.assign(promise, {
205
+ json: () => promise.then((res) => res.json()),
206
+ text: () => promise.then((res) => res.text()),
207
+ blob: () => promise.then((res) => res.blob()),
208
+ arrayBuffer: () => promise.then((res) => res.arrayBuffer()),
209
+ formData: () => promise.then((res) => res.formData())
210
+ });
211
+ }
212
+ //#endregion
213
+ //#region src/url.ts
214
+ const ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
215
+ function isAbsoluteUrl(path) {
216
+ return ABSOLUTE_URL.test(path);
217
+ }
218
+ function toQueryString(params) {
219
+ if (!params) return "";
220
+ const search = new URLSearchParams();
221
+ for (const [key, value] of Object.entries(params)) {
222
+ if (value === null || value === void 0) continue;
223
+ search.append(key, String(value));
224
+ }
225
+ return search.toString();
226
+ }
227
+ /**
228
+ * Join `baseURL` and `path` by plain string concatenation so a path prefix in
229
+ * the base (`/v1`) survives a leading slash in the path. WHATWG `new URL()`
230
+ * would drop it. Absolute paths skip the base entirely.
231
+ */
232
+ function resolveUrl(baseURL, path, params) {
233
+ let url;
234
+ if (isAbsoluteUrl(path)) url = path;
235
+ else {
236
+ if (baseURL === void 0) throw new TypeError(`arthron: relative path "${path}" requires a baseURL`);
237
+ url = `${baseURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
238
+ }
239
+ const query = toQueryString(params);
240
+ if (!query) return url;
241
+ return `${url}${url.includes("?") ? "&" : "?"}${query}`;
242
+ }
243
+ //#endregion
244
+ //#region src/client.ts
245
+ /** Normalize once so `extend` can merge headers and append plugins cheaply. */
246
+ function resolveConfig(config) {
247
+ const { headers, use, ...rest } = config;
248
+ return {
249
+ ...rest,
250
+ headers: toHeaderRecord(headers),
251
+ use: use ?? []
252
+ };
253
+ }
254
+ /**
255
+ * Build a client from config. The result is immutable — `use` and `extend`
256
+ * return new clients and never touch the one they were called on.
257
+ */
258
+ function arthron(config = {}) {
259
+ const resolved = resolveConfig(config);
260
+ const fetchImpl = resolved.fetch ?? globalThis.fetch;
261
+ /** The one place a failure becomes an ArthronError. Cancellation passes through. */
262
+ const send = async (ctx) => {
263
+ let res;
264
+ try {
265
+ res = await fetchImpl(ctx.url, ctx.init);
266
+ } catch (thrown) {
267
+ if (isCancellation(thrown)) throw thrown;
268
+ throw networkError(thrown, ctx);
269
+ }
270
+ if (!res.ok) throw await responseError(res, ctx, resolved.errorMessage);
271
+ return res;
272
+ };
273
+ const dispatch = (path, method, rawBody, { params, headers: callHeaders, ...init }) => {
274
+ try {
275
+ const url = resolveUrl(resolved.baseURL, path, params);
276
+ const { body, headers } = serializeBody(rawBody, mergeHeaders(resolved.headers, callHeaders));
277
+ const requestInit = {
278
+ ...resolved.credentials !== void 0 && { credentials: resolved.credentials },
279
+ ...init,
280
+ method,
281
+ headers
282
+ };
283
+ if (body !== void 0) requestInit.body = body;
284
+ return toArthronPromise(runPipeline(resolved.use, {
285
+ url,
286
+ init: requestInit
287
+ }, send));
288
+ } catch (thrown) {
289
+ return toArthronPromise(Promise.reject(thrown));
290
+ }
291
+ };
292
+ const derive = (next) => {
293
+ return arthron({
294
+ ...resolved,
295
+ ...next,
296
+ headers: mergeHeaders(resolved.headers, next.headers),
297
+ use: [...resolved.use, ...next.use ?? []]
298
+ });
299
+ };
300
+ return {
301
+ get: (path, init = {}) => dispatch(path, "GET", void 0, init),
302
+ delete: (path, init = {}) => dispatch(path, "DELETE", void 0, init),
303
+ head: (path, init = {}) => dispatch(path, "HEAD", void 0, init),
304
+ post: (path, body, init = {}) => dispatch(path, "POST", body, init),
305
+ put: (path, body, init = {}) => dispatch(path, "PUT", body, init),
306
+ patch: (path, body, init = {}) => dispatch(path, "PATCH", body, init),
307
+ request: (path, init) => {
308
+ const { method, body, ...rest } = init;
309
+ if (!method) return toArthronPromise(Promise.reject(/* @__PURE__ */ new TypeError("arthron: request() requires init.method")));
310
+ return dispatch(path, method, body, rest);
311
+ },
312
+ use: (...plugins) => derive({ use: plugins }),
313
+ extend: (next) => derive(next)
314
+ };
315
+ }
316
+ //#endregion
317
+ //#region src/define.ts
318
+ const defineMiddleware = (middleware) => middleware;
319
+ const defineInterceptor = {
320
+ request: (interceptor) => interceptor,
321
+ response: (interceptor) => interceptor,
322
+ error: (interceptor) => interceptor
323
+ };
324
+ const definePlugin = (plugin) => plugin;
325
+ //#endregion
326
+ //#region src/plugins/logging.ts
327
+ /**
328
+ * One line when a request starts, one when it settles, with status and
329
+ * elapsed time. Wraps the whole call, so retries are included in the time.
330
+ * The query string is dropped — it tends to carry tokens.
331
+ */
332
+ function logging(options = {}) {
333
+ const { enabled = true, logger = console, now = () => performance.now() } = options;
334
+ return {
335
+ name: "logging",
336
+ middleware: async (ctx, next) => {
337
+ if (!enabled) return next(ctx);
338
+ const method = ctx.init.method ?? "GET";
339
+ const path = ctx.url.split("?")[0];
340
+ const started = now();
341
+ const elapsed = () => `${Math.round(now() - started)}ms`;
342
+ logger.log(`[arthron] → ${method} ${path}`);
343
+ try {
344
+ const res = await next(ctx);
345
+ logger.log(`[arthron] ← ${method} ${path} ${res.status} ${elapsed()}`);
346
+ return res;
347
+ } catch (thrown) {
348
+ if (!isCancellation(thrown)) {
349
+ const status = isArthronError(thrown) ? thrown.status : "-";
350
+ logger.error(`[arthron] ✕ ${method} ${path} ${status} ${elapsed()}`);
351
+ }
352
+ throw thrown;
353
+ }
354
+ }
355
+ };
356
+ }
357
+ //#endregion
358
+ //#region src/plugins/retry.ts
359
+ const DEFAULT_RETRY_ON = [
360
+ 408,
361
+ 429,
362
+ 500,
363
+ 502,
364
+ 503,
365
+ 504
366
+ ];
367
+ const DEFAULT_METHODS = [
368
+ "GET",
369
+ "HEAD",
370
+ "OPTIONS",
371
+ "PUT",
372
+ "DELETE"
373
+ ];
374
+ const DEFAULT_MAX_RETRY_AFTER = 6e4;
375
+ function defaultSleep(ms, signal) {
376
+ return new Promise((resolve, reject) => {
377
+ if (signal?.aborted) return reject(signal.reason);
378
+ const onAbort = () => {
379
+ clearTimeout(timer);
380
+ reject(signal?.reason);
381
+ };
382
+ const timer = setTimeout(() => {
383
+ signal?.removeEventListener("abort", onAbort);
384
+ resolve();
385
+ }, ms);
386
+ signal?.addEventListener("abort", onAbort, { once: true });
387
+ });
388
+ }
389
+ /** `Retry-After` as ms — seconds or an HTTP-date. `undefined` when absent or unparsable. */
390
+ function retryAfterMs(error) {
391
+ const value = error.headers.get("retry-after")?.trim();
392
+ if (!value) return void 0;
393
+ const seconds = Number(value);
394
+ if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
395
+ const at = Date.parse(value);
396
+ if (Number.isNaN(at)) return void 0;
397
+ return Math.max(0, at - Date.now());
398
+ }
399
+ function isStreamBody(ctx) {
400
+ return typeof ReadableStream !== "undefined" && ctx.init.body instanceof ReadableStream;
401
+ }
402
+ /**
403
+ * Retry on network errors and the configured statuses by calling `next`
404
+ * again — the whole attempt (request interceptors included) is re-run.
405
+ * Only `ArthronError` is considered; cancellation and plugin bugs pass through.
406
+ */
407
+ function retry(options = {}) {
408
+ const { retries = 2, retryOn = DEFAULT_RETRY_ON, methods = DEFAULT_METHODS, delay = (retry) => 300 * 2 ** retry, sleep = defaultSleep, maxRetryAfter = DEFAULT_MAX_RETRY_AFTER } = options;
409
+ const allowed = new Set(methods.map((m) => m.toUpperCase()));
410
+ return {
411
+ name: "retry",
412
+ middleware: async (ctx, next) => {
413
+ const method = (ctx.init.method ?? "GET").toUpperCase();
414
+ const eligible = allowed.has(method) && !isStreamBody(ctx);
415
+ for (let attempt = 0;; attempt += 1) try {
416
+ return await next(ctx);
417
+ } catch (thrown) {
418
+ if (!eligible || attempt >= retries) throw thrown;
419
+ if (!isArthronError(thrown)) throw thrown;
420
+ if (thrown.status !== 0 && !retryOn.includes(thrown.status)) throw thrown;
421
+ if (ctx.init.signal?.aborted) throw thrown;
422
+ const retryAfter = retryAfterMs(thrown);
423
+ if (retryAfter !== void 0 && retryAfter > maxRetryAfter) throw thrown;
424
+ await sleep(retryAfter ?? delay(attempt, thrown), ctx.init.signal);
425
+ }
426
+ }
427
+ };
428
+ }
429
+ //#endregion
430
+ export { ArthronError, arthron, defineInterceptor, defineMiddleware, definePlugin, isArthronError, logging, retry };
431
+
432
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","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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"}