ani-client 1.1.0 → 1.3.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 +363 -127
- package/dist/index.d.mts +440 -7
- package/dist/index.d.ts +440 -7
- package/dist/index.js +703 -81
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +699 -82
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -15
package/README.md
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
# ani-client
|
|
2
2
|
|
|
3
|
+
[](https://github.com/gonzyui/ani-client/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/ani-client)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
> A simple, typed client to fetch anime, manga, character, staff and user data from [AniList](https://anilist.co).
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
- **Zero dependencies** — uses the native `fetch` API
|
|
10
|
+
- **Universal** — Node.js ≥ 18, Bun, Deno and modern browsers
|
|
11
|
+
- **Dual format** — ships ESM + CJS with full TypeScript declarations
|
|
12
|
+
- **Built-in caching** — in-memory LRU with optional Redis adapter
|
|
13
|
+
- **Rate-limit aware** — auto-retry on 429, configurable timeout & network error retry
|
|
14
|
+
- **Request deduplication** — identical in-flight requests are coalesced
|
|
15
|
+
- **Event hooks** — observe every request, cache hit, retry and response
|
|
16
|
+
- **Batch queries** — fetch multiple IDs in a single GraphQL call
|
|
17
|
+
|
|
18
|
+
## Table of contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [Client options](#client-options)
|
|
23
|
+
- [API reference](#api-reference)
|
|
24
|
+
- [Media](#media)
|
|
25
|
+
- [Characters](#characters)
|
|
26
|
+
- [Staff](#staff)
|
|
27
|
+
- [Users](#users)
|
|
28
|
+
- [Airing, Chapters & Planning](#airing-chapters--planning)
|
|
29
|
+
- [Season charts](#season-charts)
|
|
30
|
+
- [User media lists](#user-media-lists)
|
|
31
|
+
- [Recommendations](#recommendations)
|
|
32
|
+
- [Relations](#relations)
|
|
33
|
+
- [Studios](#studios)
|
|
34
|
+
- [Genres & Tags](#genres--tags)
|
|
35
|
+
- [Batch queries](#batch-queries)
|
|
36
|
+
- [Raw queries](#raw-queries)
|
|
37
|
+
- [Auto-pagination](#auto-pagination)
|
|
38
|
+
- [Caching](#caching)
|
|
39
|
+
- [Memory cache](#memory-cache)
|
|
40
|
+
- [Redis cache](#redis-cache)
|
|
41
|
+
- [Custom adapter](#custom-adapter)
|
|
42
|
+
- [Cache invalidation](#cache-invalidation)
|
|
43
|
+
- [Rate limiting](#rate-limiting)
|
|
44
|
+
- [Request deduplication](#request-deduplication)
|
|
45
|
+
- [Event hooks](#event-hooks)
|
|
46
|
+
- [Error handling](#error-handling)
|
|
47
|
+
- [Types](#types)
|
|
48
|
+
- [Contributing](#contributing)
|
|
49
|
+
- [License](#license)
|
|
7
50
|
|
|
8
51
|
## Install
|
|
9
52
|
|
|
@@ -42,7 +85,8 @@ const trending = await client.getTrending(MediaType.ANIME);
|
|
|
42
85
|
console.log(trending.results[0].title.romaji);
|
|
43
86
|
```
|
|
44
87
|
|
|
45
|
-
|
|
88
|
+
<details>
|
|
89
|
+
<summary>CommonJS</summary>
|
|
46
90
|
|
|
47
91
|
```js
|
|
48
92
|
const { AniListClient } = require("ani-client");
|
|
@@ -51,212 +95,404 @@ const client = new AniListClient();
|
|
|
51
95
|
client.getMedia(1).then((anime) => console.log(anime.title.romaji));
|
|
52
96
|
```
|
|
53
97
|
|
|
54
|
-
|
|
98
|
+
</details>
|
|
55
99
|
|
|
56
|
-
|
|
100
|
+
## Client options
|
|
57
101
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
```ts
|
|
103
|
+
const client = new AniListClient({
|
|
104
|
+
// AniList OAuth bearer token (optional)
|
|
105
|
+
token: "your-token",
|
|
106
|
+
|
|
107
|
+
// Custom API endpoint
|
|
108
|
+
apiUrl: "https://graphql.anilist.co",
|
|
109
|
+
|
|
110
|
+
// In-memory cache settings
|
|
111
|
+
cache: {
|
|
112
|
+
ttl: 86_400_000, // 24 hours (ms)
|
|
113
|
+
maxSize: 500, // max entries (LRU eviction)
|
|
114
|
+
enabled: true,
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// Or bring your own adapter (takes precedence over `cache`)
|
|
118
|
+
cacheAdapter: new RedisCache({ client: redisClient }),
|
|
119
|
+
|
|
120
|
+
// Rate limiter
|
|
121
|
+
rateLimit: {
|
|
122
|
+
maxRequests: 85, // per window (AniList allows 90)
|
|
123
|
+
windowMs: 60_000, // 1 minute
|
|
124
|
+
maxRetries: 3, // retries on 429
|
|
125
|
+
retryDelayMs: 2_000, // delay between retries
|
|
126
|
+
timeoutMs: 30_000, // per-request timeout (0 = none)
|
|
127
|
+
retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
|
|
128
|
+
enabled: true,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Event hooks
|
|
132
|
+
hooks: {
|
|
133
|
+
onRequest: (query, variables) => {},
|
|
134
|
+
onResponse: (query, durationMs, fromCache) => {},
|
|
135
|
+
onCacheHit: (key) => {},
|
|
136
|
+
onRateLimit: (retryAfterMs) => {},
|
|
137
|
+
onRetry: (attempt, reason, delayMs) => {},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## API reference
|
|
64
143
|
|
|
65
144
|
### Media
|
|
66
145
|
|
|
67
|
-
| Method
|
|
68
|
-
|
|
|
69
|
-
| `getMedia(id
|
|
70
|
-
| `searchMedia(options?)`
|
|
71
|
-
| `getTrending(type?, page?, perPage?)` |
|
|
146
|
+
| Method | Description |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `getMedia(id)` | Fetch a single anime / manga by ID |
|
|
149
|
+
| `searchMedia(options?)` | Search & filter anime / manga |
|
|
150
|
+
| `getTrending(type?, page?, perPage?)` | Currently trending entries |
|
|
151
|
+
| `getMediaBySeason(options)` | Anime/manga for a given season & year |
|
|
152
|
+
| `getRecommendations(mediaId, options?)` | User recommendations for a media |
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const anime = await client.getMedia(1);
|
|
156
|
+
|
|
157
|
+
const results = await client.searchMedia({
|
|
158
|
+
query: "Naruto",
|
|
159
|
+
type: MediaType.ANIME,
|
|
160
|
+
format: MediaFormat.TV,
|
|
161
|
+
genre: "Action",
|
|
162
|
+
perPage: 10,
|
|
163
|
+
});
|
|
164
|
+
```
|
|
72
165
|
|
|
73
166
|
### Characters
|
|
74
167
|
|
|
75
|
-
| Method
|
|
76
|
-
|
|
|
77
|
-
| `getCharacter(id
|
|
78
|
-
| `searchCharacters(options?)`
|
|
168
|
+
| Method | Description |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `getCharacter(id)` | Fetch a character by ID |
|
|
171
|
+
| `searchCharacters(options?)` | Search characters by name |
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const spike = await client.getCharacter(1);
|
|
175
|
+
const results = await client.searchCharacters({ query: "Luffy", perPage: 5 });
|
|
176
|
+
```
|
|
79
177
|
|
|
80
178
|
### Staff
|
|
81
179
|
|
|
82
|
-
| Method
|
|
83
|
-
|
|
|
84
|
-
| `getStaff(id
|
|
85
|
-
| `searchStaff(options?)`
|
|
180
|
+
| Method | Description |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `getStaff(id)` | Fetch a staff member by ID |
|
|
183
|
+
| `searchStaff(options?)` | Search for staff members |
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const staff = await client.getStaff(95001);
|
|
187
|
+
const results = await client.searchStaff({ query: "Miyazaki" });
|
|
188
|
+
```
|
|
86
189
|
|
|
87
190
|
### Users
|
|
88
191
|
|
|
89
|
-
| Method
|
|
90
|
-
|
|
|
91
|
-
| `getUser(id
|
|
92
|
-
| `getUserByName(name
|
|
192
|
+
| Method | Description |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `getUser(id)` | Fetch a user by ID |
|
|
195
|
+
| `getUserByName(name)` | Fetch a user by username |
|
|
196
|
+
| `getUserMediaList(options)` | Get a user's anime or manga list |
|
|
93
197
|
|
|
94
198
|
### Airing, Chapters & Planning
|
|
95
199
|
|
|
96
|
-
| Method
|
|
97
|
-
|
|
|
98
|
-
| `getAiredEpisodes(options?)`
|
|
99
|
-
| `getAiredChapters(options?)`
|
|
100
|
-
| `getPlanning(options?)`
|
|
200
|
+
| Method | Description |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `getAiredEpisodes(options?)` | Recently aired anime episodes (last 24 h by default) |
|
|
203
|
+
| `getAiredChapters(options?)` | Recently updated releasing manga |
|
|
204
|
+
| `getPlanning(options?)` | Upcoming not-yet-released anime / manga |
|
|
101
205
|
|
|
102
|
-
|
|
206
|
+
```ts
|
|
207
|
+
// Episodes aired in the last 7 days
|
|
208
|
+
const week = await client.getAiredEpisodes({
|
|
209
|
+
airingAtGreater: Math.floor(Date.now() / 1000) - 7 * 24 * 3600,
|
|
210
|
+
perPage: 50,
|
|
211
|
+
});
|
|
103
212
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
213
|
+
// Most anticipated upcoming anime
|
|
214
|
+
const upcoming = await client.getPlanning({
|
|
215
|
+
type: MediaType.ANIME,
|
|
216
|
+
perPage: 10,
|
|
217
|
+
});
|
|
218
|
+
```
|
|
107
219
|
|
|
108
|
-
###
|
|
220
|
+
### Season charts
|
|
109
221
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| `clearCache()` | Clear the entire response cache |
|
|
113
|
-
| `cacheSize` | Number of entries currently cached |
|
|
222
|
+
```ts
|
|
223
|
+
import { MediaSeason } from "ani-client";
|
|
114
224
|
|
|
115
|
-
|
|
225
|
+
const winter2026 = await client.getMediaBySeason({
|
|
226
|
+
season: MediaSeason.WINTER,
|
|
227
|
+
seasonYear: 2026,
|
|
228
|
+
perPage: 25,
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| Option | Type | Default | Description |
|
|
233
|
+
| --- | --- | --- | --- |
|
|
234
|
+
| `season` | `MediaSeason` | **(required)** | WINTER, SPRING, SUMMER, or FALL |
|
|
235
|
+
| `seasonYear` | `number` | **(required)** | The year |
|
|
236
|
+
| `type` | `MediaType` | `ANIME` | Filter by ANIME or MANGA |
|
|
237
|
+
| `sort` | `MediaSort[]` | `["POPULARITY_DESC"]` | Sort order |
|
|
238
|
+
| `page` | `number` | `1` | Page number |
|
|
239
|
+
| `perPage` | `number` | `20` | Results per page (max 50) |
|
|
116
240
|
|
|
117
|
-
|
|
241
|
+
### User media lists
|
|
118
242
|
|
|
119
243
|
```ts
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
244
|
+
import { MediaType, MediaListStatus } from "ani-client";
|
|
245
|
+
|
|
246
|
+
const list = await client.getUserMediaList({
|
|
247
|
+
userName: "AniList",
|
|
248
|
+
type: MediaType.ANIME,
|
|
249
|
+
status: MediaListStatus.COMPLETED,
|
|
126
250
|
});
|
|
251
|
+
list.results.forEach((entry) =>
|
|
252
|
+
console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
|
|
253
|
+
);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Provide either `userId` or `userName`. The `type` field is required.
|
|
127
257
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
258
|
+
### Recommendations
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const recs = await client.getRecommendations(1); // Cowboy Bebop
|
|
262
|
+
recs.results.forEach((r) =>
|
|
263
|
+
console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
|
|
264
|
+
);
|
|
131
265
|
```
|
|
132
266
|
|
|
133
|
-
|
|
267
|
+
### Relations
|
|
134
268
|
|
|
135
|
-
|
|
269
|
+
Every media object includes a `relations` field with sequels, prequels, spin-offs, etc.
|
|
136
270
|
|
|
137
271
|
```ts
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
maxRetries: 5, // retry up to 5 times on 429
|
|
143
|
-
retryDelayMs: 3000, // wait 3s between retries
|
|
144
|
-
enabled: true, // set to false to disable
|
|
145
|
-
},
|
|
146
|
-
});
|
|
272
|
+
const anime = await client.getMedia(1);
|
|
273
|
+
anime.relations?.edges.forEach((edge) =>
|
|
274
|
+
console.log(`${edge.relationType}: ${edge.node.title.romaji}`)
|
|
275
|
+
);
|
|
147
276
|
```
|
|
148
277
|
|
|
149
|
-
|
|
278
|
+
Available types: `ADAPTATION`, `PREQUEL`, `SEQUEL`, `PARENT`, `SIDE_STORY`, `CHARACTER`, `SUMMARY`, `ALTERNATIVE`, `SPIN_OFF`, `OTHER`, `SOURCE`, `COMPILATION`, `CONTAINS`.
|
|
150
279
|
|
|
151
|
-
|
|
280
|
+
### Studios
|
|
152
281
|
|
|
153
|
-
|
|
282
|
+
| Method | Description |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `getStudio(id)` | Fetch a studio with its productions |
|
|
285
|
+
| `searchStudios(options?)` | Search studios by name |
|
|
154
286
|
|
|
155
|
-
|
|
287
|
+
```ts
|
|
288
|
+
const studio = await client.getStudio(44); // Bones
|
|
289
|
+
const results = await client.searchStudios({ query: "MAPPA" });
|
|
290
|
+
```
|
|
156
291
|
|
|
157
|
-
|
|
158
|
-
| ----------------- | -------------- | -------------------- | -------------------------------------------- |
|
|
159
|
-
| `airingAtGreater` | `number` | `now - 24h` (UNIX) | Only episodes aired after this timestamp |
|
|
160
|
-
| `airingAtLesser` | `number` | `now` (UNIX) | Only episodes aired before this timestamp |
|
|
161
|
-
| `sort` | `AiringSort[]` | `["TIME_DESC"]` | Sort order |
|
|
162
|
-
| `page` | `number` | `1` | Page number |
|
|
163
|
-
| `perPage` | `number` | `20` | Results per page (max 50) |
|
|
292
|
+
### Genres & Tags
|
|
164
293
|
|
|
165
294
|
```ts
|
|
166
|
-
|
|
295
|
+
const genres = await client.getGenres();
|
|
296
|
+
// ["Action", "Adventure", "Comedy", ...]
|
|
167
297
|
|
|
168
|
-
const
|
|
298
|
+
const tags = await client.getTags();
|
|
299
|
+
// [{ id, name, description, category, isAdult }, ...]
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Batch queries
|
|
303
|
+
|
|
304
|
+
Fetch multiple IDs in a single GraphQL request (up to 50 per call, auto-chunked).
|
|
305
|
+
|
|
306
|
+
| Method | Description |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| `getMediaBatch(ids)` | Fetch multiple anime / manga |
|
|
309
|
+
| `getCharacterBatch(ids)` | Fetch multiple characters |
|
|
310
|
+
| `getStaffBatch(ids)` | Fetch multiple staff members |
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const [bebop, naruto, aot] = await client.getMediaBatch([1, 20, 16498]);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Raw queries
|
|
169
317
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
318
|
+
Execute any GraphQL query against the AniList API.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
const data = await client.raw<{ Media: { id: number; title: { romaji: string } } }>(
|
|
322
|
+
"query { Media(id: 1) { id title { romaji } } }",
|
|
174
323
|
);
|
|
324
|
+
```
|
|
175
325
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
326
|
+
### Auto-pagination
|
|
327
|
+
|
|
328
|
+
`paginate()` returns an async iterator that fetches pages on demand.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
for await (const anime of client.paginate(
|
|
332
|
+
(page) => client.searchMedia({ query: "Gundam", page, perPage: 10 }),
|
|
333
|
+
3, // max 3 pages
|
|
334
|
+
)) {
|
|
335
|
+
console.log(anime.title.romaji);
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Caching
|
|
340
|
+
|
|
341
|
+
### Memory cache
|
|
342
|
+
|
|
343
|
+
The default in-memory cache uses **LRU eviction** (24 h TTL, 500 entries max).
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
const client = new AniListClient({
|
|
347
|
+
cache: {
|
|
348
|
+
ttl: 1000 * 60 * 60, // 1 hour
|
|
349
|
+
maxSize: 200,
|
|
350
|
+
enabled: true, // false to disable
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Redis cache
|
|
356
|
+
|
|
357
|
+
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+.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import Redis from "ioredis";
|
|
361
|
+
import { AniListClient, RedisCache } from "ani-client";
|
|
362
|
+
|
|
363
|
+
const client = new AniListClient({
|
|
364
|
+
cacheAdapter: new RedisCache({
|
|
365
|
+
client: new Redis(),
|
|
366
|
+
prefix: "ani:", // key prefix (default)
|
|
367
|
+
ttl: 86_400, // seconds (default: 24 h)
|
|
368
|
+
}),
|
|
180
369
|
});
|
|
181
370
|
```
|
|
182
371
|
|
|
183
|
-
###
|
|
372
|
+
### Custom adapter
|
|
184
373
|
|
|
185
|
-
|
|
374
|
+
Implement the `CacheAdapter` interface to bring your own storage:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import type { CacheAdapter } from "ani-client";
|
|
378
|
+
|
|
379
|
+
class MyCache implements CacheAdapter {
|
|
380
|
+
get<T>(key: string): T | undefined | Promise<T | undefined> { /* ... */ }
|
|
381
|
+
set<T>(key: string, data: T): void | Promise<void> { /* ... */ }
|
|
382
|
+
delete(key: string): boolean | Promise<boolean> { /* ... */ }
|
|
383
|
+
clear(): void | Promise<void> { /* ... */ }
|
|
384
|
+
get size(): number { return -1; } // return -1 if unknown
|
|
385
|
+
keys(): string[] | Promise<string[]> { /* ... */ }
|
|
386
|
+
// Optional — the client provides a fallback if omitted
|
|
387
|
+
invalidate?(pattern: string | RegExp): number | Promise<number> { /* ... */ }
|
|
388
|
+
}
|
|
389
|
+
```
|
|
186
390
|
|
|
187
|
-
|
|
188
|
-
| --------- | -------- | ------- | ----------------------- |
|
|
189
|
-
| `page` | `number` | `1` | Page number |
|
|
190
|
-
| `perPage` | `number` | `20` | Results per page (max 50) |
|
|
391
|
+
### Cache invalidation
|
|
191
392
|
|
|
192
393
|
```ts
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
394
|
+
// Clear everything
|
|
395
|
+
await client.clearCache();
|
|
396
|
+
|
|
397
|
+
// Remove entries matching a pattern
|
|
398
|
+
const removed = await client.invalidateCache(/Media/);
|
|
399
|
+
console.log(`Removed ${removed} entries`);
|
|
400
|
+
|
|
401
|
+
// Current cache size
|
|
402
|
+
console.log(client.cacheSize);
|
|
197
403
|
```
|
|
198
404
|
|
|
199
|
-
|
|
405
|
+
## Rate limiting
|
|
200
406
|
|
|
201
|
-
|
|
407
|
+
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.
|
|
202
408
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
409
|
+
```ts
|
|
410
|
+
const client = new AniListClient({
|
|
411
|
+
rateLimit: {
|
|
412
|
+
maxRequests: 60,
|
|
413
|
+
windowMs: 60_000,
|
|
414
|
+
maxRetries: 5,
|
|
415
|
+
retryDelayMs: 3_000,
|
|
416
|
+
timeoutMs: 30_000, // abort after 30 s
|
|
417
|
+
retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
|
|
418
|
+
enabled: true,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Request deduplication
|
|
424
|
+
|
|
425
|
+
When multiple callers request the same data at the same time, only one API call is made. All callers receive the same response.
|
|
209
426
|
|
|
210
427
|
```ts
|
|
211
|
-
|
|
428
|
+
// Only 1 HTTP request is sent
|
|
429
|
+
const [a, b] = await Promise.all([
|
|
430
|
+
client.getMedia(1),
|
|
431
|
+
client.getMedia(1),
|
|
432
|
+
]);
|
|
433
|
+
```
|
|
212
434
|
|
|
213
|
-
|
|
214
|
-
const upcoming = await client.getPlanning({ type: MediaType.ANIME, perPage: 10 });
|
|
215
|
-
upcoming.results.forEach((m) => console.log(m.title.romaji));
|
|
435
|
+
## Event hooks
|
|
216
436
|
|
|
217
|
-
|
|
218
|
-
|
|
437
|
+
Monitor every request lifecycle event for logging, metrics, or debugging.
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
const client = new AniListClient({
|
|
441
|
+
hooks: {
|
|
442
|
+
onRequest: (query, variables) => console.log("→", query.slice(0, 40)),
|
|
443
|
+
onResponse: (query, durationMs, fromCache) => console.log(`← ${durationMs}ms (cache: ${fromCache})`),
|
|
444
|
+
onCacheHit: (key) => console.log("Cache hit:", key.slice(0, 30)),
|
|
445
|
+
onRateLimit: (retryAfterMs) => console.warn(`Rate limited, waiting ${retryAfterMs}ms`),
|
|
446
|
+
onRetry: (attempt, reason, delayMs) => console.warn(`Retry #${attempt}: ${reason}`),
|
|
447
|
+
},
|
|
448
|
+
});
|
|
219
449
|
```
|
|
220
450
|
|
|
221
451
|
## Error handling
|
|
222
452
|
|
|
223
|
-
All API errors throw an `AniListError` with
|
|
224
|
-
|
|
225
|
-
- `message` — Human-readable error message
|
|
226
|
-
- `status` — HTTP status code
|
|
227
|
-
- `errors` — Raw error array from the API
|
|
453
|
+
All API errors throw an `AniListError` with `message`, `status` and `errors`:
|
|
228
454
|
|
|
229
455
|
```ts
|
|
230
|
-
import {
|
|
456
|
+
import { AniListError } from "ani-client";
|
|
231
457
|
|
|
232
458
|
try {
|
|
233
459
|
await client.getMedia(999999999);
|
|
234
460
|
} catch (err) {
|
|
235
461
|
if (err instanceof AniListError) {
|
|
236
|
-
console.error(err.message
|
|
462
|
+
console.error(err.message); // "Not Found."
|
|
463
|
+
console.error(err.status); // 404
|
|
464
|
+
console.error(err.errors); // raw API error array
|
|
237
465
|
}
|
|
238
466
|
}
|
|
239
467
|
```
|
|
240
468
|
|
|
241
469
|
## Types
|
|
242
470
|
|
|
243
|
-
All types are exported
|
|
471
|
+
All types and enums are exported:
|
|
244
472
|
|
|
245
473
|
```ts
|
|
246
474
|
import type {
|
|
247
|
-
Media,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
475
|
+
Media, Character, Staff, User,
|
|
476
|
+
AiringSchedule, MediaListEntry, Recommendation, StudioDetail,
|
|
477
|
+
MediaEdge, MediaConnection, PageInfo, PagedResult,
|
|
478
|
+
CacheAdapter, AniListHooks, AniListClientOptions,
|
|
479
|
+
SearchMediaOptions, SearchCharacterOptions, SearchStaffOptions,
|
|
480
|
+
SearchStudioOptions, GetAiringOptions, GetRecentChaptersOptions,
|
|
481
|
+
GetPlanningOptions, GetSeasonOptions, GetUserMediaListOptions,
|
|
482
|
+
GetRecommendationsOptions,
|
|
483
|
+
} from "ani-client";
|
|
484
|
+
|
|
485
|
+
import {
|
|
486
|
+
MediaType, MediaFormat, MediaStatus, MediaSeason, MediaSort,
|
|
487
|
+
CharacterSort, AiringSort, RecommendationSort,
|
|
488
|
+
MediaRelationType, MediaListStatus, MediaListSort,
|
|
257
489
|
} from "ani-client";
|
|
258
490
|
```
|
|
259
491
|
|
|
492
|
+
## Contributing
|
|
493
|
+
|
|
494
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding standards, and how to submit changes.
|
|
495
|
+
|
|
260
496
|
## License
|
|
261
497
|
|
|
262
498
|
[MIT](LICENSE) © gonzyui
|