anilink-api-wrapper 2.0.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.
- package/README.md +32 -484
- package/dist/AniLink.d.ts +1656 -724
- package/dist/AniLink.mjs +1005 -371
- package/dist/mal.d.ts +1 -0
- package/dist/mal.mjs +5 -0
- package/package.json +28 -18
package/README.md
CHANGED
|
@@ -1,522 +1,70 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs-src/public/logo.png" alt="AniLink" width="256" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">AniLink</h1>
|
|
2
6
|
|
|
3
7
|
[](https://www.npmjs.com/package/anilink-api-wrapper)
|
|
4
8
|
[](https://www.npmjs.com/package/anilink-api-wrapper)
|
|
5
9
|
[](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE)
|
|
6
10
|
[](https://github.com/RLAlpha49/AniLink/actions/workflows/ci.yml)
|
|
7
11
|
[](https://github.com/RLAlpha49/AniLink/actions/workflows/codeql.yml)
|
|
8
|
-
[. AniLink turns raw AniList GraphQL into a set of named methods. You can query a user, save a list entry, or toggle a favourite. You do not need to write query strings or hand-roll HTTP.
|
|
11
|
-
|
|
12
|
-
> 🧪 Try the [AniLink API Explorer](https://rlalpha49.github.io/AniLink/explorer/) — build and test AniLink calls live against AniList.
|
|
13
|
-
|
|
14
|
-
## Why It Exists
|
|
15
|
-
|
|
16
|
-
AniList exposes a GraphQL API that is flexible but verbose. Every request needs a query document, a variables object, and careful handling of the response shape. AniLink removes that ceremony. You call a method, pass a plain object, and get back typed data. AniLink also validates your variables before they leave your app. A wrong field type fails fast with a clear message instead of a confusing API error.
|
|
17
|
-
|
|
18
|
-
## What You Can Do With It
|
|
19
|
-
|
|
20
|
-
AniLink uses one instance with a single `anilist` surface:
|
|
21
|
-
|
|
22
|
-
- `anilist.query` fetches data. This includes users, media, characters, staff, studios, reviews, activities, threads, notifications, and more.
|
|
23
|
-
|
|
24
|
-
- `anilist.query.page` returns paginated versions of the same resources.
|
|
25
|
-
|
|
26
|
-
- `anilist.paginate`, `anilist.paginatePages`, and `anilist.paginateChunks` walk every page or chunk for you, so you do not hand-roll `hasNextPage` loops.
|
|
12
|
+
[](https://anilink.alpha49.com/)
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- `anilist.custom` sends any raw query or mutation when you need something the named methods do not cover.
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
import { AniLink } from "anilink-api-wrapper";
|
|
34
|
-
|
|
35
|
-
// Remember that you can create multiple instances with different auth tokens
|
|
36
|
-
// or one instance without a token for public queries.
|
|
37
|
-
const aniLink = new AniLink();
|
|
38
|
-
const aniLinkAuth = new AniLink("your-auth-token");
|
|
39
|
-
|
|
40
|
-
// Fetch a user
|
|
41
|
-
const user = await aniLink.anilist.query.user({ id: 542244 });
|
|
42
|
-
|
|
43
|
-
// Save an anime to your list
|
|
44
|
-
await aniLinkAuth.anilist.mutation.saveMediaListEntry({
|
|
45
|
-
mediaId: 1,
|
|
46
|
-
status: "COMPLETED",
|
|
47
|
-
score: 9,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Send a raw query
|
|
51
|
-
const viewer = await aniLinkAuth.anilist.custom("query { Viewer { id } }");
|
|
52
|
-
```
|
|
14
|
+
A typed TypeScript wrapper for the [AniList GraphQL API](https://docs.anilist.co/) and the [MyAnimeList REST API](https://myanimelist.net/apiconfig/references/api/v2). One class, two isolated provider surfaces, normalized errors, retries, and a generated operation reference.
|
|
53
15
|
|
|
54
|
-
##
|
|
55
|
-
|
|
56
|
-
- **Typed end to end.** Every method has a typed variables interface and a typed response. Your editor catches mistakes before runtime.
|
|
57
|
-
|
|
58
|
-
- **No GraphQL strings for common tasks.** The named query and mutation methods cover the everyday AniList operations.
|
|
59
|
-
|
|
60
|
-
- **Variable validation.** AniLink checks required fields and types before sending. It throws a descriptive error like `Invalid id: 542244. Expected type: number` when something is wrong.
|
|
61
|
-
|
|
62
|
-
- **Optional auth.** Construct with a token for authenticated calls, or without one for public queries. You can create multiple instances with different tokens.
|
|
63
|
-
|
|
64
|
-
- **Custom escape hatch.** `anilist.custom` accepts any query or mutation string with an optional variables object.
|
|
65
|
-
|
|
66
|
-
- **Pagination built in.** Page queries accept `page` and `perPage`, and the `paginate` / `paginatePages` / `paginateChunks` helpers walk every page or chunk with a max-page guard.
|
|
67
|
-
|
|
68
|
-
- **Clear error handling.** API errors, including rate limits, surface as thrown errors you can catch and retry.
|
|
69
|
-
|
|
70
|
-
## Who It Is For
|
|
71
|
-
|
|
72
|
-
AniLink is for developers building tools around AniList. This includes trackers, recommendation engines, Discord bots, dashboards, or anything that reads or writes AniList data. If you would rather call `query.media({ id: 1 })` than maintain GraphQL documents, this is for you.
|
|
73
|
-
|
|
74
|
-
## Getting Started
|
|
75
|
-
|
|
76
|
-
### Install
|
|
16
|
+
## Quickstart
|
|
77
17
|
|
|
78
18
|
```bash
|
|
79
19
|
npm install anilink-api-wrapper
|
|
80
20
|
```
|
|
81
21
|
|
|
82
|
-
### Initialize
|
|
83
|
-
|
|
84
22
|
```typescript
|
|
85
23
|
import { AniLink } from "anilink-api-wrapper";
|
|
86
24
|
|
|
87
|
-
//
|
|
88
|
-
const aniLinkAuth = new AniLink("your-auth-token");
|
|
89
|
-
|
|
90
|
-
// Without a token (public queries only)
|
|
25
|
+
// AniList (GraphQL) — public queries need no token
|
|
91
26
|
const aniLink = new AniLink();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Get a token by registering an application on the [AniList developer settings](https://anilist.co/settings/developer).
|
|
27
|
+
const anime = await aniLink.anilist.query.media({ id: 21, type: "ANIME" });
|
|
95
28
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
| Option | Type | Default | Description |
|
|
101
|
-
| --------------------- | ------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
-
| `timeout` | `number` | `30000` | Milliseconds before a request is aborted. `0` disables the timeout. Timeout errors carry the effective duration as `timeoutMs`. |
|
|
103
|
-
| `signal` | `AbortSignal` | — | Cancel in-flight requests (for example when a user navigates away). |
|
|
104
|
-
| `retry` | `boolean \| Partial<RetryPolicy>` | default policy | Automatic retries for transient failures are **on by default** (`maxRetries: 3` over HTTP `429`/`5xx`, network, and timeout errors). Pass `retry: false` to opt out. See [Retry with backoff](#retry-with-backoff). |
|
|
105
|
-
| `paceWithRateLimit` | `boolean` | `false` | Opt into proactive pacing: when the `x-ratelimit-remaining` header drops below `rateLimitFloor`, the next request waits until the window resets instead of discovering the limit via a `429`. |
|
|
106
|
-
| `rateLimitFloor` | `number` | `1` | Remaining-quota threshold below which `paceWithRateLimit` delays the next request. |
|
|
107
|
-
| `circuitBreaker` | `{ threshold: number; cooldownMs: number }` | — | Opt into a per-client circuit breaker: after `threshold` consecutive failures, requests fail fast with `CIRCUIT_OPEN_ERROR` until `cooldownMs` elapses. |
|
|
108
|
-
| `onError` | `(error, context) => void` | — | Invoked when an attempt fails and once more when retries are exhausted. |
|
|
109
|
-
| `onRetry` | `(error, context) => void` | — | Invoked before each retry wait with the scheduled delay. |
|
|
110
|
-
| `onRequestStart` | `(context) => void` | — | Invoked just before each attempt is sent. |
|
|
111
|
-
| `onResponse` | `(context) => void` | — | Invoked after each attempt completes, with the elapsed `durationMs`. |
|
|
112
|
-
| `exposeRawAxiosError` | `boolean` | `false` | Attach the original Axios error to thrown errors for local debugging. |
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
const aniLink = new AniLink("your-auth-token", {
|
|
116
|
-
timeout: 10_000,
|
|
117
|
-
retry: { maxRetries: 2 },
|
|
118
|
-
onResponse: ({ url, durationMs }) => console.log(url, `${durationMs}ms`),
|
|
119
|
-
});
|
|
29
|
+
// MyAnimeList (REST) — isolated credential slot
|
|
30
|
+
const client = new AniLink({ mal: { accessToken: "mal-token" } });
|
|
31
|
+
const malAnime = await client.mal.anime.get(21, { fields: ["id", "title", "main_picture"] });
|
|
120
32
|
```
|
|
121
33
|
|
|
122
|
-
|
|
34
|
+
## What you can do
|
|
123
35
|
|
|
124
|
-
|
|
36
|
+
| Provider | Namespace | Capabilities |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| **AniList** | `aniLink.anilist` | Queries, page queries, mutations, pagination helpers, `custom()`, data helpers |
|
|
39
|
+
| **MyAnimeList** | `aniLink.mal` | `anime.get` and `user.me` REST reads with field selection |
|
|
125
40
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
First, register an application on the [AniList developer settings](https://anilist.co/settings/developer) to get a client ID and client secret, and pick a redirect URI.
|
|
129
|
-
|
|
130
|
-
Send the user to the authorization URL to approve your application:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import { buildAuthorizationUrl } from "anilink-api-wrapper";
|
|
134
|
-
|
|
135
|
-
const state = crypto.randomUUID(); // a fresh random value per login attempt
|
|
136
|
-
const authorizeUrl = buildAuthorizationUrl("your-client-id", "https://example.com/callback", state);
|
|
137
|
-
// Redirect the user to `authorizeUrl`. After approval, AniList sends them back
|
|
138
|
-
// to your redirect URI with `?code=` and `state=` query parameters.
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The third `state` parameter is **optional** but strongly recommended as additional CSRF protection. Always pass a random `state`, bind it to the user's session, and validate that the `state` on the redirect matches before exchanging the code. This cross-site request forgery (CSRF) check stops attackers from completing authorization flows your users never started.
|
|
142
|
-
|
|
143
|
-
Exchange the authorization code from the redirect for an access token:
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
import { getAccessToken, AniLink } from "anilink-api-wrapper";
|
|
147
|
-
|
|
148
|
-
const { access_token, refresh_token } = await getAccessToken(
|
|
149
|
-
"your-client-id",
|
|
150
|
-
"your-client-secret",
|
|
151
|
-
code, // the `code` query parameter from the redirect
|
|
152
|
-
"https://example.com/callback"
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const aniLink = new AniLink(access_token);
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
When the access token expires, exchange the stored refresh token for a new one. Note that the refresh response may not include a new `refresh_token`, in which case you keep using the one you stored:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
import { refreshAccessToken } from "anilink-api-wrapper";
|
|
162
|
-
|
|
163
|
-
const { access_token, refresh_token: rotated } = await refreshAccessToken(
|
|
164
|
-
"your-client-id",
|
|
165
|
-
"your-client-secret",
|
|
166
|
-
refresh_token
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const nextRefreshToken = rotated ?? refresh_token;
|
|
170
|
-
const aniLink = new AniLink(access_token);
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
AniList reports the token lifetime as `expires_in` seconds. Use `getTokenExpiry` to refresh proactively before the token expires instead of waiting for a `401`:
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
import { getTokenExpiry, refreshAccessToken } from "anilink-api-wrapper";
|
|
177
|
-
|
|
178
|
-
if (Date.now() >= getTokenExpiry(tokenResponse).getTime() - 60_000) {
|
|
179
|
-
// Refresh at least a minute before expiry.
|
|
180
|
-
tokenResponse = await refreshAccessToken(
|
|
181
|
-
"your-client-id",
|
|
182
|
-
"your-client-secret",
|
|
183
|
-
nextRefreshToken
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### Query
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
const user = await aniLink.anilist.query.user({ id: 542244, asHtml: true });
|
|
192
|
-
const media = await aniLink.anilist.query.media({ id: 1, type: "ANIME" });
|
|
193
|
-
const viewer = await aniLink.anilist.query.viewer({ asHtml: true });
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Paginate
|
|
197
|
-
|
|
198
|
-
Page queries accept `page` and `perPage` and return a single page with `pageInfo`. Fetch one page when you know the range:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
const page = await aniLink.anilist.query.page.medias({
|
|
202
|
-
page: 1,
|
|
203
|
-
perPage: 10,
|
|
204
|
-
type: "ANIME",
|
|
205
|
-
sort: ["POPULARITY_DESC"],
|
|
206
|
-
});
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
To walk every page, use the helpers on `aniLink.anilist`. They track `page`/`perPage` and `hasNextPage` for you and stop at a `maxPages` guard, so a runaway loop cannot fetch forever.
|
|
210
|
-
|
|
211
|
-
`paginate` collects every item across all pages into one array:
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
const result = await aniLink.anilist.paginate(
|
|
215
|
-
(page, perPage) => aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" }),
|
|
216
|
-
"media",
|
|
217
|
-
{ perPage: 50, maxPages: 10 }
|
|
218
|
-
);
|
|
219
|
-
console.log(result.items.length, result.pageCount, result.truncated);
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
`paginatePages` yields each raw page response in turn. Use it for streaming or early exit when you do not need every item in memory:
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
for await (const page of aniLink.anilist.paginatePages((page, perPage) =>
|
|
226
|
-
aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" })
|
|
227
|
-
)) {
|
|
228
|
-
console.log(page.pageInfo.currentPage, page.media.length);
|
|
229
|
-
if (page.media.length > 0 && page.media[0].id === 1) break;
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
`paginateChunks` walks `MediaListCollection` chunks, which AniList returns with `hasNextChunk` instead of `pageInfo`:
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
const result = await aniLink.anilist.paginateChunks(
|
|
237
|
-
(chunk, perChunk) =>
|
|
238
|
-
aniLink.anilist.query.mediaListCollection({
|
|
239
|
-
userId: 542244,
|
|
240
|
-
type: "ANIME",
|
|
241
|
-
chunk,
|
|
242
|
-
perChunk,
|
|
243
|
-
}),
|
|
244
|
-
"lists",
|
|
245
|
-
{ perChunk: 500, maxChunks: 20 }
|
|
246
|
-
);
|
|
247
|
-
console.log(result.items.length, result.chunkCount, result.truncated);
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Each helper returns `truncated: true` when it stopped at the guard before the source ran out of pages or chunks.
|
|
251
|
-
|
|
252
|
-
By default pages and chunks are fetched strictly one at a time. Pass `concurrency` to keep several requests in flight while collecting results — useful for large traversals where per-page latency dominates. Results are always returned in order regardless of which request finishes first, scheduling stops as soon as the source reports no more data, and the `maxPages` / `maxChunks` guards still apply:
|
|
253
|
-
|
|
254
|
-
```typescript
|
|
255
|
-
const result = await aniLink.anilist.paginate(
|
|
256
|
-
(page, perPage) => aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" }),
|
|
257
|
-
"media",
|
|
258
|
-
{ perPage: 50, maxPages: 10, concurrency: 4 }
|
|
259
|
-
);
|
|
260
|
-
console.log(result.items.length, result.pageCount, result.truncated);
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Keep `concurrency` modest (2–8): AniList rate-limits aggressive clients, and values above 8 are clamped down to 8.
|
|
264
|
-
|
|
265
|
-
### Mutate
|
|
266
|
-
|
|
267
|
-
```typescript
|
|
268
|
-
await aniLink.anilist.mutation.saveMediaListEntry({
|
|
269
|
-
mediaId: 1,
|
|
270
|
-
status: "COMPLETED",
|
|
271
|
-
score: 9,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
await aniLink.anilist.mutation.toggleFavourite({ animeId: 1 });
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Handle errors
|
|
278
|
-
|
|
279
|
-
AniLink throws typed errors with stable `code` values. HTTP failures are
|
|
280
|
-
represented by `AniLinkApiError` and expose the HTTP `status`; network,
|
|
281
|
-
timeout, and cancellation failures use `AniLinkNetworkError`. Calling an
|
|
282
|
-
authenticated operation without a token throws `AniLinkAuthError`. Successful
|
|
283
|
-
AniList response data is returned normally as the typed result of each
|
|
284
|
-
query or mutation. For failed API requests, `AniLinkApiError.data` contains
|
|
285
|
-
the upstream AniList response body. AniLink does not expose the raw Axios
|
|
286
|
-
response object, request headers, bearer token, or request internals.
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
import {
|
|
290
|
-
AniLinkApiError,
|
|
291
|
-
AniLinkAuthError,
|
|
292
|
-
AniLinkGraphQLError,
|
|
293
|
-
AniLinkNetworkError,
|
|
294
|
-
} from "anilink-api-wrapper";
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const user = await aniLink.anilist.query.user({ id: 542244 });
|
|
298
|
-
console.log(user);
|
|
299
|
-
} catch (error: unknown) {
|
|
300
|
-
if (error instanceof AniLinkApiError) {
|
|
301
|
-
console.error(error.code, error.status, error.data);
|
|
302
|
-
|
|
303
|
-
if (error.status === 429) {
|
|
304
|
-
// Rate-limit accounting from the response headers, when present.
|
|
305
|
-
console.error("Quota reset at:", error.rateLimit?.reset);
|
|
306
|
-
}
|
|
307
|
-
} else if (error instanceof AniLinkGraphQLError) {
|
|
308
|
-
// The request returned HTTP 200 but carried GraphQL errors.
|
|
309
|
-
console.error(error.graphqlErrors.map((e) => e.message));
|
|
310
|
-
console.error(error.data); // any partial data returned alongside the errors
|
|
311
|
-
} else if (error instanceof AniLinkAuthError) {
|
|
312
|
-
console.error(error.code, error.message);
|
|
313
|
-
} else if (error instanceof AniLinkNetworkError) {
|
|
314
|
-
console.error(error.code, error.message);
|
|
315
|
-
} else {
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
The available transport codes are `API_ERROR`, `GRAPHQL_ERROR`, `NETWORK_ERROR`,
|
|
322
|
-
`TIMEOUT_ERROR`, `ABORTED_ERROR`, `CIRCUIT_OPEN_ERROR`, `AUTH_ERROR`,
|
|
323
|
-
`VALIDATION_ERROR`, and `UNKNOWN_ERROR`.
|
|
324
|
-
|
|
325
|
-
GraphQL-level failures can arrive inside an HTTP 200 response. AniLink throws
|
|
326
|
-
these as `AniLinkGraphQLError` (a subclass of `AniLinkApiError` with `status`
|
|
327
|
-
`200`) instead of returning half-valid data. The upstream `errors` array is
|
|
328
|
-
available on `error.graphqlErrors`, and any partial `data` on `error.data`.
|
|
329
|
-
|
|
330
|
-
When AniList includes rate-limit headers (`x-ratelimit-limit`,
|
|
331
|
-
`x-ratelimit-remaining`, `x-ratelimit-reset`), every `AniLinkApiError` exposes
|
|
332
|
-
them as a read-only `rateLimit` object so schedulers and UIs can self-throttle.
|
|
333
|
-
|
|
334
|
-
For local debugging, you can opt into the original Axios error:
|
|
335
|
-
|
|
336
|
-
```typescript
|
|
337
|
-
const debugClient = new AniLink("your-auth-token", {
|
|
338
|
-
exposeRawAxiosError: true,
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
await debugClient.anilist.query.user({ id: 542244 });
|
|
343
|
-
} catch (error: unknown) {
|
|
344
|
-
if (error instanceof AniLinkApiError) {
|
|
345
|
-
console.error(error.rawAxiosError);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
`exposeRawAxiosError` defaults to `false`. Use it for local debugging;
|
|
351
|
-
the raw Axios error can contain request configuration and bearer-token
|
|
352
|
-
headers.
|
|
353
|
-
|
|
354
|
-
### Retry with backoff
|
|
355
|
-
|
|
356
|
-
AniLink retries transient failures for you by default: HTTP `429` and `5xx`
|
|
357
|
-
responses plus network and timeout errors are retried with exponential
|
|
358
|
-
backoff (`maxRetries: 3`). The `Retry-After` header is honored for `429`
|
|
359
|
-
responses (capped at 60 seconds). Pass `retry: false` to opt out and send
|
|
360
|
-
every request exactly once, or pass a partial policy to tune individual
|
|
361
|
-
knobs on top of the defaults.
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
// Opt out of the default policy
|
|
365
|
-
const aniLink = new AniLink("your-auth-token", { retry: false });
|
|
366
|
-
|
|
367
|
-
// Or tune the default policy
|
|
368
|
-
const aniLink = new AniLink("your-auth-token", {
|
|
369
|
-
retry: {
|
|
370
|
-
maxRetries: 3, // retries after the initial attempt
|
|
371
|
-
baseDelayMs: 250, // first backoff delay
|
|
372
|
-
maxDelayMs: 5_000, // backoff cap
|
|
373
|
-
retryOnStatus: [429, 500, 502, 503, 504],
|
|
374
|
-
retryOnNetworkError: true,
|
|
375
|
-
jitter: true, // randomize each wait within [0, computed delay]
|
|
376
|
-
},
|
|
377
|
-
});
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Backoff delays use **full jitter** by default: each wait is a random value
|
|
381
|
-
between `0` and the computed exponential cap. This spreads out retries from
|
|
382
|
-
many concurrent clients instead of letting them re-fire in lockstep against
|
|
383
|
-
the shared rate limit. Server-dictated `Retry-After` waits are never jittered.
|
|
384
|
-
Pass `jitter: false` for deterministic delays.
|
|
385
|
-
|
|
386
|
-
### Rate-limit pacing and circuit breaking
|
|
387
|
-
|
|
388
|
-
Two further resilience knobs are available per instance, both off by default:
|
|
389
|
-
|
|
390
|
-
```typescript
|
|
391
|
-
import { AniLinkNetworkError } from "anilink-api-wrapper";
|
|
392
|
-
|
|
393
|
-
const aniLink = new AniLink("your-auth-token", {
|
|
394
|
-
// Slow down before AniList does: once a successful response reports fewer
|
|
395
|
-
// remaining requests than `rateLimitFloor`, hold the next request until
|
|
396
|
-
// the window resets instead of eating a 429.
|
|
397
|
-
paceWithRateLimit: true,
|
|
398
|
-
rateLimitFloor: 5,
|
|
399
|
-
// Fail fast during sustained outages: after 5 consecutive failures,
|
|
400
|
-
// requests throw `CIRCUIT_OPEN_ERROR` without touching the network for
|
|
401
|
-
// 30 seconds, then one probe request is let through again.
|
|
402
|
-
circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
await aniLink.anilist.query.user({ id: 542244 });
|
|
407
|
-
} catch (error: unknown) {
|
|
408
|
-
if (error instanceof AniLinkNetworkError && error.code === "CIRCUIT_OPEN_ERROR") {
|
|
409
|
-
// Back off; the breaker will probe recovery automatically.
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Error hook
|
|
415
|
-
|
|
416
|
-
The `onError` hook is invoked when a request ultimately fails after all
|
|
417
|
-
retries are exhausted (or immediately when retries are disabled). Use it
|
|
418
|
-
to implement your own fallback (for example a cache or an offline queue).
|
|
419
|
-
|
|
420
|
-
```typescript
|
|
421
|
-
import { AniLink, AniLinkApiError } from "anilink-api-wrapper";
|
|
422
|
-
|
|
423
|
-
const aniLink = new AniLink("your-auth-token", {
|
|
424
|
-
onError: (error, context) => {
|
|
425
|
-
console.error(`Request to ${context.url} failed on attempt ${context.attempt}`);
|
|
426
|
-
if (error instanceof AniLinkApiError && error.status === 429) {
|
|
427
|
-
// queue the request for later
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
### Observability hooks
|
|
434
|
-
|
|
435
|
-
Beyond `onError`, three optional hooks let you instrument the request
|
|
436
|
-
lifecycle without wrapping any methods. All are per-instance options:
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
const aniLink = new AniLink("your-auth-token", {
|
|
440
|
-
// Fires just before each attempt is sent.
|
|
441
|
-
onRequestStart: ({ url, method, attempt }) => {
|
|
442
|
-
console.log(`#${attempt} -> ${method} ${url}`);
|
|
443
|
-
},
|
|
444
|
-
// Fires after each attempt completes, success or failure.
|
|
445
|
-
onResponse: ({ url, durationMs }) => {
|
|
446
|
-
console.log(`${url} took ${durationMs}ms`);
|
|
447
|
-
},
|
|
448
|
-
// Fires before each retry wait, with the scheduled delay.
|
|
449
|
-
onRetry: (error, { attempt, nextDelayMs, status }) => {
|
|
450
|
-
console.warn(
|
|
451
|
-
`attempt ${attempt} failed (${status ?? error.code}); retrying in ${nextDelayMs}ms`
|
|
452
|
-
);
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
- `onRequestStart` receives `{ url, method, attempt }`.
|
|
458
|
-
- `onResponse` receives the same context plus `durationMs`, and fires for
|
|
459
|
-
failed attempts too, so it can drive latency histograms.
|
|
460
|
-
- `onRetry` receives the normalized error plus `{ url, method, attempt, code,
|
|
461
|
-
status?, nextDelayMs }`. When you do not set `onRetry`, per-attempt
|
|
462
|
-
notifications fall back to `onError` (which then fires before each retry
|
|
463
|
-
wait and once more at terminal failure).
|
|
464
|
-
|
|
465
|
-
These hooks are synchronous and fire in addition to the promise result; they
|
|
466
|
-
never change request behavior. Hook exceptions are isolated: a throwing hook
|
|
467
|
-
is reported as a console warning and never crashes a request or distorts
|
|
468
|
-
retry and error classification.
|
|
469
|
-
|
|
470
|
-
Common status codes from the AniList API:
|
|
471
|
-
|
|
472
|
-
- `400` bad request
|
|
473
|
-
- `401` unauthorized
|
|
474
|
-
- `404` not found
|
|
475
|
-
- `429` too many requests (rate limit)
|
|
476
|
-
- `500` internal server error
|
|
41
|
+
Both surfaces share one transport layer (timeouts, retries, pacing, circuit breaker, hooks) while keeping credentials and transport settings isolated per provider slot.
|
|
477
42
|
|
|
478
43
|
## Documentation
|
|
479
44
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
AniList's GraphQL API is unversioned: fields can appear, be deprecated, or disappear at any time. AniLink pins a schema snapshot and gates every pull request against it with `npm run anilist:api:compare -- --strict --ignore-unimplemented`, so drift is caught before merge rather than at your runtime. The comparison report classifies upstream changes as:
|
|
489
|
-
|
|
490
|
-
- **Discrepancy** — an implemented operation's contract no longer matches the snapshot (missing field, changed type). Strict mode fails CI; these ship as a patch or minor fix-forward.
|
|
491
|
-
- **Removed operation** — an operation AniLink wrapped no longer exists upstream. The wrapper method is dropped and the removal ships as a **minor** release with a documented migration snippet using `custom()`.
|
|
492
|
-
- **Deprecated operation** — AniList marked a field or operation deprecated. AniLink keeps wrapping it, tracks the deprecation in the report, and removes it only when AniList does.
|
|
493
|
-
- **Unimplemented operation** — an upstream operation AniLink has never wrapped. These are listed in the report; operations that can never be wrapped are recorded with a dated review note in `IGNORED_UNIMPLEMENTED_OPERATIONS` (`lib/api-compare/compare.ts`).
|
|
494
|
-
|
|
495
|
-
Until a removal ships, `custom()` lets you call any upstream operation directly:
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
const result = await aniLink.anilist.custom(
|
|
499
|
-
"query ($id: Int) { Media (id: $id) { id title { romaji } } }",
|
|
500
|
-
{ id: 1 }
|
|
501
|
-
);
|
|
502
|
-
```
|
|
45
|
+
| Surface | Start here |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| **Guides** | [Introduction](https://anilink.alpha49.com/introduction) · [Getting started](https://anilink.alpha49.com/getting-started) · [Provider configuration](https://anilink.alpha49.com/provider-configuration) · [Per-request options](https://anilink.alpha49.com/per-request-options) · [Error handling](https://anilink.alpha49.com/error-handling) · [Retries & resilience](https://anilink.alpha49.com/retries-and-resilience) · [Cancellation & timeouts](https://anilink.alpha49.com/cancellation-and-timeouts) · [Observability](https://anilink.alpha49.com/observability) · [Recipes](https://anilink.alpha49.com/recipes) · [TypeScript patterns](https://anilink.alpha49.com/typescript-patterns) · [Troubleshooting](https://anilink.alpha49.com/troubleshooting) |
|
|
48
|
+
| **AniList guides** | [Authentication](https://anilink.alpha49.com/guides/anilist/authentication) · [Client configuration](https://anilink.alpha49.com/guides/anilist/configuration) · [Querying](https://anilink.alpha49.com/guides/anilist/querying) · [Page queries](https://anilink.alpha49.com/guides/anilist/page-queries) · [Pagination](https://anilink.alpha49.com/guides/anilist/pagination) · [Mutations](https://anilink.alpha49.com/guides/anilist/mutations) · [Custom queries](https://anilink.alpha49.com/guides/anilist/custom-queries) · [Helpers](https://anilink.alpha49.com/guides/anilist/helpers) |
|
|
49
|
+
| **MAL guides** | [Authentication](https://anilink.alpha49.com/guides/mal/authentication) · [Client configuration](https://anilink.alpha49.com/guides/mal/configuration) · [Operations](https://anilink.alpha49.com/guides/mal/operations) |
|
|
50
|
+
| **Operation reference** | [Overview](https://anilink.alpha49.com/operations/) · [AniList catalog](https://anilink.alpha49.com/operations/anilist) · [MAL catalog](https://anilink.alpha49.com/operations/mal) |
|
|
51
|
+
| **API reference (TypeDoc)** | [AniLink](https://anilink.alpha49.com/classes/AniLink.AniLink.html) — full generated reference at the [docs root](https://anilink.alpha49.com/) |
|
|
503
52
|
|
|
504
53
|
## Development
|
|
505
54
|
|
|
506
55
|
```bash
|
|
507
56
|
npm install # install dependencies
|
|
508
|
-
npm run
|
|
509
|
-
npm
|
|
510
|
-
npm run
|
|
511
|
-
npm run lint # lint source and tests
|
|
512
|
-
npm run build # build to dist/
|
|
513
|
-
npm run docs:generate # generate API docs to docs/
|
|
57
|
+
npm run check # typecheck, lint, tests, format, JSDoc, api-compare, build
|
|
58
|
+
npm run docs:generate # TypeDoc + operation reference + guides site into docs/
|
|
59
|
+
npm run docs:dev # serve the guides site locally
|
|
514
60
|
```
|
|
515
61
|
|
|
62
|
+
See the [contributing guide](CONTRIBUTING.md) for workflow details.
|
|
63
|
+
|
|
516
64
|
## Resources
|
|
517
65
|
|
|
518
|
-
- [Contributing guide](CONTRIBUTING.md)
|
|
519
66
|
- [Changelog](CHANGELOG.md) and [GitHub Releases](https://github.com/RLAlpha49/AniLink/releases)
|
|
67
|
+
- [Contributing guide](CONTRIBUTING.md)
|
|
520
68
|
|
|
521
69
|
## License
|
|
522
70
|
|