ani-client 1.4.2 → 1.4.4
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 +10 -547
- package/dist/index.d.mts +458 -448
- package/dist/index.d.ts +458 -448
- package/dist/index.js +76 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +76 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -6,48 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
> A simple, typed client to fetch anime, manga, character, staff and user data from [AniList](https://anilist.co).
|
|
8
8
|
|
|
9
|
+
✨ **Showcase**: [Check here](https://ani-client-docs.vercel.app/showcase) to see which projects use this package!
|
|
10
|
+
|
|
9
11
|
- **Zero dependencies** — uses the native `fetch` API
|
|
10
12
|
- **Universal** — Node.js ≥ 20, Bun, Deno and modern browsers
|
|
11
13
|
- **Dual format** — ships ESM + CJS with full TypeScript declarations
|
|
12
|
-
- **Built-in caching
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **Event hooks** — observe every request, cache hit, retry and response
|
|
16
|
-
- **Batch queries** — fetch multiple IDs in a single GraphQL call
|
|
14
|
+
- **Reliable** — Built-in caching, Rate-limit protections, automatic retries & request deduplication!
|
|
15
|
+
|
|
16
|
+
## 📖 Documentation
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
The full API reference, usage guide, and configuration examples are available on our official documentation website!
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
- [Quick start](#quick-start)
|
|
22
|
-
- [Client options](#client-options)
|
|
23
|
-
- [API reference](#api-reference)
|
|
24
|
-
- [Media](#media)
|
|
25
|
-
- [Include options](#include-options)
|
|
26
|
-
- [Characters](#characters)
|
|
27
|
-
- [Staff](#staff)
|
|
28
|
-
- [Users](#users)
|
|
29
|
-
- [Airing, Chapters & Planning](#airing-chapters--planning)
|
|
30
|
-
- [Season charts](#season-charts)
|
|
31
|
-
- [User media lists](#user-media-lists)
|
|
32
|
-
- [Recommendations](#recommendations)
|
|
33
|
-
- [Relations](#relations)
|
|
34
|
-
- [Studios](#studios)
|
|
35
|
-
- [Genres & Tags](#genres--tags)
|
|
36
|
-
- [Batch queries](#batch-queries)
|
|
37
|
-
- [Raw queries](#raw-queries)
|
|
38
|
-
- [Auto-pagination](#auto-pagination)
|
|
39
|
-
- [Caching](#caching)
|
|
40
|
-
- [Memory cache](#memory-cache)
|
|
41
|
-
- [Redis cache](#redis-cache)
|
|
42
|
-
- [Custom adapter](#custom-adapter)
|
|
43
|
-
- [Cache invalidation](#cache-invalidation)
|
|
44
|
-
- [Rate limiting](#rate-limiting)
|
|
45
|
-
- [Request deduplication](#request-deduplication)
|
|
46
|
-
- [Event hooks](#event-hooks)
|
|
47
|
-
- [Error handling](#error-handling)
|
|
48
|
-
- [Types](#types)
|
|
49
|
-
- [Contributing](#contributing)
|
|
50
|
-
- [License](#license)
|
|
20
|
+
**[👉 View the full documentation here](https://ani-client-docs.vercel.app/)**
|
|
51
21
|
|
|
52
22
|
## Install
|
|
53
23
|
|
|
@@ -60,6 +30,9 @@ pnpm add ani-client
|
|
|
60
30
|
|
|
61
31
|
# yarn
|
|
62
32
|
yarn add ani-client
|
|
33
|
+
|
|
34
|
+
# bun
|
|
35
|
+
bun add ani-client
|
|
63
36
|
```
|
|
64
37
|
|
|
65
38
|
## Quick start
|
|
@@ -80,516 +53,6 @@ const results = await client.searchMedia({
|
|
|
80
53
|
perPage: 5,
|
|
81
54
|
});
|
|
82
55
|
console.log(results.results.map((m) => m.title.english));
|
|
83
|
-
|
|
84
|
-
// Trending anime
|
|
85
|
-
const trending = await client.getTrending(MediaType.ANIME);
|
|
86
|
-
console.log(trending.results[0].title.romaji);
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
<details>
|
|
90
|
-
<summary>CommonJS</summary>
|
|
91
|
-
|
|
92
|
-
```js
|
|
93
|
-
const { AniListClient } = require("ani-client");
|
|
94
|
-
|
|
95
|
-
const client = new AniListClient();
|
|
96
|
-
client.getMedia(1).then((anime) => console.log(anime.title.romaji));
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
</details>
|
|
100
|
-
|
|
101
|
-
## Client options
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
const client = new AniListClient({
|
|
105
|
-
// AniList OAuth bearer token (optional)
|
|
106
|
-
token: "your-token",
|
|
107
|
-
|
|
108
|
-
// Custom API endpoint
|
|
109
|
-
apiUrl: "https://graphql.anilist.co",
|
|
110
|
-
|
|
111
|
-
// In-memory cache settings
|
|
112
|
-
cache: {
|
|
113
|
-
ttl: 86_400_000, // 24 hours (ms)
|
|
114
|
-
maxSize: 500, // max entries (LRU eviction)
|
|
115
|
-
enabled: true,
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
// Or bring your own adapter (takes precedence over `cache`)
|
|
119
|
-
cacheAdapter: new RedisCache({ client: redisClient }),
|
|
120
|
-
|
|
121
|
-
// Rate limiter
|
|
122
|
-
rateLimit: {
|
|
123
|
-
maxRequests: 85, // per window (AniList allows 90)
|
|
124
|
-
windowMs: 60_000, // 1 minute
|
|
125
|
-
maxRetries: 3, // retries on 429
|
|
126
|
-
retryDelayMs: 2_000, // delay between retries
|
|
127
|
-
timeoutMs: 30_000, // per-request timeout (0 = none)
|
|
128
|
-
retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
|
|
129
|
-
enabled: true,
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
// Event hooks
|
|
133
|
-
hooks: {
|
|
134
|
-
onRequest: (query, variables) => {},
|
|
135
|
-
onResponse: (query, durationMs, fromCache) => {},
|
|
136
|
-
onCacheHit: (key) => {},
|
|
137
|
-
onRateLimit: (retryAfterMs) => {},
|
|
138
|
-
onRetry: (attempt, reason, delayMs) => {},
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## API reference
|
|
144
|
-
|
|
145
|
-
### Media
|
|
146
|
-
|
|
147
|
-
| Method | Description |
|
|
148
|
-
| --- | --- |
|
|
149
|
-
| `getMedia(id, include?)` | Fetch a single anime / manga by ID with optional extra data |
|
|
150
|
-
| `searchMedia(options?)` | Search & filter anime / manga |
|
|
151
|
-
| `getTrending(type?, page?, perPage?)` | Currently trending entries |
|
|
152
|
-
| `getMediaBySeason(options)` | Anime/manga for a given season & year |
|
|
153
|
-
| `getRecommendations(mediaId, options?)` | User recommendations for a media |
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
// Simple — same as before
|
|
157
|
-
const anime = await client.getMedia(1);
|
|
158
|
-
|
|
159
|
-
// With extra data
|
|
160
|
-
const anime = await client.getMedia(1, {
|
|
161
|
-
characters: { perPage: 25, sort: true },
|
|
162
|
-
staff: true,
|
|
163
|
-
relations: true,
|
|
164
|
-
streamingEpisodes: true,
|
|
165
|
-
externalLinks: true,
|
|
166
|
-
stats: true,
|
|
167
|
-
recommendations: { perPage: 5 },
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const results = await client.searchMedia({
|
|
171
|
-
query: "Naruto",
|
|
172
|
-
type: MediaType.ANIME,
|
|
173
|
-
format: MediaFormat.TV,
|
|
174
|
-
genre: "Action",
|
|
175
|
-
perPage: 10,
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Include options
|
|
180
|
-
|
|
181
|
-
The second parameter of `getMedia()` lets you opt-in to additional data. By default, only `relations` are included for backward compatibility.
|
|
182
|
-
|
|
183
|
-
| Option | Type | Default | Description |
|
|
184
|
-
| --- | --- | --- | --- |
|
|
185
|
-
| `characters` | `boolean \| { perPage?, sort?, voiceActors? }` | — | Characters with their roles (MAIN, SUPPORTING, BACKGROUND). Set `voiceActors: true` to include VA data. |
|
|
186
|
-
| `staff` | `boolean \| { perPage?, sort? }` | — | Staff members with their roles |
|
|
187
|
-
| `relations` | `boolean` | `true` | Sequels, prequels, adaptations, etc. Set `false` to exclude |
|
|
188
|
-
| `streamingEpisodes` | `boolean` | — | Streaming links (Crunchyroll, Funimation, etc.) |
|
|
189
|
-
| `externalLinks` | `boolean` | — | External links (MAL, official site, etc.) |
|
|
190
|
-
| `stats` | `boolean` | — | Score & status distribution |
|
|
191
|
-
| `recommendations` | `boolean \| { perPage? }` | — | User recommendations |
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
// Include characters sorted by role (25 per page by default)
|
|
195
|
-
const anime = await client.getMedia(1, { characters: true });
|
|
196
|
-
anime.characters?.edges.forEach((e) =>
|
|
197
|
-
console.log(`${e.node.name.full} (${e.role})`)
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
// 50 characters, no sorting
|
|
201
|
-
const anime = await client.getMedia(1, {
|
|
202
|
-
characters: { perPage: 50, sort: false },
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Include voice actors alongside characters
|
|
206
|
-
const anime = await client.getMedia(1, {
|
|
207
|
-
characters: { voiceActors: true },
|
|
208
|
-
});
|
|
209
|
-
anime.characters?.edges.forEach((e) => {
|
|
210
|
-
console.log(e.node.name.full);
|
|
211
|
-
e.voiceActors?.forEach((va) =>
|
|
212
|
-
console.log(` VA: ${va.name.full} (${va.languageV2})`)
|
|
213
|
-
);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Staff members
|
|
217
|
-
const anime = await client.getMedia(1, { staff: true });
|
|
218
|
-
anime.staff?.edges.forEach((e) =>
|
|
219
|
-
console.log(`${e.node.name.full} — ${e.role}`)
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// Everything at once
|
|
223
|
-
const anime = await client.getMedia(1, {
|
|
224
|
-
characters: { perPage: 50 },
|
|
225
|
-
staff: { perPage: 25 },
|
|
226
|
-
relations: true,
|
|
227
|
-
streamingEpisodes: true,
|
|
228
|
-
externalLinks: true,
|
|
229
|
-
stats: true,
|
|
230
|
-
recommendations: { perPage: 10 },
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// Lightweight — exclude relations
|
|
234
|
-
const anime = await client.getMedia(1, {
|
|
235
|
-
characters: true,
|
|
236
|
-
relations: false,
|
|
237
|
-
});
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Characters
|
|
241
|
-
|
|
242
|
-
| Method | Description |
|
|
243
|
-
| --- | --- |
|
|
244
|
-
| `getCharacter(id, include?)` | Fetch a character by ID, optionally with voice actors |
|
|
245
|
-
| `searchCharacters(options?)` | Search characters by name, optionally with voice actors |
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
const spike = await client.getCharacter(1);
|
|
249
|
-
const results = await client.searchCharacters({ query: "Luffy", perPage: 5 });
|
|
250
|
-
|
|
251
|
-
// With voice actors
|
|
252
|
-
const spike = await client.getCharacter(1, { voiceActors: true });
|
|
253
|
-
spike.media?.edges?.forEach((e) => {
|
|
254
|
-
console.log(e.node.title.romaji);
|
|
255
|
-
e.voiceActors?.forEach((va) =>
|
|
256
|
-
console.log(` VA: ${va.name.full} (${va.languageV2})`)
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Search with voice actors
|
|
261
|
-
const result = await client.searchCharacters({ query: "Luffy", voiceActors: true });
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Staff
|
|
265
|
-
|
|
266
|
-
| Method | Description |
|
|
267
|
-
| --- | --- |
|
|
268
|
-
| `getStaff(id, include?)` | Fetch a staff member by ID (optionally with media) |
|
|
269
|
-
| `searchStaff(options?)` | Search for staff members |
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
const staff = await client.getStaff(95001);
|
|
273
|
-
const results = await client.searchStaff({ query: "Miyazaki" });
|
|
274
|
-
|
|
275
|
-
// With media the staff member worked on
|
|
276
|
-
const staffWithMedia = await client.getStaff(95001, { media: true });
|
|
277
|
-
staffWithMedia.staffMedia?.nodes.forEach((m) => {
|
|
278
|
-
console.log(m.title.romaji, m.format, m.averageScore);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Customize the number of media returned
|
|
282
|
-
const staffWith5Media = await client.getStaff(95001, { media: { perPage: 5 } });
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Users
|
|
286
|
-
|
|
287
|
-
| Method | Description |
|
|
288
|
-
| --- | --- |
|
|
289
|
-
| `getUser(id)` | Fetch a user by ID |
|
|
290
|
-
| `getUserByName(name)` | Fetch a user by username |
|
|
291
|
-
| `getUserMediaList(options)` | Get a user's anime or manga list |
|
|
292
|
-
|
|
293
|
-
### Airing, Chapters & Planning
|
|
294
|
-
|
|
295
|
-
| Method | Description |
|
|
296
|
-
| --- | --- |
|
|
297
|
-
| `getAiredEpisodes(options?)` | Recently aired anime episodes (last 24 h by default) |
|
|
298
|
-
| `getAiredChapters(options?)` | Recently updated releasing manga |
|
|
299
|
-
| `getPlanning(options?)` | Upcoming not-yet-released anime / manga |
|
|
300
|
-
|
|
301
|
-
```ts
|
|
302
|
-
// Episodes aired in the last 7 days
|
|
303
|
-
const week = await client.getAiredEpisodes({
|
|
304
|
-
airingAtGreater: Math.floor(Date.now() / 1000) - 7 * 24 * 3600,
|
|
305
|
-
perPage: 50,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// Most anticipated upcoming anime
|
|
309
|
-
const upcoming = await client.getPlanning({
|
|
310
|
-
type: MediaType.ANIME,
|
|
311
|
-
perPage: 10,
|
|
312
|
-
});
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Season charts
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
import { MediaSeason } from "ani-client";
|
|
319
|
-
|
|
320
|
-
const winter2026 = await client.getMediaBySeason({
|
|
321
|
-
season: MediaSeason.WINTER,
|
|
322
|
-
seasonYear: 2026,
|
|
323
|
-
perPage: 25,
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
| Option | Type | Default | Description |
|
|
328
|
-
| --- | --- | --- | --- |
|
|
329
|
-
| `season` | `MediaSeason` | **(required)** | WINTER, SPRING, SUMMER, or FALL |
|
|
330
|
-
| `seasonYear` | `number` | **(required)** | The year |
|
|
331
|
-
| `type` | `MediaType` | `ANIME` | Filter by ANIME or MANGA |
|
|
332
|
-
| `sort` | `MediaSort[]` | `["POPULARITY_DESC"]` | Sort order |
|
|
333
|
-
| `page` | `number` | `1` | Page number |
|
|
334
|
-
| `perPage` | `number` | `20` | Results per page (max 50) |
|
|
335
|
-
|
|
336
|
-
### User media lists
|
|
337
|
-
|
|
338
|
-
```ts
|
|
339
|
-
import { MediaType, MediaListStatus } from "ani-client";
|
|
340
|
-
|
|
341
|
-
const list = await client.getUserMediaList({
|
|
342
|
-
userName: "AniList",
|
|
343
|
-
type: MediaType.ANIME,
|
|
344
|
-
status: MediaListStatus.COMPLETED,
|
|
345
|
-
});
|
|
346
|
-
list.results.forEach((entry) =>
|
|
347
|
-
console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
|
|
348
|
-
);
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
Provide either `userId` or `userName`. The `type` field is required.
|
|
352
|
-
|
|
353
|
-
### Recommendations
|
|
354
|
-
|
|
355
|
-
```ts
|
|
356
|
-
const recs = await client.getRecommendations(1); // Cowboy Bebop
|
|
357
|
-
recs.results.forEach((r) =>
|
|
358
|
-
console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
|
|
359
|
-
);
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### Relations
|
|
363
|
-
|
|
364
|
-
Relations (sequels, prequels, spin-offs, etc.) are included by default when using `getMedia()`. You can also explicitly request them via the `include` parameter, or exclude them with `relations: false`.
|
|
365
|
-
|
|
366
|
-
```ts
|
|
367
|
-
const anime = await client.getMedia(1);
|
|
368
|
-
anime.relations?.edges.forEach((edge) =>
|
|
369
|
-
console.log(`${edge.relationType}: ${edge.node.title.romaji}`)
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
// Exclude relations for a lighter response
|
|
373
|
-
const anime = await client.getMedia(1, { relations: false });
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
Available types: `ADAPTATION`, `PREQUEL`, `SEQUEL`, `PARENT`, `SIDE_STORY`, `CHARACTER`, `SUMMARY`, `ALTERNATIVE`, `SPIN_OFF`, `OTHER`, `SOURCE`, `COMPILATION`, `CONTAINS`.
|
|
377
|
-
|
|
378
|
-
### Studios
|
|
379
|
-
|
|
380
|
-
| Method | Description |
|
|
381
|
-
| --- | --- |
|
|
382
|
-
| `getStudio(id)` | Fetch a studio with its productions |
|
|
383
|
-
| `searchStudios(options?)` | Search studios by name |
|
|
384
|
-
|
|
385
|
-
```ts
|
|
386
|
-
const studio = await client.getStudio(44); // Bones
|
|
387
|
-
const results = await client.searchStudios({ query: "MAPPA" });
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Genres & Tags
|
|
391
|
-
|
|
392
|
-
```ts
|
|
393
|
-
const genres = await client.getGenres();
|
|
394
|
-
// ["Action", "Adventure", "Comedy", ...]
|
|
395
|
-
|
|
396
|
-
const tags = await client.getTags();
|
|
397
|
-
// [{ id, name, description, category, isAdult }, ...]
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Batch queries
|
|
401
|
-
|
|
402
|
-
Fetch multiple IDs in a single GraphQL request (up to 50 per call, auto-chunked).
|
|
403
|
-
|
|
404
|
-
| Method | Description |
|
|
405
|
-
| --- | --- |
|
|
406
|
-
| `getMediaBatch(ids)` | Fetch multiple anime / manga |
|
|
407
|
-
| `getCharacterBatch(ids)` | Fetch multiple characters |
|
|
408
|
-
| `getStaffBatch(ids)` | Fetch multiple staff members |
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
const [bebop, naruto, aot] = await client.getMediaBatch([1, 20, 16498]);
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Raw queries
|
|
415
|
-
|
|
416
|
-
Execute any GraphQL query against the AniList API.
|
|
417
|
-
|
|
418
|
-
```ts
|
|
419
|
-
const data = await client.raw<{ Media: { id: number; title: { romaji: string } } }>(
|
|
420
|
-
"query { Media(id: 1) { id title { romaji } } }",
|
|
421
|
-
);
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
### Auto-pagination
|
|
425
|
-
|
|
426
|
-
`paginate()` returns an async iterator that fetches pages on demand.
|
|
427
|
-
|
|
428
|
-
```ts
|
|
429
|
-
for await (const anime of client.paginate(
|
|
430
|
-
(page) => client.searchMedia({ query: "Gundam", page, perPage: 10 }),
|
|
431
|
-
3, // max 3 pages
|
|
432
|
-
)) {
|
|
433
|
-
console.log(anime.title.romaji);
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
## Caching
|
|
438
|
-
|
|
439
|
-
### Memory cache
|
|
440
|
-
|
|
441
|
-
The default in-memory cache uses **LRU eviction** (24 h TTL, 500 entries max).
|
|
442
|
-
|
|
443
|
-
```ts
|
|
444
|
-
const client = new AniListClient({
|
|
445
|
-
cache: {
|
|
446
|
-
ttl: 1000 * 60 * 60, // 1 hour
|
|
447
|
-
maxSize: 200,
|
|
448
|
-
enabled: true, // false to disable
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Redis cache
|
|
454
|
-
|
|
455
|
-
For distributed or persistent caching, use the built-in Redis adapter. Compatible with [ioredis](https://github.com/redis/ioredis) and [node-redis](https://github.com/redis/node-redis) v4+.
|
|
456
|
-
|
|
457
|
-
```ts
|
|
458
|
-
import Redis from "ioredis";
|
|
459
|
-
import { AniListClient, RedisCache } from "ani-client";
|
|
460
|
-
|
|
461
|
-
const client = new AniListClient({
|
|
462
|
-
cacheAdapter: new RedisCache({
|
|
463
|
-
client: new Redis(),
|
|
464
|
-
prefix: "ani:", // key prefix (default)
|
|
465
|
-
ttl: 86_400, // seconds (default: 24 h)
|
|
466
|
-
}),
|
|
467
|
-
});
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
### Custom adapter
|
|
471
|
-
|
|
472
|
-
Implement the `CacheAdapter` interface to bring your own storage:
|
|
473
|
-
|
|
474
|
-
```ts
|
|
475
|
-
import type { CacheAdapter } from "ani-client";
|
|
476
|
-
|
|
477
|
-
class MyCache implements CacheAdapter {
|
|
478
|
-
get<T>(key: string): T | undefined | Promise<T | undefined> { /* ... */ }
|
|
479
|
-
set<T>(key: string, data: T): void | Promise<void> { /* ... */ }
|
|
480
|
-
delete(key: string): boolean | Promise<boolean> { /* ... */ }
|
|
481
|
-
clear(): void | Promise<void> { /* ... */ }
|
|
482
|
-
get size(): number { return -1; } // return -1 if unknown
|
|
483
|
-
keys(): string[] | Promise<string[]> { /* ... */ }
|
|
484
|
-
// Optional — the client provides a fallback if omitted
|
|
485
|
-
invalidate?(pattern: string | RegExp): number | Promise<number> { /* ... */ }
|
|
486
|
-
}
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### Cache invalidation
|
|
490
|
-
|
|
491
|
-
```ts
|
|
492
|
-
// Clear everything
|
|
493
|
-
await client.clearCache();
|
|
494
|
-
|
|
495
|
-
// Remove entries matching a pattern
|
|
496
|
-
const removed = await client.invalidateCache(/Media/);
|
|
497
|
-
console.log(`Removed ${removed} entries`);
|
|
498
|
-
|
|
499
|
-
// Current cache size
|
|
500
|
-
console.log(client.cacheSize);
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
## Rate limiting
|
|
504
|
-
|
|
505
|
-
The client respects AniList's rate limit (90 req/min) with a conservative default of **85 req/min**. On HTTP 429, it retries automatically with progressive backoff.
|
|
506
|
-
|
|
507
|
-
```ts
|
|
508
|
-
const client = new AniListClient({
|
|
509
|
-
rateLimit: {
|
|
510
|
-
maxRequests: 60,
|
|
511
|
-
windowMs: 60_000,
|
|
512
|
-
maxRetries: 5,
|
|
513
|
-
retryDelayMs: 3_000,
|
|
514
|
-
timeoutMs: 30_000, // abort after 30 s
|
|
515
|
-
retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
|
|
516
|
-
enabled: true,
|
|
517
|
-
},
|
|
518
|
-
});
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## Request deduplication
|
|
522
|
-
|
|
523
|
-
When multiple callers request the same data at the same time, only one API call is made. All callers receive the same response.
|
|
524
|
-
|
|
525
|
-
```ts
|
|
526
|
-
// Only 1 HTTP request is sent
|
|
527
|
-
const [a, b] = await Promise.all([
|
|
528
|
-
client.getMedia(1),
|
|
529
|
-
client.getMedia(1),
|
|
530
|
-
]);
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
## Event hooks
|
|
534
|
-
|
|
535
|
-
Monitor every request lifecycle event for logging, metrics, or debugging.
|
|
536
|
-
|
|
537
|
-
```ts
|
|
538
|
-
const client = new AniListClient({
|
|
539
|
-
hooks: {
|
|
540
|
-
onRequest: (query, variables) => console.log("→", query.slice(0, 40)),
|
|
541
|
-
onResponse: (query, durationMs, fromCache) => console.log(`← ${durationMs}ms (cache: ${fromCache})`),
|
|
542
|
-
onCacheHit: (key) => console.log("Cache hit:", key.slice(0, 30)),
|
|
543
|
-
onRateLimit: (retryAfterMs) => console.warn(`Rate limited, waiting ${retryAfterMs}ms`),
|
|
544
|
-
onRetry: (attempt, reason, delayMs) => console.warn(`Retry #${attempt}: ${reason}`),
|
|
545
|
-
},
|
|
546
|
-
});
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
## Error handling
|
|
550
|
-
|
|
551
|
-
All API errors throw an `AniListError` with `message`, `status` and `errors`:
|
|
552
|
-
|
|
553
|
-
```ts
|
|
554
|
-
import { AniListError } from "ani-client";
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
await client.getMedia(999999999);
|
|
558
|
-
} catch (err) {
|
|
559
|
-
if (err instanceof AniListError) {
|
|
560
|
-
console.error(err.message); // "Not Found."
|
|
561
|
-
console.error(err.status); // 404
|
|
562
|
-
console.error(err.errors); // raw API error array
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
## Types
|
|
568
|
-
|
|
569
|
-
All types and enums are exported:
|
|
570
|
-
|
|
571
|
-
```ts
|
|
572
|
-
import type {
|
|
573
|
-
Media, Character, Staff, User, VoiceActor,
|
|
574
|
-
AiringSchedule, MediaListEntry, Recommendation, StudioDetail,
|
|
575
|
-
MediaEdge, MediaConnection, MediaCharacterEdge, MediaCharacterConnection,
|
|
576
|
-
CharacterMediaEdge, CharacterIncludeOptions,
|
|
577
|
-
MediaStaffEdge, MediaStaffConnection, MediaIncludeOptions,
|
|
578
|
-
StaffMediaNode, StaffIncludeOptions,
|
|
579
|
-
StreamingEpisode, ExternalLink, MediaStats, MediaRecommendationNode,
|
|
580
|
-
PageInfo, PagedResult,
|
|
581
|
-
CacheAdapter, AniListHooks, AniListClientOptions,
|
|
582
|
-
SearchMediaOptions, SearchCharacterOptions, SearchStaffOptions,
|
|
583
|
-
SearchStudioOptions, GetAiringOptions, GetRecentChaptersOptions,
|
|
584
|
-
GetPlanningOptions, GetSeasonOptions, GetUserMediaListOptions,
|
|
585
|
-
GetRecommendationsOptions,
|
|
586
|
-
} from "ani-client";
|
|
587
|
-
|
|
588
|
-
import {
|
|
589
|
-
MediaType, MediaFormat, MediaStatus, MediaSeason, MediaSort,
|
|
590
|
-
CharacterSort, CharacterRole, AiringSort, RecommendationSort,
|
|
591
|
-
MediaRelationType, MediaListStatus, MediaListSort,
|
|
592
|
-
} from "ani-client";
|
|
593
56
|
```
|
|
594
57
|
|
|
595
58
|
## Contributing
|