arthron 1.2.0 → 2.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.
@@ -0,0 +1,239 @@
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 function this attempt is sent with — the client's `fetchFn` at first,
16
+ * else the global `fetch`. A plugin may replace it for this attempt
17
+ * (`uploadProgress()` sends over XMLHttpRequest) or wrap it to observe.
18
+ * Always set on contexts arthron creates; optional so a context you build
19
+ * yourself stays valid.
20
+ */
21
+ fetchFn?: typeof fetch;
22
+ }
23
+ /**
24
+ * Per-call settings for plugins, passed as `api.get(path, { meta })` and
25
+ * carried next to `init` — never sent to fetch. Each built-in plugin with a
26
+ * per-call setting reads the key named after it — `bearerAuth`, `retry`,
27
+ * `timeout`, `uploadProgress`; your own add keys by augmentation:
28
+ *
29
+ * ```ts
30
+ * declare module "arthron" {
31
+ * interface ArthronMeta { skipTrace?: boolean }
32
+ * }
33
+ * ```
34
+ */
35
+ interface ArthronMeta {
36
+ /**
37
+ * `false` sends this call without the `bearerAuth()` token and never refreshes
38
+ * on its 401 — for the login and token-refresh endpoints.
39
+ */
40
+ bearerAuth?: false;
41
+ /**
42
+ * Upload progress for this call, with `uploadProgress()` on the client:
43
+ * `{ onProgress: ({ loaded, total, fraction }) => … }`. `onProgress` is
44
+ * never called without that plugin or without a body, nor where progress
45
+ * cannot be had: a client with its own `fetchFn`, another plugin's
46
+ * `fetchFn`, Deno, Bun, Cloudflare Workers, a service worker, or a
47
+ * `ReadableStream` body in a browser — those calls are sent as usual.
48
+ */
49
+ uploadProgress?: {
50
+ /** Called for each step of the upload, from 0 again on each attempt. */
51
+ onProgress: (event: UploadProgressEvent) => void;
52
+ };
53
+ /** `false` turns `retry()` off for this call; an object overrides its options. */
54
+ retry?: false | RetryOptions;
55
+ /**
56
+ * `false` lifts `timeout()`'s deadlines for this call; a number replaces
57
+ * its `total` (ms); an object overrides the parts it sets, `total` or
58
+ * `idle`: `{ idle: 30_000 }` keeps the configured total.
59
+ */
60
+ timeout?: false | number | TimeoutOptions;
61
+ }
62
+ type TimeoutOptions = {
63
+ /**
64
+ * Abort after this many ms in all, counted until the body has been fully
65
+ * read — the same deadline as `timeout(ms)`. Default: none.
66
+ */
67
+ total?: number;
68
+ /**
69
+ * Abort when no data has arrived for this many ms — while waiting for the
70
+ * headers and between body chunks. Unlike `total`, a stream that keeps
71
+ * flowing is never cut off: `timeout({ idle: 30_000 })`.
72
+ */
73
+ idle?: number;
74
+ };
75
+ /** One step of an upload, as `meta.uploadProgress` receives it. */
76
+ type UploadProgressEvent = {
77
+ /** Bytes sent so far. */
78
+ loaded: number;
79
+ /** Bytes in the whole body; `undefined` when it cannot be known (a stream without content-length). */
80
+ total: number | undefined;
81
+ /**
82
+ * `loaded / total`, a fraction from 0 to 1 — not a percentage: multiply
83
+ * by 100 for one. `undefined` when `total` is.
84
+ */
85
+ fraction: number | undefined;
86
+ };
87
+ type RetryOptions = {
88
+ /** Number of retries after the first attempt. Default 2 (3 requests total). */
89
+ retries?: number;
90
+ /** Statuses worth retrying. Default 408, 429, 500, 502, 503, 504; network errors (status 0) always are. */
91
+ statuses?: readonly number[];
92
+ /**
93
+ * Methods safe to repeat (QUERY is, per RFC 10008). Compared
94
+ * case-insensitively. Default GET, HEAD, OPTIONS, QUERY, PUT, DELETE.
95
+ */
96
+ methods?: readonly string[];
97
+ /**
98
+ * Returns the wait in ms before the next attempt. `attempt` is 0-based: 0
99
+ * after the first request failed. Default "full jitter": a random share
100
+ * of 300 · 2^attempt, so clients that failed together do not retry
101
+ * together. A `Retry-After` header wins over it. Must return a finite
102
+ * number ≥ 0, or the call rejects with a TypeError.
103
+ */
104
+ delayFn?: (attempt: number, error: unknown) => number;
105
+ /**
106
+ * Your own verdict on one failure (`attempt` 0-based, as for `delayFn`):
107
+ * `true` retries, `false` stops, `undefined` leaves it to
108
+ * `statuses`/`methods`. It sees every error — a TimeoutError from a
109
+ * `timeout()` placed after `retry()` included — but cannot exceed
110
+ * `retries`, resend a `ReadableStream` body, retry a call the caller
111
+ * aborted, or wait out a `Retry-After` longer than `maxRetryAfter`.
112
+ */
113
+ shouldRetry?: (error: unknown, attempt: number, requestContext: RequestContext) => boolean | undefined;
114
+ /**
115
+ * Called before each wait, for logs and metrics, with the same 0-based
116
+ * `attempt` and the `delay` in ms about to be waited.
117
+ */
118
+ onRetry?: (error: unknown, attempt: number, delay: number) => void;
119
+ /**
120
+ * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not
121
+ * retried at all. Default 60 000.
122
+ */
123
+ maxRetryAfter?: number;
124
+ };
125
+ /**
126
+ * Query parameters. `null`/`undefined` values are skipped, an array becomes
127
+ * repeated keys (`ids=1&ids=2`), and a `URLSearchParams` is used as is.
128
+ */
129
+ type QueryParams = Record<string, string | number | boolean | null | undefined | readonly (string | number | boolean | null | undefined)[]> | URLSearchParams;
130
+ //#endregion
131
+ //#region src/error/arthron-error.d.ts
132
+ /**
133
+ * What `new ArthronError(options)` takes — for a test or a plugin that
134
+ * builds one by hand. Each field becomes the error's field of that name.
135
+ */
136
+ type ArthronErrorOptions = {
137
+ message: string;
138
+ status: number;
139
+ statusText: string;
140
+ headers: Headers;
141
+ payload: unknown;
142
+ description?: string | undefined;
143
+ requestContext: RequestContext;
144
+ response?: Response;
145
+ cause?: unknown;
146
+ };
147
+ /**
148
+ * The only error arthron itself produces. Two sources: a response with
149
+ * `ok === false`, or fetch rejecting outright (network failure, `status` 0).
150
+ * Everything else — cancellation, interceptor bugs — is rethrown untouched.
151
+ */
152
+ declare class ArthronError extends Error {
153
+ readonly name = "ArthronError";
154
+ /** HTTP status of the failed response; `0` when fetch itself failed (network). */
155
+ readonly status: number;
156
+ readonly statusText: string;
157
+ /** Response headers; empty for network failures. Not enumerable — kept out of logs. */
158
+ readonly headers: Headers;
159
+ /**
160
+ * The failed response's payload, parsed: JSON if it parses, else the text;
161
+ * `undefined` for a network failure, which has no response. At most 64 KiB
162
+ * is read (text if cut). `response.body`, by contrast, is the stream.
163
+ */
164
+ readonly payload: unknown;
165
+ /**
166
+ * For people: the server's description of the failure, extracted from
167
+ * `payload` (by default its `message` → `error` → `error.message` →
168
+ * `detail`; see the `extractDescription` option). `undefined` when there was
169
+ * none, and for a network failure. Untrusted server text: show it as text,
170
+ * never as HTML. `message`, by contrast, is arthron's — method, URL
171
+ * without query, status — for logs, not for people.
172
+ */
173
+ readonly description: string | undefined;
174
+ /**
175
+ * The `RequestContext` that was sent — what plugins get as
176
+ * `requestContext`, after request interceptors: `url`, `init` (method,
177
+ * headers as a lowercase record, body), `meta`. Not a fetch `Request`: the
178
+ * method and headers are at `requestContext.init.method` and
179
+ * `.init.headers`. Not enumerable: it holds credentials, so
180
+ * `JSON.stringify`, `console.error` and error trackers leave it out. Read
181
+ * it directly when you need it.
182
+ */
183
+ readonly requestContext: RequestContext;
184
+ /** The failed Response, still readable — its body holds what arthron read (at most 64 KiB). Absent for network failures. Not enumerable. */
185
+ readonly response: Response | undefined;
186
+ /** The underlying error for network failures, with the URL's query and userinfo taken out of its message, stack and causes. */
187
+ readonly cause: unknown;
188
+ constructor(options: ArthronErrorOptions);
189
+ /** `instanceof ArthronError` holds for an error from any copy of the package. */
190
+ static [Symbol.hasInstance](value: unknown): boolean;
191
+ /** What serializers see: no headers, no request body, no query or userinfo. */
192
+ toJSON(): {
193
+ name: string;
194
+ message: string;
195
+ description: string | undefined;
196
+ status: number;
197
+ statusText: string;
198
+ method: string;
199
+ url: string;
200
+ payload: unknown;
201
+ };
202
+ }
203
+ /** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
204
+ declare function isArthronError(error: unknown): error is ArthronError;
205
+ //#endregion
206
+ //#region src/types/plugin.types.d.ts
207
+ /** Runs before each attempt. Returns the context to send (may be a new object). */
208
+ type RequestInterceptor = (requestContext: RequestContext) => RequestContext | Promise<RequestContext>;
209
+ /** Runs after a successful attempt. The body is single-use — `clone()` to peek. */
210
+ type ResponseInterceptor = (response: Response, requestContext: RequestContext) => Response | Promise<Response>;
211
+ /** Runs after a failed attempt. Returns the error to throw; it is always thrown. */
212
+ type ErrorInterceptor = (error: ArthronError, requestContext: RequestContext) => ArthronError | Promise<ArthronError>;
213
+ /**
214
+ * Wraps the whole call. `next` runs everything below it (request interceptors →
215
+ * fetch → response/error interceptors) and may be called again to retry.
216
+ * `next` rejects with an `ArthronError`, or with the untouched original when the
217
+ * failure was a cancellation or a bug in a plugin.
218
+ */
219
+ type Middleware = (requestContext: RequestContext, next: (requestContext: RequestContext) => Promise<Response>) => Promise<Response>;
220
+ interface Plugin {
221
+ /**
222
+ * Identity within a client, required: `use`/`extend` replace the plugin of
223
+ * the same pluginName in place, `without(pluginName)` removes it. The
224
+ * built-ins are "bearerAuth", "logging", "retry", "timeout" and
225
+ * "uploadProgress". Not `name`: a function has one, so a bare middleware
226
+ * would pass for a plugin.
227
+ */
228
+ pluginName: string;
229
+ /** Wraps the whole call and controls its flow: may call `next` again, or not at all. */
230
+ middleware?: Middleware;
231
+ /** Transforms each attempt's context before it is sent; returns the context to send. */
232
+ requestInterceptor?: RequestInterceptor;
233
+ /** Transforms each successful attempt's Response; returns the Response to pass on. */
234
+ responseInterceptor?: ResponseInterceptor;
235
+ /** Transforms each failed attempt's ArthronError; returns the error to throw. */
236
+ errorInterceptor?: ErrorInterceptor;
237
+ }
238
+ //#endregion
239
+ export { ResponseInterceptor as a, isArthronError as c, RequestContext as d, RetryOptions as f, RequestInterceptor as i, ArthronMeta as l, UploadProgressEvent as m, Middleware as n, ArthronError as o, TimeoutOptions as p, Plugin as r, ArthronErrorOptions as s, ErrorInterceptor as t, QueryParams as u };
@@ -0,0 +1,239 @@
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 function this attempt is sent with — the client's `fetchFn` at first,
16
+ * else the global `fetch`. A plugin may replace it for this attempt
17
+ * (`uploadProgress()` sends over XMLHttpRequest) or wrap it to observe.
18
+ * Always set on contexts arthron creates; optional so a context you build
19
+ * yourself stays valid.
20
+ */
21
+ fetchFn?: typeof fetch;
22
+ }
23
+ /**
24
+ * Per-call settings for plugins, passed as `api.get(path, { meta })` and
25
+ * carried next to `init` — never sent to fetch. Each built-in plugin with a
26
+ * per-call setting reads the key named after it — `bearerAuth`, `retry`,
27
+ * `timeout`, `uploadProgress`; your own add keys by augmentation:
28
+ *
29
+ * ```ts
30
+ * declare module "arthron" {
31
+ * interface ArthronMeta { skipTrace?: boolean }
32
+ * }
33
+ * ```
34
+ */
35
+ interface ArthronMeta {
36
+ /**
37
+ * `false` sends this call without the `bearerAuth()` token and never refreshes
38
+ * on its 401 — for the login and token-refresh endpoints.
39
+ */
40
+ bearerAuth?: false;
41
+ /**
42
+ * Upload progress for this call, with `uploadProgress()` on the client:
43
+ * `{ onProgress: ({ loaded, total, fraction }) => … }`. `onProgress` is
44
+ * never called without that plugin or without a body, nor where progress
45
+ * cannot be had: a client with its own `fetchFn`, another plugin's
46
+ * `fetchFn`, Deno, Bun, Cloudflare Workers, a service worker, or a
47
+ * `ReadableStream` body in a browser — those calls are sent as usual.
48
+ */
49
+ uploadProgress?: {
50
+ /** Called for each step of the upload, from 0 again on each attempt. */
51
+ onProgress: (event: UploadProgressEvent) => void;
52
+ };
53
+ /** `false` turns `retry()` off for this call; an object overrides its options. */
54
+ retry?: false | RetryOptions;
55
+ /**
56
+ * `false` lifts `timeout()`'s deadlines for this call; a number replaces
57
+ * its `total` (ms); an object overrides the parts it sets, `total` or
58
+ * `idle`: `{ idle: 30_000 }` keeps the configured total.
59
+ */
60
+ timeout?: false | number | TimeoutOptions;
61
+ }
62
+ type TimeoutOptions = {
63
+ /**
64
+ * Abort after this many ms in all, counted until the body has been fully
65
+ * read — the same deadline as `timeout(ms)`. Default: none.
66
+ */
67
+ total?: number;
68
+ /**
69
+ * Abort when no data has arrived for this many ms — while waiting for the
70
+ * headers and between body chunks. Unlike `total`, a stream that keeps
71
+ * flowing is never cut off: `timeout({ idle: 30_000 })`.
72
+ */
73
+ idle?: number;
74
+ };
75
+ /** One step of an upload, as `meta.uploadProgress` receives it. */
76
+ type UploadProgressEvent = {
77
+ /** Bytes sent so far. */
78
+ loaded: number;
79
+ /** Bytes in the whole body; `undefined` when it cannot be known (a stream without content-length). */
80
+ total: number | undefined;
81
+ /**
82
+ * `loaded / total`, a fraction from 0 to 1 — not a percentage: multiply
83
+ * by 100 for one. `undefined` when `total` is.
84
+ */
85
+ fraction: number | undefined;
86
+ };
87
+ type RetryOptions = {
88
+ /** Number of retries after the first attempt. Default 2 (3 requests total). */
89
+ retries?: number;
90
+ /** Statuses worth retrying. Default 408, 429, 500, 502, 503, 504; network errors (status 0) always are. */
91
+ statuses?: readonly number[];
92
+ /**
93
+ * Methods safe to repeat (QUERY is, per RFC 10008). Compared
94
+ * case-insensitively. Default GET, HEAD, OPTIONS, QUERY, PUT, DELETE.
95
+ */
96
+ methods?: readonly string[];
97
+ /**
98
+ * Returns the wait in ms before the next attempt. `attempt` is 0-based: 0
99
+ * after the first request failed. Default "full jitter": a random share
100
+ * of 300 · 2^attempt, so clients that failed together do not retry
101
+ * together. A `Retry-After` header wins over it. Must return a finite
102
+ * number ≥ 0, or the call rejects with a TypeError.
103
+ */
104
+ delayFn?: (attempt: number, error: unknown) => number;
105
+ /**
106
+ * Your own verdict on one failure (`attempt` 0-based, as for `delayFn`):
107
+ * `true` retries, `false` stops, `undefined` leaves it to
108
+ * `statuses`/`methods`. It sees every error — a TimeoutError from a
109
+ * `timeout()` placed after `retry()` included — but cannot exceed
110
+ * `retries`, resend a `ReadableStream` body, retry a call the caller
111
+ * aborted, or wait out a `Retry-After` longer than `maxRetryAfter`.
112
+ */
113
+ shouldRetry?: (error: unknown, attempt: number, requestContext: RequestContext) => boolean | undefined;
114
+ /**
115
+ * Called before each wait, for logs and metrics, with the same 0-based
116
+ * `attempt` and the `delay` in ms about to be waited.
117
+ */
118
+ onRetry?: (error: unknown, attempt: number, delay: number) => void;
119
+ /**
120
+ * Longest `Retry-After` (ms) worth honouring. Beyond it the request is not
121
+ * retried at all. Default 60 000.
122
+ */
123
+ maxRetryAfter?: number;
124
+ };
125
+ /**
126
+ * Query parameters. `null`/`undefined` values are skipped, an array becomes
127
+ * repeated keys (`ids=1&ids=2`), and a `URLSearchParams` is used as is.
128
+ */
129
+ type QueryParams = Record<string, string | number | boolean | null | undefined | readonly (string | number | boolean | null | undefined)[]> | URLSearchParams;
130
+ //#endregion
131
+ //#region src/error/arthron-error.d.ts
132
+ /**
133
+ * What `new ArthronError(options)` takes — for a test or a plugin that
134
+ * builds one by hand. Each field becomes the error's field of that name.
135
+ */
136
+ type ArthronErrorOptions = {
137
+ message: string;
138
+ status: number;
139
+ statusText: string;
140
+ headers: Headers;
141
+ payload: unknown;
142
+ description?: string | undefined;
143
+ requestContext: RequestContext;
144
+ response?: Response;
145
+ cause?: unknown;
146
+ };
147
+ /**
148
+ * The only error arthron itself produces. Two sources: a response with
149
+ * `ok === false`, or fetch rejecting outright (network failure, `status` 0).
150
+ * Everything else — cancellation, interceptor bugs — is rethrown untouched.
151
+ */
152
+ declare class ArthronError extends Error {
153
+ readonly name = "ArthronError";
154
+ /** HTTP status of the failed response; `0` when fetch itself failed (network). */
155
+ readonly status: number;
156
+ readonly statusText: string;
157
+ /** Response headers; empty for network failures. Not enumerable — kept out of logs. */
158
+ readonly headers: Headers;
159
+ /**
160
+ * The failed response's payload, parsed: JSON if it parses, else the text;
161
+ * `undefined` for a network failure, which has no response. At most 64 KiB
162
+ * is read (text if cut). `response.body`, by contrast, is the stream.
163
+ */
164
+ readonly payload: unknown;
165
+ /**
166
+ * For people: the server's description of the failure, extracted from
167
+ * `payload` (by default its `message` → `error` → `error.message` →
168
+ * `detail`; see the `extractDescription` option). `undefined` when there was
169
+ * none, and for a network failure. Untrusted server text: show it as text,
170
+ * never as HTML. `message`, by contrast, is arthron's — method, URL
171
+ * without query, status — for logs, not for people.
172
+ */
173
+ readonly description: string | undefined;
174
+ /**
175
+ * The `RequestContext` that was sent — what plugins get as
176
+ * `requestContext`, after request interceptors: `url`, `init` (method,
177
+ * headers as a lowercase record, body), `meta`. Not a fetch `Request`: the
178
+ * method and headers are at `requestContext.init.method` and
179
+ * `.init.headers`. Not enumerable: it holds credentials, so
180
+ * `JSON.stringify`, `console.error` and error trackers leave it out. Read
181
+ * it directly when you need it.
182
+ */
183
+ readonly requestContext: RequestContext;
184
+ /** The failed Response, still readable — its body holds what arthron read (at most 64 KiB). Absent for network failures. Not enumerable. */
185
+ readonly response: Response | undefined;
186
+ /** The underlying error for network failures, with the URL's query and userinfo taken out of its message, stack and causes. */
187
+ readonly cause: unknown;
188
+ constructor(options: ArthronErrorOptions);
189
+ /** `instanceof ArthronError` holds for an error from any copy of the package. */
190
+ static [Symbol.hasInstance](value: unknown): boolean;
191
+ /** What serializers see: no headers, no request body, no query or userinfo. */
192
+ toJSON(): {
193
+ name: string;
194
+ message: string;
195
+ description: string | undefined;
196
+ status: number;
197
+ statusText: string;
198
+ method: string;
199
+ url: string;
200
+ payload: unknown;
201
+ };
202
+ }
203
+ /** `true` for errors arthron produced: a `!ok` response or a network failure. Cancellations and plugin bugs are not ArthronErrors. */
204
+ declare function isArthronError(error: unknown): error is ArthronError;
205
+ //#endregion
206
+ //#region src/types/plugin.types.d.ts
207
+ /** Runs before each attempt. Returns the context to send (may be a new object). */
208
+ type RequestInterceptor = (requestContext: RequestContext) => RequestContext | Promise<RequestContext>;
209
+ /** Runs after a successful attempt. The body is single-use — `clone()` to peek. */
210
+ type ResponseInterceptor = (response: Response, requestContext: RequestContext) => Response | Promise<Response>;
211
+ /** Runs after a failed attempt. Returns the error to throw; it is always thrown. */
212
+ type ErrorInterceptor = (error: ArthronError, requestContext: RequestContext) => ArthronError | Promise<ArthronError>;
213
+ /**
214
+ * Wraps the whole call. `next` runs everything below it (request interceptors →
215
+ * fetch → response/error interceptors) and may be called again to retry.
216
+ * `next` rejects with an `ArthronError`, or with the untouched original when the
217
+ * failure was a cancellation or a bug in a plugin.
218
+ */
219
+ type Middleware = (requestContext: RequestContext, next: (requestContext: RequestContext) => Promise<Response>) => Promise<Response>;
220
+ interface Plugin {
221
+ /**
222
+ * Identity within a client, required: `use`/`extend` replace the plugin of
223
+ * the same pluginName in place, `without(pluginName)` removes it. The
224
+ * built-ins are "bearerAuth", "logging", "retry", "timeout" and
225
+ * "uploadProgress". Not `name`: a function has one, so a bare middleware
226
+ * would pass for a plugin.
227
+ */
228
+ pluginName: string;
229
+ /** Wraps the whole call and controls its flow: may call `next` again, or not at all. */
230
+ middleware?: Middleware;
231
+ /** Transforms each attempt's context before it is sent; returns the context to send. */
232
+ requestInterceptor?: RequestInterceptor;
233
+ /** Transforms each successful attempt's Response; returns the Response to pass on. */
234
+ responseInterceptor?: ResponseInterceptor;
235
+ /** Transforms each failed attempt's ArthronError; returns the error to throw. */
236
+ errorInterceptor?: ErrorInterceptor;
237
+ }
238
+ //#endregion
239
+ export { ResponseInterceptor as a, isArthronError as c, RequestContext as d, RetryOptions as f, RequestInterceptor as i, ArthronMeta as l, UploadProgressEvent as m, Middleware as n, ArthronError as o, TimeoutOptions as p, Plugin as r, ArthronErrorOptions as s, ErrorInterceptor as t, QueryParams as u };