arthron 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ //#region src/types/request.types.d.ts
2
+ /**
3
+ * What plugins see and return. `init.headers` is always a plain record with
4
+ * lowercased names — spread it, read it, add to it — and the pipeline puts
5
+ * it back into that shape after every request interceptor.
6
+ */
7
+ interface RequestContext {
8
+ url: string;
9
+ init: Omit<RequestInit, "headers"> & {
10
+ headers: Record<string, string>;
11
+ };
12
+ /** Per-call settings for plugins (`RequestOptions.meta`); never sent. */
13
+ meta: ArthronMeta;
14
+ /**
15
+ * The fetch this attempt is sent with — the client's `fetch`, else the
16
+ * global one. Always set on contexts arthron creates; replace it to change
17
+ * the transport for this attempt, wrap it to observe. Optional so a
18
+ * context you build yourself stays valid.
19
+ */
20
+ fetch?: typeof fetch;
21
+ }
22
+ /**
23
+ * Per-call settings for plugins, passed as `api.get(path, { meta })` and
24
+ * carried next to `init` — never sent to fetch. The built-in plugins read
25
+ * `bearerAuth`, `onUploadProgress`, `retry` and `timeout`; your own add keys
26
+ * by augmentation:
27
+ *
28
+ * ```ts
29
+ * declare module "arthron" {
30
+ * interface ArthronMeta { skipTrace?: boolean }
31
+ * }
32
+ * ```
33
+ */
34
+ interface ArthronMeta {
35
+ /**
36
+ * `false` sends this call without the `bearerAuth()` token and never refreshes
37
+ * on its 401 — for the login and token-refresh endpoints.
38
+ */
39
+ bearerAuth?: false;
40
+ /**
41
+ * Upload progress for this call, with `uploadProgress()` on the client.
42
+ * Never called without that plugin or without a body, nor where progress
43
+ * cannot be had: a client with an injected `fetch`, another plugin's
44
+ * transport, Deno, Bun, Cloudflare Workers, a service worker, or a
45
+ * `ReadableStream` body in a browser — those calls are sent as usual.
46
+ */
47
+ onUploadProgress?: (event: UploadProgressEvent) => void;
48
+ /** `false` turns `retry()` off for this call; an object overrides its options. */
49
+ retry?: false | Omit<RetryOptions, "sleep">;
50
+ /** Deadline for this call in ms, overriding `timeout(ms)`. `Infinity` lifts it. */
51
+ timeout?: number;
52
+ }
53
+ /** One step of an upload, as `meta.onUploadProgress` receives it. */
54
+ type UploadProgressEvent = {
55
+ /** Bytes sent so far. */
56
+ loaded: number;
57
+ /** Bytes in the whole body; `undefined` when it cannot be known (a stream without content-length). */
58
+ total: number | undefined;
59
+ /** `loaded / total`, from 0 to 1; `undefined` when `total` is. */
60
+ progress: number | undefined;
61
+ };
62
+ type RetryOptions = {
63
+ /** Number of retries after the first attempt. Default 2 (3 requests total). */
64
+ retries?: number;
65
+ /** Statuses worth retrying. Network errors (status 0) always are. */
66
+ retryOn?: readonly number[];
67
+ /** Methods safe to repeat (QUERY is, per RFC 10008). Compared case-insensitively. */
68
+ methods?: readonly string[];
69
+ /**
70
+ * Backoff in ms for retry number `retry` (0-based). Default "full jitter":
71
+ * a random share of 300 · 2^retry, so clients that failed together do not
72
+ * retry together.
73
+ */
74
+ delay?: (retry: number, error: unknown) => number;
75
+ /**
76
+ * Your own verdict on one failure: `true` retries, `false` stops,
77
+ * `undefined` leaves it to `retryOn`/`methods`. It sees every error — a
78
+ * TimeoutError from a `timeout()` placed after `retry()` included — but
79
+ * cannot exceed `retries`, resend a `ReadableStream` body, or retry a call
80
+ * the caller aborted.
81
+ */
82
+ shouldRetry?: (error: unknown, attempt: number, ctx: RequestContext) => boolean | undefined;
83
+ /** Called before each wait, for logs and metrics. */
84
+ onRetry?: (error: unknown, attempt: number, waitMs: number) => void;
85
+ /** Injectable for tests. Default `setTimeout`; rejects with the abort reason. */
86
+ sleep?: (ms: number, signal?: AbortSignal | null) => Promise<void>;
87
+ /**
88
+ * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not
89
+ * retried at all. Default 60 000.
90
+ */
91
+ maxRetryAfter?: number;
92
+ };
93
+ type QueryValue = string | number | boolean | null | undefined;
94
+ /**
95
+ * Query parameters. `null`/`undefined` values are skipped, an array becomes
96
+ * repeated keys (`ids=1&ids=2`), and a `URLSearchParams` is used as is.
97
+ */
98
+ type QueryParams = Record<string, QueryValue | readonly QueryValue[]> | URLSearchParams;
99
+ /**
100
+ * Headers as config and per-call options accept them: anything fetch's
101
+ * `headers` takes, or a plain object whose `null`/`undefined` values delete
102
+ * that header from the layer underneath (a client default, an inherited
103
+ * header). Not spelled `HeadersInit`: that name exists only in the DOM lib,
104
+ * and a Node project typed with `@types/node` alone does not have it.
105
+ */
106
+ type HeadersOption = NonNullable<RequestInit["headers"]> | Record<string, string | null | undefined>;
107
+ //#endregion
108
+ //#region src/error/arthron-error.d.ts
109
+ type ArthronErrorInit = {
110
+ message: string;
111
+ status: number;
112
+ statusText: string;
113
+ headers: Headers;
114
+ data: unknown;
115
+ detail?: string | undefined;
116
+ request: RequestContext;
117
+ response?: Response;
118
+ cause?: unknown;
119
+ };
120
+ /**
121
+ * The only error arthron itself produces. Two sources: a response with
122
+ * `ok === false`, or fetch rejecting outright (network failure, `status` 0).
123
+ * Everything else — cancellation, interceptor bugs — is rethrown untouched.
124
+ */
125
+ declare class ArthronError extends Error {
126
+ readonly name = "ArthronError";
127
+ /** HTTP status of the failed response; `0` when fetch itself failed (network). */
128
+ readonly status: number;
129
+ readonly statusText: string;
130
+ /** Response headers; empty for network failures. Not enumerable — kept out of logs. */
131
+ readonly headers: Headers;
132
+ /** Parsed body: JSON if it parses, else the text, else `undefined`. */
133
+ readonly data: unknown;
134
+ /**
135
+ * The server's own explanation, picked out of `data` (by default
136
+ * `message` → `error` → `error.message` → `detail`; see the `errorDetail`
137
+ * config). `undefined` when the body had none. Untrusted server text: show
138
+ * it to users as text, never as HTML. `message`, by contrast, is written by
139
+ * arthron — method, URL without query, status — and is meant for logs.
140
+ */
141
+ readonly detail: string | undefined;
142
+ /**
143
+ * The context that was sent — url and init after request interceptors.
144
+ * Not enumerable: it holds credentials, so `JSON.stringify`, `console.error`
145
+ * and error trackers leave it out. Read it directly when you need it.
146
+ */
147
+ readonly request: RequestContext;
148
+ /** The failed Response, still readable — its body holds what arthron read (at most 64 KiB). Absent for network failures. Not enumerable. */
149
+ readonly response: Response | undefined;
150
+ /** The underlying error for network failures. */
151
+ readonly cause: unknown;
152
+ constructor(init: ArthronErrorInit);
153
+ /** `instanceof ArthronError` holds for an error from any copy of the package. */
154
+ static [Symbol.hasInstance](value: unknown): boolean;
155
+ /** What serializers see: no headers, no body, no query or userinfo. */
156
+ toJSON(): {
157
+ name: string;
158
+ message: string;
159
+ detail: string | undefined;
160
+ status: number;
161
+ statusText: string;
162
+ method: string;
163
+ url: string;
164
+ data: unknown;
165
+ };
166
+ }
167
+ /** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
168
+ declare function isArthronError(error: unknown): error is ArthronError;
169
+ //#endregion
170
+ //#region src/types/plugin.types.d.ts
171
+ /** Runs before each attempt. Returns the context to send (may be a new object). */
172
+ type RequestInterceptor = (ctx: RequestContext) => RequestContext | Promise<RequestContext>;
173
+ /** Runs after a successful attempt. The body is single-use — `clone()` to peek. */
174
+ type ResponseInterceptor = (res: Response, ctx: RequestContext) => Response | Promise<Response>;
175
+ /** Runs after a failed attempt. Returns the error to throw; it is always thrown. */
176
+ type ErrorInterceptor = (err: ArthronError, ctx: RequestContext) => ArthronError | Promise<ArthronError>;
177
+ /**
178
+ * Wraps the whole call. `next` runs everything below it (request interceptors →
179
+ * fetch → response/error interceptors) and may be called again to retry.
180
+ * `next` rejects with an `ArthronError`, or with the untouched original when the
181
+ * failure was a cancellation or a bug in a plugin.
182
+ */
183
+ type Middleware = (ctx: RequestContext, next: (ctx: RequestContext) => Promise<Response>) => Promise<Response>;
184
+ interface Plugin {
185
+ /**
186
+ * Identity within a client: `use`/`extend` replace the plugin of the same
187
+ * name in place, `without(name)` removes it. The built-ins are named
188
+ * "bearerAuth", "logging", "retry", "timeout" and "uploadProgress". Leave it out to always
189
+ * append.
190
+ */
191
+ name?: string;
192
+ middleware?: Middleware;
193
+ request?: RequestInterceptor;
194
+ response?: ResponseInterceptor;
195
+ error?: ErrorInterceptor;
196
+ }
197
+ //#endregion
198
+ //#region src/plugins/bearer-auth/bearer-auth.types.d.ts
199
+ type BearerAuthOptions = {
200
+ /**
201
+ * Where the token may go: a URL, or several. Only their origins count, so
202
+ * your `baseURL` works as is. A request anywhere else — a CDN, a URL taken
203
+ * from a response body — is sent without the token.
204
+ */
205
+ origin: string | readonly string[];
206
+ /**
207
+ * The current access token, read on every attempt. `null`, `undefined` or
208
+ * `""` sends no `authorization` header.
209
+ */
210
+ token: () => string | null | undefined | Promise<string | null | undefined>;
211
+ /**
212
+ * Get a new token after a 401; the call is then redone once. Concurrent
213
+ * 401s share one refresh, and a call whose token was already replaced
214
+ * retries without one. Throw when the refresh fails — every waiting call
215
+ * rejects with that error. Requests the refresh makes through the same
216
+ * client need `meta: { bearerAuth: false }`. Without `refresh`, or for a
217
+ * `ReadableStream` body (already read, so it cannot be resent), a 401 is
218
+ * thrown as is.
219
+ */
220
+ refresh?: () => Promise<void>;
221
+ };
222
+ //#endregion
223
+ //#region src/plugins/bearer-auth/bearer-auth.d.ts
224
+ /**
225
+ * Bearer-token auth. A request interceptor sets `authorization: Bearer
226
+ * <token>` on every attempt to `origin`; with `refresh`, a middleware turns a
227
+ * 401 into one refresh and one more try. Concurrent 401s share the refresh
228
+ * (with refresh-token rotation, parallel refreshes revoke each other), and a
229
+ * call whose token was replaced meanwhile just retries. A call opts out with
230
+ * `meta: { bearerAuth: false }`.
231
+ */
232
+ declare function bearerAuth(options: BearerAuthOptions): Plugin;
233
+ //#endregion
234
+ //#region src/plugins/logging/logging.types.d.ts
235
+ type LogEventBase = {
236
+ method: string;
237
+ path: string;
238
+ };
239
+ type LogEvent = ({
240
+ phase: "start";
241
+ } & LogEventBase) | ({
242
+ phase: "success";
243
+ status: number;
244
+ ms: number;
245
+ } & LogEventBase) | ({
246
+ phase: "error";
247
+ /** HTTP status for an ArthronError (0 = network); undefined for anything else. */
248
+ status: number | undefined;
249
+ ms: number;
250
+ error: unknown;
251
+ } & LogEventBase);
252
+ type LoggingOptions = {
253
+ /** Default `true`. The library never reads `NODE_ENV` — pass `isDev` yourself. */
254
+ enabled?: boolean;
255
+ logger?: Pick<Console, "log" | "error">;
256
+ /** Receive structured events instead of the default console lines. */
257
+ onEvent?: (event: LogEvent) => void;
258
+ /** Clock in ms. Injectable for tests. */
259
+ now?: () => number;
260
+ };
261
+ //#endregion
262
+ //#region src/plugins/logging/logging.d.ts
263
+ /**
264
+ * One event when a request starts, one when it settles, with status and
265
+ * elapsed time. Wraps the whole call, so retries are included in the time.
266
+ * Default output is a console line per event; pass `onEvent` to get the
267
+ * structured event instead. Cancellations are not reported.
268
+ */
269
+ declare function logging(options?: LoggingOptions): Plugin;
270
+ //#endregion
271
+ //#region src/plugins/retry/retry.d.ts
272
+ /**
273
+ * Retry on network errors and the configured statuses by calling `next`
274
+ * again — the whole attempt (request interceptors included) is re-run.
275
+ * Only `ArthronError` is considered; cancellation and plugin bugs pass through.
276
+ * A call can turn it off or override its options with `meta.retry`.
277
+ */
278
+ declare function retry(options?: RetryOptions): Plugin;
279
+ //#endregion
280
+ //#region src/plugins/timeout/timeout.types.d.ts
281
+ type TimeoutOptions = {
282
+ /**
283
+ * Abort after this many ms in all, counted until the body has been fully
284
+ * read — the same deadline as `timeout(ms)`. Default: none.
285
+ */
286
+ total?: number;
287
+ /**
288
+ * Abort when no data has arrived for this many ms — while waiting for the
289
+ * headers and between body chunks. Unlike `total`, a stream that keeps
290
+ * flowing is never cut off: `timeout({ idle: 30_000 })`.
291
+ */
292
+ idle?: number;
293
+ };
294
+ //#endregion
295
+ //#region src/plugins/timeout/timeout.d.ts
296
+ /**
297
+ * Abort the request when it takes longer than `ms` — counted from the start
298
+ * of the attempt until the body has been fully read, so a server that sends
299
+ * headers and then stalls still trips it. `timeout({ idle })` instead aborts
300
+ * once no data has arrived for that long, and never cuts off a stream that
301
+ * keeps flowing; `timeout({ total, idle })` does both. Rejects with a
302
+ * `TimeoutError` (a `DOMException`, like `AbortSignal.timeout()` would give);
303
+ * it is not an `ArthronError`, so error interceptors and `retry` leave it
304
+ * alone.
305
+ *
306
+ * A call can change the total deadline with `meta.timeout`.
307
+ *
308
+ * Deadlines are timers on an AbortController — not `AbortSignal.timeout`,
309
+ * since the idle one restarts — joined to the caller's signal with
310
+ * `AbortSignal.any`.
311
+ */
312
+ declare function timeout(ms: number): Plugin;
313
+ declare function timeout(options: TimeoutOptions): Plugin;
314
+ /** @deprecated Use `timeout({ total: ms, idle })`; this form goes in the next major. */
315
+ declare function timeout(ms: number, options: Pick<TimeoutOptions, "idle">): Plugin;
316
+ //#endregion
317
+ //#region src/plugins/upload-progress/upload-progress.d.ts
318
+ /**
319
+ * Does nothing by itself — only a call that passes `meta.onUploadProgress`
320
+ * reports upload progress. On such a call it changes how that one attempt
321
+ * is sent: over XMLHttpRequest in a browser, with a counted body in Node.
322
+ * Every other call, on this client too, goes out exactly as before.
323
+ *
324
+ * Progress needs the runtime's own fetch: with an injected `fetch`, another
325
+ * plugin's transport, or a runtime that is neither (Deno, Bun, Cloudflare
326
+ * Workers, a service worker), the call is sent as usual without progress.
327
+ * Put it last in `use`: the call's signal carries a throwing callback's
328
+ * error, and an interceptor after it that replaces the signal would cut it.
329
+ */
330
+ declare function uploadProgress(): Plugin;
331
+ //#endregion
332
+ export { UploadProgressEvent as S, ArthronMeta as _, logging as a, RequestContext as b, bearerAuth as c, Middleware as d, Plugin as f, isArthronError as g, ArthronError as h, retry as i, BearerAuthOptions as l, ResponseInterceptor as m, timeout as n, LogEvent as o, RequestInterceptor as p, TimeoutOptions as r, LoggingOptions as s, uploadProgress as t, ErrorInterceptor as u, HeadersOption as v, RetryOptions as x, QueryParams as y };
333
+ //# sourceMappingURL=index-C3lylSNd.d.mts.map