@mittwald/axios-cache-with-retry 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/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/key.js +58 -0
- package/dist/esm/response.js +40 -0
- package/dist/esm/retry.js +66 -0
- package/dist/esm/setup.js +253 -0
- package/dist/esm/storage.js +73 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/key.d.ts +5 -0
- package/dist/types/response.d.ts +5 -0
- package/dist/types/retry.d.ts +5 -0
- package/dist/types/setup.d.ts +3 -0
- package/dist/types/storage.d.ts +19 -0
- package/dist/types/types.d.ts +86 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mittwald CM Service GmbH & Co. KG and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @mittwald/axios-cache-with-retry
|
|
2
|
+
|
|
3
|
+
Coordinated retry, cache and in-flight dedupe for Axios — as a single adapter
|
|
4
|
+
rather than three interceptors that each re-run the request behind each other's
|
|
5
|
+
back.
|
|
6
|
+
|
|
7
|
+
Retry and caching cannot be composed out of separate interceptors: a retry is a
|
|
8
|
+
second dispatch, so it re-enters the interceptor chain and every layer around it
|
|
9
|
+
sees a request the caller never made. Combining `axios-retry` with
|
|
10
|
+
`axios-cache-interceptor` therefore multiplies requests instead of deduplicating
|
|
11
|
+
them, and which of the two even gets to see a failure depends on
|
|
12
|
+
`validateStatus`. [Why this exists](#why-this-exists) walks through a concrete
|
|
13
|
+
example.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @mittwald/axios-cache-with-retry
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`axios` is a peer dependency (`^1`).
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import axios from "axios";
|
|
27
|
+
import { setupAxiosRetryCache } from "@mittwald/axios-cache-with-retry";
|
|
28
|
+
|
|
29
|
+
const client = setupAxiosRetryCache(axios.create(), {
|
|
30
|
+
requestKey: ({ config }) => `${config.method ?? "get"}:${config.url}`,
|
|
31
|
+
cache: { ttl: 60_000, staleIfError: true },
|
|
32
|
+
retry: { retries: 3, retryOnStatus: [408, 429, 500, 502, 503, 504] },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await client.get("/users");
|
|
36
|
+
await client.retryCache.invalidate("get:/users");
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The retry decision is made for thrown Axios errors and for regular responses
|
|
40
|
+
alike, so it keeps working under `validateStatus: () => true`.
|
|
41
|
+
|
|
42
|
+
`setupAxiosRetryCache` replaces the instance's adapter and returns the same
|
|
43
|
+
instance, typed with an added `retryCache` property. Calling it twice on one
|
|
44
|
+
instance re-wraps the original adapter rather than stacking two layers.
|
|
45
|
+
|
|
46
|
+
## Per-request options
|
|
47
|
+
|
|
48
|
+
Every request config accepts a `retryCache` field that overrides the global
|
|
49
|
+
options for that one request. The module augments Axios' own
|
|
50
|
+
`AxiosRequestConfig`, so the field is typed wherever `axios` is.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
await client.get("/users", { retryCache: { retry: { retries: 5 } } });
|
|
54
|
+
await client.get("/metrics", { retryCache: { cache: false } });
|
|
55
|
+
await client.get("/raw", { retryCache: false });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Value | Effect |
|
|
59
|
+
| ------------------- | ------------------------------------------------ |
|
|
60
|
+
| `false` | Bypasses the adapter entirely |
|
|
61
|
+
| `true` | Enables cache and retry with the global settings |
|
|
62
|
+
| `{ cache, retry }` | Merges into the global settings for this request |
|
|
63
|
+
| `{ dedupe: false }` | Opts this request out of in-flight deduplication |
|
|
64
|
+
|
|
65
|
+
## Options
|
|
66
|
+
|
|
67
|
+
### `cache`
|
|
68
|
+
|
|
69
|
+
| Option | Default | Description |
|
|
70
|
+
| -------------- | ---------------- | ------------------------------------------------------------ |
|
|
71
|
+
| `enabled` | `true` | Set to `false` to keep the cache off until a request opts in |
|
|
72
|
+
| `ttl` | `60000` | Lifetime of an entry in milliseconds |
|
|
73
|
+
| `methods` | `["get","head"]` | Methods whose responses are cached |
|
|
74
|
+
| `staleIfError` | `false` | Serve an expired entry once retries are exhausted |
|
|
75
|
+
| `shouldCache` | — | Predicate deciding whether a response is stored |
|
|
76
|
+
|
|
77
|
+
### `retry`
|
|
78
|
+
|
|
79
|
+
| Option | Default | Description |
|
|
80
|
+
| --------------------- | ----------------------------------- | -------------------------------------------------------- |
|
|
81
|
+
| `enabled` | `true` | Set to `false` to keep retry off until a request opts in |
|
|
82
|
+
| `retries` | `2` | Attempts after the initial one |
|
|
83
|
+
| `methods` | `["get","head","options"]` | Methods that may be retried |
|
|
84
|
+
| `retryOnStatus` | `408, 425, 429, 500, 502, 503, 504` | Status codes that trigger a retry |
|
|
85
|
+
| `retryOnNetworkError` | `true` | Retry errors that never produced a response |
|
|
86
|
+
| `respectRetryAfter` | `true` | Honour a `Retry-After` response header |
|
|
87
|
+
| `delay` | exponential, capped at 30 s | Fixed milliseconds or a function |
|
|
88
|
+
| `shouldRetry` | — | Replaces the built-in decision entirely |
|
|
89
|
+
|
|
90
|
+
### `requestKey`
|
|
91
|
+
|
|
92
|
+
Cache and dedupe key for a request. Defaults to method, base URL, URL, a stably
|
|
93
|
+
serialized `params` and — for non-`GET`/`HEAD` — a stably serialized body.
|
|
94
|
+
Returning `undefined` excludes the request from both caching and deduplication.
|
|
95
|
+
|
|
96
|
+
### `storage`
|
|
97
|
+
|
|
98
|
+
Defaults to an in-memory store. Pass `createMemoryStorage({ maxEntries })` for a
|
|
99
|
+
bounded one, or any object implementing `RetryCacheStorage`; every method may
|
|
100
|
+
return a promise, so an async backend works as well.
|
|
101
|
+
|
|
102
|
+
## Cache API
|
|
103
|
+
|
|
104
|
+
`client.retryCache` exposes `get`, `set`, `invalidate`, `invalidatePrefix` and
|
|
105
|
+
`clear`. `invalidatePrefix` needs a storage that implements either
|
|
106
|
+
`deletePrefix` or `keys` — the built-in memory storage implements both.
|
|
107
|
+
|
|
108
|
+
## Behaviour worth knowing
|
|
109
|
+
|
|
110
|
+
- **Deduplication wraps the whole operation**, retries included: concurrent
|
|
111
|
+
callers with the same key wait for one shared result and each receive their
|
|
112
|
+
own shallow copy.
|
|
113
|
+
- **A cache hit short-circuits before retry**, so a cached response never
|
|
114
|
+
produces a request.
|
|
115
|
+
- **`staleIfError` only serves an expired entry after retries are exhausted**,
|
|
116
|
+
never instead of a retry.
|
|
117
|
+
- **Only responses are cached, never errors.**
|
|
118
|
+
|
|
119
|
+
## Why this exists
|
|
120
|
+
|
|
121
|
+
Retry, cache and in-flight deduplication are three decisions about the _same_
|
|
122
|
+
request, but an interceptor can only see a request on its way out and a response
|
|
123
|
+
on its way back. It cannot wrap the dispatch itself — and a retry is by
|
|
124
|
+
definition a second dispatch. So an interceptor-based retry has to re-send the
|
|
125
|
+
config, which means re-entering the interceptor chain, which means every other
|
|
126
|
+
layer sees a request the caller never made. Stacking the layers is the only
|
|
127
|
+
composition the interceptor API offers, and both stacking orders are wrong.
|
|
128
|
+
|
|
129
|
+
The popular packages each solve one third of the problem and sit in a position
|
|
130
|
+
that collides with the others:
|
|
131
|
+
|
|
132
|
+
- **`axios-retry`** and **`retry-axios`** hook the response (error) path and
|
|
133
|
+
retry by re-dispatching the request config.
|
|
134
|
+
- **`axios-cache-interceptor`** caches _and_ deduplicates concurrent requests
|
|
135
|
+
from a request/response interceptor pair, with its own in-flight bookkeeping
|
|
136
|
+
keyed per request.
|
|
137
|
+
- **`axios-cache-adapter`** (unmaintained) and **`axios-extensions`**
|
|
138
|
+
(`cacheAdapterEnhancer`, `throttleAdapterEnhancer`) take the adapter position
|
|
139
|
+
instead — so they cannot coexist with each other, or with anything else that
|
|
140
|
+
wants to own the adapter.
|
|
141
|
+
|
|
142
|
+
### Where it snags
|
|
143
|
+
|
|
144
|
+
Three components ask for `GET /users` at the same time. The endpoint answers
|
|
145
|
+
`503` twice and then `200`. Retry is configured for two extra attempts, and the
|
|
146
|
+
cache layer deduplicates in-flight requests. One logical request, one expected
|
|
147
|
+
outcome: three responses out of three network calls.
|
|
148
|
+
|
|
149
|
+
**Deduplication inside the retry** (cache layer closest to the request): dedupe
|
|
150
|
+
collapses the three callers onto one dispatch, that dispatch fails with `503`,
|
|
151
|
+
and the rejection fans back out to all three callers — each of which then runs
|
|
152
|
+
its _own_ retry handler, because each call walks the interceptor chain
|
|
153
|
+
separately. Three retry loops instead of one, up to nine network calls for one
|
|
154
|
+
logical `GET`, and whichever loop happens to finish first decides what ends up
|
|
155
|
+
in the cache while the others are still retrying.
|
|
156
|
+
|
|
157
|
+
**Retry inside the deduplication** (retry layer closest to the request): every
|
|
158
|
+
re-dispatch looks like a brand-new request to the cache layer above it, while
|
|
159
|
+
that layer's in-flight entry for the first attempt is still open and still
|
|
160
|
+
waiting to be settled by an attempt that has already been abandoned. Depending
|
|
161
|
+
on how the cache keys its pending state, the second attempt either registers
|
|
162
|
+
alongside the first — so the entry that gets stored is not the response the
|
|
163
|
+
caller received — or waits on a promise the retry loop is never going to fulfil.
|
|
164
|
+
|
|
165
|
+
**And the two layers disagree about what a failure is.** Retry lives on the
|
|
166
|
+
error path, the cache lives on the success path, and which one a `503` takes is
|
|
167
|
+
the caller's `validateStatus` setting. Accept non-2xx responses and the retry
|
|
168
|
+
handler is never invoked at all, while the cache stores the `503` and serves it
|
|
169
|
+
for the rest of its TTL.
|
|
170
|
+
|
|
171
|
+
### What this package does instead
|
|
172
|
+
|
|
173
|
+
All three concerns live in a single adapter, the one place that _does_ wrap the
|
|
174
|
+
dispatch. One request key is resolved once, and one shared in-flight promise
|
|
175
|
+
covers the entire operation: the cache lookup, the retry loop, and the cache
|
|
176
|
+
write after the final attempt. Concurrent callers wait for that one result
|
|
177
|
+
instead of starting their own loops, retries never re-enter the interceptor
|
|
178
|
+
chain, and the retry decision is made for returned responses and thrown errors
|
|
179
|
+
alike — so `validateStatus` stops being part of the retry semantics. The
|
|
180
|
+
guarantees that follow from it are listed under
|
|
181
|
+
[Behaviour worth knowing](#behaviour-worth-knowing) above.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
package/dist/esm/key.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export async function resolveRequestKey(key, config) {
|
|
2
|
+
if (typeof key === "function") {
|
|
3
|
+
return key({ config });
|
|
4
|
+
}
|
|
5
|
+
return defaultRequestKey(config);
|
|
6
|
+
}
|
|
7
|
+
export function defaultRequestKey(config) {
|
|
8
|
+
const method = (config.method ?? "get").toLowerCase();
|
|
9
|
+
const baseURL = config.baseURL ?? "";
|
|
10
|
+
const url = config.url ?? "";
|
|
11
|
+
const params = stableSerialize(config.params);
|
|
12
|
+
const data = method === "get" || method === "head" ? "" : stableSerialize(config.data);
|
|
13
|
+
if (!url) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return [method, baseURL, url, params, data].join(" ");
|
|
17
|
+
}
|
|
18
|
+
export function stableSerialize(value) {
|
|
19
|
+
if (value === undefined || value === null || value === "") {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
if (isURLSearchParams(value)) {
|
|
26
|
+
return serializeURLSearchParams(value);
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(sortValue(value));
|
|
29
|
+
}
|
|
30
|
+
function sortValue(value) {
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map(sortValue);
|
|
33
|
+
}
|
|
34
|
+
if (!isPlainRecord(value)) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return Object.keys(value)
|
|
38
|
+
.sort()
|
|
39
|
+
.reduce((accumulator, key) => {
|
|
40
|
+
accumulator[key] = sortValue(value[key]);
|
|
41
|
+
return accumulator;
|
|
42
|
+
}, {});
|
|
43
|
+
}
|
|
44
|
+
function isPlainRecord(value) {
|
|
45
|
+
return (typeof value === "object" && value !== null && value.constructor === Object);
|
|
46
|
+
}
|
|
47
|
+
function isURLSearchParams(value) {
|
|
48
|
+
return (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams);
|
|
49
|
+
}
|
|
50
|
+
function serializeURLSearchParams(params) {
|
|
51
|
+
return JSON.stringify(Array.from(params.entries()).sort(([leftKey, leftValue], [rightKey, rightValue]) => {
|
|
52
|
+
const keyComparison = leftKey.localeCompare(rightKey);
|
|
53
|
+
if (keyComparison !== 0) {
|
|
54
|
+
return keyComparison;
|
|
55
|
+
}
|
|
56
|
+
return leftValue.localeCompare(rightValue);
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function snapshotResponse(response) {
|
|
2
|
+
return {
|
|
3
|
+
data: response.data,
|
|
4
|
+
status: response.status,
|
|
5
|
+
statusText: response.statusText,
|
|
6
|
+
headers: normalizeHeaders(response.headers),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function responseFromCache(entry, config) {
|
|
10
|
+
return {
|
|
11
|
+
data: entry.response.data,
|
|
12
|
+
status: entry.response.status,
|
|
13
|
+
statusText: entry.response.statusText,
|
|
14
|
+
headers: { ...entry.response.headers },
|
|
15
|
+
config,
|
|
16
|
+
request: undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function cloneResponse(response) {
|
|
20
|
+
return {
|
|
21
|
+
...response,
|
|
22
|
+
headers: { ...response.headers },
|
|
23
|
+
config: { ...response.config },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function normalizeHeaders(headers) {
|
|
27
|
+
if (!headers || typeof headers !== "object") {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
const output = {};
|
|
31
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
32
|
+
if (value === undefined || value === null) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
output[key.toLowerCase()] = Array.isArray(value)
|
|
36
|
+
? value.join(", ")
|
|
37
|
+
: String(value);
|
|
38
|
+
}
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const DEFAULT_RETRY_STATUS = [408, 425, 429, 500, 502, 503, 504];
|
|
2
|
+
export async function shouldRetry(context, retry) {
|
|
3
|
+
if (context.attempt > retry.retries) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
if (retry.shouldRetry) {
|
|
7
|
+
return retry.shouldRetry(context);
|
|
8
|
+
}
|
|
9
|
+
const method = (context.config.method ?? "get").toLowerCase();
|
|
10
|
+
const allowedMethods = retry.methods?.map((value) => value.toLowerCase());
|
|
11
|
+
if (allowedMethods && !allowedMethods.includes(method)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (context.response) {
|
|
15
|
+
return (retry.retryOnStatus ?? DEFAULT_RETRY_STATUS).includes(context.response.status);
|
|
16
|
+
}
|
|
17
|
+
return (retry.retryOnNetworkError !== false &&
|
|
18
|
+
isRetryableNetworkError(context.error));
|
|
19
|
+
}
|
|
20
|
+
export async function retryDelay(context, retry) {
|
|
21
|
+
const retryAfter = retry.respectRetryAfter
|
|
22
|
+
? parseRetryAfter(context.response?.headers?.["retry-after"])
|
|
23
|
+
: undefined;
|
|
24
|
+
if (retryAfter !== undefined) {
|
|
25
|
+
return retryAfter;
|
|
26
|
+
}
|
|
27
|
+
if (typeof retry.delay === "number") {
|
|
28
|
+
return retry.delay;
|
|
29
|
+
}
|
|
30
|
+
if (typeof retry.delay === "function") {
|
|
31
|
+
return retry.delay(context);
|
|
32
|
+
}
|
|
33
|
+
return Math.min(100 * 2 ** Math.max(context.attempt - 1, 0), 30_000);
|
|
34
|
+
}
|
|
35
|
+
export function sleep(ms) {
|
|
36
|
+
if (ms <= 0) {
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
setTimeout(resolve, ms);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function isRetryableNetworkError(error) {
|
|
44
|
+
if (!error) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const maybeAxiosError = error;
|
|
48
|
+
if (maybeAxiosError.response) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function parseRetryAfter(value) {
|
|
54
|
+
if (typeof value !== "string") {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const seconds = Number(value);
|
|
58
|
+
if (Number.isFinite(seconds)) {
|
|
59
|
+
return Math.max(0, seconds * 1000);
|
|
60
|
+
}
|
|
61
|
+
const timestamp = Date.parse(value);
|
|
62
|
+
if (Number.isNaN(timestamp)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return Math.max(0, timestamp - Date.now());
|
|
66
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { resolveRequestKey } from "./key.js";
|
|
3
|
+
import { cloneResponse, responseFromCache, snapshotResponse, } from "./response.js";
|
|
4
|
+
import { DEFAULT_RETRY_STATUS, retryDelay, shouldRetry, sleep, } from "./retry.js";
|
|
5
|
+
import { createMemoryStorage, deletePrefix } from "./storage.js";
|
|
6
|
+
const DEFAULT_CACHE = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
ttl: 60_000,
|
|
9
|
+
methods: ["get", "head"],
|
|
10
|
+
staleIfError: false,
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_RETRY = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
retries: 2,
|
|
15
|
+
methods: ["get", "head", "options"],
|
|
16
|
+
retryOnStatus: DEFAULT_RETRY_STATUS,
|
|
17
|
+
retryOnNetworkError: true,
|
|
18
|
+
respectRetryAfter: true,
|
|
19
|
+
};
|
|
20
|
+
const installedAdapters = new WeakMap();
|
|
21
|
+
export function setupAxiosRetryCache(instance, options = {}) {
|
|
22
|
+
const storage = options.storage ?? createMemoryStorage();
|
|
23
|
+
const originalAdapter = installedAdapters.get(instance) ??
|
|
24
|
+
axios.getAdapter(instance.defaults.adapter);
|
|
25
|
+
const inflight = new Map();
|
|
26
|
+
installedAdapters.set(instance, originalAdapter);
|
|
27
|
+
instance.defaults.adapter = async (config) => {
|
|
28
|
+
const requestOptions = config.retryCache;
|
|
29
|
+
if (requestOptions === false) {
|
|
30
|
+
return originalAdapter(config);
|
|
31
|
+
}
|
|
32
|
+
const effective = resolveEffectiveOptions(options, requestOptions);
|
|
33
|
+
const method = (config.method ?? "get").toLowerCase();
|
|
34
|
+
const cacheEnabled = Boolean(effective.cache && methodAllowed(method, effective.cache.methods));
|
|
35
|
+
const retryEnabled = Boolean(effective.retry);
|
|
36
|
+
const dedupeEnabled = effective.dedupe !== false;
|
|
37
|
+
const requestKey = await resolveRequestKey(options.requestKey, config);
|
|
38
|
+
const cacheKey = requestKey;
|
|
39
|
+
const dedupeKey = requestKey;
|
|
40
|
+
if (!cacheEnabled && !retryEnabled) {
|
|
41
|
+
return originalAdapter(config);
|
|
42
|
+
}
|
|
43
|
+
if (cacheEnabled && cacheKey) {
|
|
44
|
+
const cached = await storage.get(cacheKey);
|
|
45
|
+
if (cached && isFresh(cached)) {
|
|
46
|
+
return responseFromCache(cached, config);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const operation = () => runOperation({
|
|
50
|
+
adapter: originalAdapter,
|
|
51
|
+
config,
|
|
52
|
+
cacheKey,
|
|
53
|
+
cacheEnabled,
|
|
54
|
+
retry: effective.retry,
|
|
55
|
+
cache: effective.cache,
|
|
56
|
+
staleEntry: cacheEnabled && cacheKey ? storage.get(cacheKey) : undefined,
|
|
57
|
+
storage,
|
|
58
|
+
});
|
|
59
|
+
if (dedupeEnabled && dedupeKey) {
|
|
60
|
+
const existing = inflight.get(dedupeKey);
|
|
61
|
+
if (existing) {
|
|
62
|
+
return cloneResponse(await existing);
|
|
63
|
+
}
|
|
64
|
+
const promise = operation().finally(() => {
|
|
65
|
+
inflight.delete(dedupeKey);
|
|
66
|
+
});
|
|
67
|
+
inflight.set(dedupeKey, promise);
|
|
68
|
+
return cloneResponse(await promise);
|
|
69
|
+
}
|
|
70
|
+
return operation();
|
|
71
|
+
};
|
|
72
|
+
const client = instance;
|
|
73
|
+
client.retryCache = {
|
|
74
|
+
async get(key) {
|
|
75
|
+
return storage.get(key);
|
|
76
|
+
},
|
|
77
|
+
async set(key, response, setOptions = {}) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
await storage.set(key, {
|
|
80
|
+
key,
|
|
81
|
+
createdAt: now,
|
|
82
|
+
expiresAt: now + (setOptions.ttl ?? normalizeCacheOptions(options.cache).ttl),
|
|
83
|
+
response: snapshotResponse(response),
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
async invalidate(key) {
|
|
87
|
+
return storage.delete(key);
|
|
88
|
+
},
|
|
89
|
+
async invalidatePrefix(prefix) {
|
|
90
|
+
return deletePrefix(storage, prefix);
|
|
91
|
+
},
|
|
92
|
+
async clear() {
|
|
93
|
+
await storage.clear();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return client;
|
|
97
|
+
}
|
|
98
|
+
async function runOperation(input) {
|
|
99
|
+
const staleEntry = await input.staleEntry;
|
|
100
|
+
let attempt = 1;
|
|
101
|
+
let lastError;
|
|
102
|
+
while (true) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await input.adapter(input.config);
|
|
105
|
+
const retryOptions = input.retry;
|
|
106
|
+
const retry = retryOptions
|
|
107
|
+
? await shouldRetry({
|
|
108
|
+
attempt,
|
|
109
|
+
retries: retryOptions.retries,
|
|
110
|
+
config: input.config,
|
|
111
|
+
response,
|
|
112
|
+
}, retryOptions)
|
|
113
|
+
: false;
|
|
114
|
+
if (retry && retryOptions) {
|
|
115
|
+
await sleep(await retryDelay({
|
|
116
|
+
attempt,
|
|
117
|
+
retries: retryOptions.retries,
|
|
118
|
+
config: input.config,
|
|
119
|
+
response,
|
|
120
|
+
}, retryOptions));
|
|
121
|
+
attempt += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (input.cacheEnabled &&
|
|
125
|
+
input.cacheKey &&
|
|
126
|
+
input.cache &&
|
|
127
|
+
(await shouldCache(input.cacheKey, input.config, response, input.cache))) {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
await input.storage?.set(input.cacheKey, {
|
|
130
|
+
key: input.cacheKey,
|
|
131
|
+
createdAt: now,
|
|
132
|
+
expiresAt: now + input.cache.ttl,
|
|
133
|
+
response: snapshotResponse(response),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
lastError = error;
|
|
140
|
+
const axiosError = error;
|
|
141
|
+
const errorResponse = axiosError?.response;
|
|
142
|
+
const retryOptions = input.retry;
|
|
143
|
+
const retry = retryOptions
|
|
144
|
+
? await shouldRetry({
|
|
145
|
+
attempt,
|
|
146
|
+
retries: retryOptions.retries,
|
|
147
|
+
config: input.config,
|
|
148
|
+
response: errorResponse,
|
|
149
|
+
error: error instanceof Error ? error : undefined,
|
|
150
|
+
}, retryOptions)
|
|
151
|
+
: false;
|
|
152
|
+
if (retry && retryOptions) {
|
|
153
|
+
await sleep(await retryDelay({
|
|
154
|
+
attempt,
|
|
155
|
+
retries: retryOptions.retries,
|
|
156
|
+
config: input.config,
|
|
157
|
+
response: errorResponse,
|
|
158
|
+
error: error instanceof Error ? error : undefined,
|
|
159
|
+
}, retryOptions));
|
|
160
|
+
attempt += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (input.cache &&
|
|
164
|
+
input.cache.staleIfError &&
|
|
165
|
+
staleEntry &&
|
|
166
|
+
!isFresh(staleEntry)) {
|
|
167
|
+
return responseFromCache(staleEntry, input.config);
|
|
168
|
+
}
|
|
169
|
+
throw lastError;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function resolveEffectiveOptions(globalOptions, requestOptions) {
|
|
174
|
+
const requestOverrides = requestOptions === true
|
|
175
|
+
? { cache: true, retry: true }
|
|
176
|
+
: typeof requestOptions === "object"
|
|
177
|
+
? requestOptions
|
|
178
|
+
: undefined;
|
|
179
|
+
const cache = resolveCacheOptions(globalOptions.cache, requestOverrides?.cache);
|
|
180
|
+
const retry = resolveRetryOptions(globalOptions.retry, requestOverrides?.retry);
|
|
181
|
+
return {
|
|
182
|
+
cache,
|
|
183
|
+
retry,
|
|
184
|
+
dedupe: requestOverrides?.dedupe ?? globalOptions.dedupe ?? true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function resolveCacheOptions(globalCache, requestCache) {
|
|
188
|
+
if (requestCache === false) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (requestCache === true) {
|
|
192
|
+
return {
|
|
193
|
+
...normalizeCacheOptions(globalCache),
|
|
194
|
+
enabled: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (globalCache === false &&
|
|
198
|
+
(typeof requestCache !== "object" || requestCache.enabled !== true)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const cache = {
|
|
202
|
+
...normalizeCacheOptions(globalCache),
|
|
203
|
+
...definedOptions(requestCache),
|
|
204
|
+
};
|
|
205
|
+
return cache.enabled === false ? false : cache;
|
|
206
|
+
}
|
|
207
|
+
function resolveRetryOptions(globalRetry, requestRetry) {
|
|
208
|
+
if (requestRetry === false) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (requestRetry === true) {
|
|
212
|
+
return {
|
|
213
|
+
...normalizeRetryOptions(globalRetry),
|
|
214
|
+
enabled: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (globalRetry === false &&
|
|
218
|
+
(typeof requestRetry !== "object" || requestRetry.enabled !== true)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const retry = {
|
|
222
|
+
...normalizeRetryOptions(globalRetry),
|
|
223
|
+
...definedOptions(requestRetry),
|
|
224
|
+
};
|
|
225
|
+
return retry.enabled === false ? false : retry;
|
|
226
|
+
}
|
|
227
|
+
function normalizeCacheOptions(cache) {
|
|
228
|
+
return {
|
|
229
|
+
...DEFAULT_CACHE,
|
|
230
|
+
...definedOptions(cache),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function normalizeRetryOptions(retry) {
|
|
234
|
+
return {
|
|
235
|
+
...DEFAULT_RETRY,
|
|
236
|
+
...definedOptions(retry),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function definedOptions(options) {
|
|
240
|
+
if (!options) {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
|
|
244
|
+
}
|
|
245
|
+
function shouldCache(key, config, response, cache) {
|
|
246
|
+
return cache.shouldCache?.({ key, config, response }) ?? true;
|
|
247
|
+
}
|
|
248
|
+
function methodAllowed(method, methods) {
|
|
249
|
+
return (!methods || methods.map((value) => value.toLowerCase()).includes(method));
|
|
250
|
+
}
|
|
251
|
+
function isFresh(entry) {
|
|
252
|
+
return entry.expiresAt > Date.now();
|
|
253
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class MemoryRetryCacheStorage {
|
|
2
|
+
options;
|
|
3
|
+
entries = new Map();
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.options = options;
|
|
6
|
+
}
|
|
7
|
+
get(key) {
|
|
8
|
+
return this.entries.get(key);
|
|
9
|
+
}
|
|
10
|
+
set(key, entry) {
|
|
11
|
+
this.entries.set(key, entry);
|
|
12
|
+
this.enforceMaxEntries();
|
|
13
|
+
}
|
|
14
|
+
delete(key) {
|
|
15
|
+
return this.entries.delete(key);
|
|
16
|
+
}
|
|
17
|
+
clear() {
|
|
18
|
+
this.entries.clear();
|
|
19
|
+
}
|
|
20
|
+
keys() {
|
|
21
|
+
return this.entries.keys();
|
|
22
|
+
}
|
|
23
|
+
deletePrefix(prefix) {
|
|
24
|
+
let deleted = 0;
|
|
25
|
+
for (const key of this.entries.keys()) {
|
|
26
|
+
if (key.startsWith(prefix) && this.entries.delete(key)) {
|
|
27
|
+
deleted += 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return deleted;
|
|
31
|
+
}
|
|
32
|
+
enforceMaxEntries() {
|
|
33
|
+
const { maxEntries } = this.options;
|
|
34
|
+
if (!maxEntries || this.entries.size <= maxEntries) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const overflow = this.entries.size - maxEntries;
|
|
38
|
+
const keys = this.entries.keys();
|
|
39
|
+
for (let index = 0; index < overflow; index += 1) {
|
|
40
|
+
const next = keys.next();
|
|
41
|
+
if (next.done) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.entries.delete(next.value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function createMemoryStorage(options) {
|
|
49
|
+
return new MemoryRetryCacheStorage(options);
|
|
50
|
+
}
|
|
51
|
+
export async function deletePrefix(storage, prefix) {
|
|
52
|
+
if (storage.deletePrefix) {
|
|
53
|
+
return storage.deletePrefix(prefix);
|
|
54
|
+
}
|
|
55
|
+
if (!storage.keys) {
|
|
56
|
+
throw new Error("Storage backend does not support prefix invalidation.");
|
|
57
|
+
}
|
|
58
|
+
let deleted = 0;
|
|
59
|
+
const keys = await storage.keys();
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
if (!key.startsWith(prefix)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const didDelete = await storage.delete(key);
|
|
65
|
+
if (didDelete) {
|
|
66
|
+
deleted += 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return deleted;
|
|
70
|
+
}
|
|
71
|
+
export async function maybeAwait(value) {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { setupAxiosRetryCache } from "./setup.js";
|
|
2
|
+
export { createMemoryStorage, MemoryRetryCacheStorage } from "./storage.js";
|
|
3
|
+
export type { AxiosRetryCacheInstance, CacheEntry, CachedResponse, CacheOptions, RequestKeyContext, RetryCacheApi, RetryCacheOptions, RetryCacheRequestKey, RetryCacheRequestOptions, RetryCacheStorage, RetryDecisionContext, RetryDelayContext, RetryOptions, } from "./types.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { InternalAxiosRequestConfig } from "axios";
|
|
2
|
+
import type { RetryCacheRequestKey } from "./types.js";
|
|
3
|
+
export declare function resolveRequestKey(key: RetryCacheRequestKey | undefined, config: InternalAxiosRequestConfig): Promise<string | undefined>;
|
|
4
|
+
export declare function defaultRequestKey(config: InternalAxiosRequestConfig): string | undefined;
|
|
5
|
+
export declare function stableSerialize(value: unknown): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
|
2
|
+
import type { CacheEntry, CachedResponse } from "./types.js";
|
|
3
|
+
export declare function snapshotResponse(response: AxiosResponse): CachedResponse;
|
|
4
|
+
export declare function responseFromCache(entry: CacheEntry, config: InternalAxiosRequestConfig): AxiosResponse;
|
|
5
|
+
export declare function cloneResponse(response: AxiosResponse): AxiosResponse;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RetryDecisionContext, RetryDelayContext, RetryOptions } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_RETRY_STATUS: number[];
|
|
3
|
+
export declare function shouldRetry(context: RetryDecisionContext, retry: RetryOptions): Promise<boolean>;
|
|
4
|
+
export declare function retryDelay(context: RetryDelayContext, retry: RetryOptions): Promise<number>;
|
|
5
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Awaitable, CacheEntry, RetryCacheStorage } from "./types.js";
|
|
2
|
+
export interface MemoryStorageOptions {
|
|
3
|
+
maxEntries?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class MemoryRetryCacheStorage<T = unknown> implements RetryCacheStorage<T> {
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly entries;
|
|
8
|
+
constructor(options?: MemoryStorageOptions);
|
|
9
|
+
get(key: string): CacheEntry<T> | undefined;
|
|
10
|
+
set(key: string, entry: CacheEntry<T>): void;
|
|
11
|
+
delete(key: string): boolean;
|
|
12
|
+
clear(): void;
|
|
13
|
+
keys(): Iterable<string>;
|
|
14
|
+
deletePrefix(prefix: string): number;
|
|
15
|
+
private enforceMaxEntries;
|
|
16
|
+
}
|
|
17
|
+
export declare function createMemoryStorage<T = unknown>(options?: MemoryStorageOptions): MemoryRetryCacheStorage<T>;
|
|
18
|
+
export declare function deletePrefix(storage: RetryCacheStorage, prefix: string): Promise<number>;
|
|
19
|
+
export declare function maybeAwait<T>(value: Awaitable<T>): Promise<T>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AxiosAdapter, AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
|
2
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
3
|
+
export type RetryCacheRequestKey = (context: RequestKeyContext) => Awaitable<string | undefined>;
|
|
4
|
+
export interface RequestKeyContext {
|
|
5
|
+
config: InternalAxiosRequestConfig;
|
|
6
|
+
}
|
|
7
|
+
export interface CacheOptions {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
ttl: number;
|
|
10
|
+
methods?: string[];
|
|
11
|
+
staleIfError?: boolean;
|
|
12
|
+
shouldCache?: (context: CacheDecisionContext) => Awaitable<boolean>;
|
|
13
|
+
}
|
|
14
|
+
export interface CacheDecisionContext<T = unknown, D = unknown> {
|
|
15
|
+
key: string;
|
|
16
|
+
config: InternalAxiosRequestConfig<D>;
|
|
17
|
+
response: AxiosResponse<T, D>;
|
|
18
|
+
}
|
|
19
|
+
export interface RetryDecisionContext<T = unknown, D = unknown> {
|
|
20
|
+
attempt: number;
|
|
21
|
+
retries: number;
|
|
22
|
+
config: InternalAxiosRequestConfig<D>;
|
|
23
|
+
response?: AxiosResponse<T, D>;
|
|
24
|
+
error?: AxiosError<T, D> | Error;
|
|
25
|
+
}
|
|
26
|
+
export type RetryDelayContext<T = unknown, D = unknown> = RetryDecisionContext<T, D>;
|
|
27
|
+
export interface RetryOptions {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
retries: number;
|
|
30
|
+
methods?: string[];
|
|
31
|
+
retryOnStatus?: number[];
|
|
32
|
+
retryOnNetworkError?: boolean;
|
|
33
|
+
respectRetryAfter?: boolean;
|
|
34
|
+
delay?: number | ((context: RetryDelayContext) => Awaitable<number>);
|
|
35
|
+
shouldRetry?: (context: RetryDecisionContext) => Awaitable<boolean>;
|
|
36
|
+
}
|
|
37
|
+
export interface RetryCacheRequestOptions {
|
|
38
|
+
cache?: boolean | Partial<CacheOptions>;
|
|
39
|
+
retry?: boolean | Partial<RetryOptions>;
|
|
40
|
+
dedupe?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface RetryCacheOptions {
|
|
43
|
+
requestKey?: RetryCacheRequestKey;
|
|
44
|
+
cache?: false | Partial<CacheOptions>;
|
|
45
|
+
retry?: false | Partial<RetryOptions>;
|
|
46
|
+
dedupe?: boolean;
|
|
47
|
+
storage?: RetryCacheStorage;
|
|
48
|
+
}
|
|
49
|
+
export interface CachedResponse<T = unknown> {
|
|
50
|
+
data: T;
|
|
51
|
+
status: number;
|
|
52
|
+
statusText: string;
|
|
53
|
+
headers: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
export interface CacheEntry<T = unknown> {
|
|
56
|
+
key: string;
|
|
57
|
+
createdAt: number;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
response: CachedResponse<T>;
|
|
60
|
+
}
|
|
61
|
+
export interface RetryCacheStorage<T = unknown> {
|
|
62
|
+
get(key: string): Awaitable<CacheEntry<T> | undefined>;
|
|
63
|
+
set(key: string, entry: CacheEntry<T>): Awaitable<void>;
|
|
64
|
+
delete(key: string): Awaitable<boolean>;
|
|
65
|
+
clear(): Awaitable<void>;
|
|
66
|
+
keys?(): Awaitable<Iterable<string>>;
|
|
67
|
+
deletePrefix?(prefix: string): Awaitable<number>;
|
|
68
|
+
}
|
|
69
|
+
export interface RetryCacheApi {
|
|
70
|
+
get(key: string): Promise<CacheEntry | undefined>;
|
|
71
|
+
set(key: string, response: AxiosResponse, options?: {
|
|
72
|
+
ttl?: number;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
invalidate(key: string): Promise<boolean>;
|
|
75
|
+
invalidatePrefix(prefix: string): Promise<number>;
|
|
76
|
+
clear(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export type AxiosRetryCacheInstance = AxiosInstance & {
|
|
79
|
+
retryCache: RetryCacheApi;
|
|
80
|
+
};
|
|
81
|
+
export type AdapterFactory = (adapter: AxiosAdapter) => AxiosAdapter;
|
|
82
|
+
declare module "axios" {
|
|
83
|
+
interface AxiosRequestConfig<D = any> {
|
|
84
|
+
retryCache?: boolean | RetryCacheRequestOptions;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mittwald/axios-cache-with-retry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"packageManager": "pnpm@10.29.2",
|
|
5
|
+
"author": "Mittwald CM Service GmbH & Co. KG <opensource@mittwald.de>",
|
|
6
|
+
"contributors": [
|
|
7
|
+
"Marco Falkenberg <m.falkenberg@mittwald.de>"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"description": "Coordinated retry, cache and in-flight dedupe for Axios, as a single adapter",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"adapter",
|
|
13
|
+
"axios",
|
|
14
|
+
"cache",
|
|
15
|
+
"dedupe",
|
|
16
|
+
"http",
|
|
17
|
+
"retry"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/mittwald/axios-cache-with-retry#readme",
|
|
20
|
+
"repository": "github:mittwald/axios-cache-with-retry",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/types/index.d.ts",
|
|
25
|
+
"import": "./dist/esm/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"types": "./dist/types/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "pnpm build:clean && pnpm tsc",
|
|
35
|
+
"build:clean": "rimraf dist",
|
|
36
|
+
"format": "prettier --write '**/*.{ts,yaml,yml,json,md}'",
|
|
37
|
+
"test": "pnpm test:tsc && pnpm test:lint && pnpm test:build && pnpm test:vitest",
|
|
38
|
+
"test:build": "pnpm build",
|
|
39
|
+
"test:lint": "eslint . --cache",
|
|
40
|
+
"test:tsc": "tsc --noEmit -p tsconfig.test.json",
|
|
41
|
+
"test:vitest": "vitest run"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^9.39.2",
|
|
45
|
+
"@types/node": "^24.10.12",
|
|
46
|
+
"axios": "^1.19.0",
|
|
47
|
+
"eslint": "^9.39.2",
|
|
48
|
+
"eslint-config-prettier": "^10.1.8",
|
|
49
|
+
"eslint-plugin-import": "^2.32.0",
|
|
50
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
51
|
+
"prettier": "^3.8.1",
|
|
52
|
+
"prettier-plugin-jsdoc": "^1.8.0",
|
|
53
|
+
"prettier-plugin-pkgsort": "^0.3.0",
|
|
54
|
+
"prettier-plugin-sort-json": "^4.2.0",
|
|
55
|
+
"rimraf": "^6.1.2",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"typescript-eslint": "^8.54.0",
|
|
58
|
+
"vitest": "^4.1.10"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"axios": "^1"
|
|
62
|
+
}
|
|
63
|
+
}
|