ani-client 2.2.0 → 2.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 +8 -176
- package/dist/cache/redis.d.mts +1 -1
- package/dist/cache/redis.d.ts +1 -1
- package/dist/cache/redis.js +1 -1
- package/dist/cache/redis.js.map +1 -1
- package/dist/cache/redis.mjs +1 -1
- package/dist/cache/redis.mjs.map +1 -1
- package/dist/index.d.mts +4 -9
- package/dist/index.d.ts +4 -9
- package/dist/index.js +66 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +66 -75
- package/dist/index.mjs.map +1 -1
- package/dist/{redis-AFbnh0Xa.d.mts → redis-UeRs8nqC.d.mts} +1 -1
- package/dist/{redis-AFbnh0Xa.d.ts → redis-UeRs8nqC.d.ts} +1 -1
- package/package.json +3 -13
package/README.md
CHANGED
|
@@ -8,14 +8,12 @@
|
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
10
|
> A fully typed, zero-dependency client for the [AniList](https://anilist.co) GraphQL API.
|
|
11
|
-
> Supports Node.js
|
|
12
|
-
|
|
13
|
-
> 📌 **Note** – thanks for **1K+ downloads** this month on `npm` 🎉
|
|
11
|
+
> Supports Node.js and modern browsers.
|
|
14
12
|
|
|
15
13
|
## Features
|
|
16
14
|
|
|
17
15
|
- **Zero dependencies** — uses the native `fetch` API
|
|
18
|
-
- **Universal** — Node.js ≥ 20,
|
|
16
|
+
- **Universal** — Node.js ≥ 20, and modern browsers
|
|
19
17
|
- **Dual format** — ships ESM + CJS with full `.d.ts` declarations
|
|
20
18
|
- **LRU cache** with TTL, stale-while-revalidate, and hit/miss stats
|
|
21
19
|
- **Rate-limit protection** with exponential backoff, retries, and custom strategies
|
|
@@ -54,183 +52,21 @@ const results = await client.searchMedia({
|
|
|
54
52
|
genres: ["Action"],
|
|
55
53
|
perPage: 10,
|
|
56
54
|
});
|
|
57
|
-
|
|
58
|
-
// Lookup by MyAnimeList ID
|
|
59
|
-
const fma = await client.getMediaByMalId(5114);
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Usage
|
|
63
|
-
|
|
64
|
-
### Caching
|
|
65
|
-
|
|
66
|
-
The client caches every response in memory by default. You can tune TTL, capacity, and enable stale-while-revalidate:
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
const client = new AniListClient({
|
|
70
|
-
cache: {
|
|
71
|
-
ttl: 1000 * 60 * 5, // 5 min TTL
|
|
72
|
-
maxSize: 200, // LRU capacity
|
|
73
|
-
staleWhileRevalidateMs: 60_000, // serve stale for 1 min after expiry
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
console.log(client.cacheStats);
|
|
78
|
-
// { hits: 42, misses: 8, stales: 2, hitRate: 0.84 }
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
For distributed setups, swap to the built-in Redis adapter:
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
import { AniListClient, RedisCache } from "ani-client";
|
|
85
|
-
import { createClient } from "redis";
|
|
86
|
-
|
|
87
|
-
const redis = createClient();
|
|
88
|
-
await redis.connect();
|
|
89
|
-
|
|
90
|
-
const client = new AniListClient({
|
|
91
|
-
cacheAdapter: new RedisCache(redis),
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Rate Limiting
|
|
96
|
-
|
|
97
|
-
The client is pre-configured to stay within AniList's 30 req/min limit. You can override the defaults and provide a custom retry strategy:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const client = new AniListClient({
|
|
101
|
-
rateLimit: {
|
|
102
|
-
maxRequests: 25,
|
|
103
|
-
windowMs: 60_000,
|
|
104
|
-
maxRetries: 3,
|
|
105
|
-
retryOnNetworkError: true,
|
|
106
|
-
retryStrategy: (attempt) => (attempt + 1) * 1000, // linear backoff
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Inspect the rate limit state after any request
|
|
111
|
-
console.log(client.rateLimitInfo);
|
|
112
|
-
// { remaining: 22, limit: 25, reset: 1741104000 }
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Batch Requests
|
|
116
|
-
|
|
117
|
-
Fetch multiple entries in a single API call (chunks of 50):
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
const anime = await client.getMediaBatch([1, 5, 6, 20]);
|
|
121
|
-
const characters = await client.getCharacterBatch([1, 2, 3]);
|
|
122
|
-
const staff = await client.getStaffBatch([95269, 95270]);
|
|
123
55
|
```
|
|
124
56
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Use the built-in async iterator to walk through all pages automatically:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
for await (const anime of client.paginate(
|
|
131
|
-
(page) => client.searchMedia({ query: "Gundam", page, perPage: 50 }),
|
|
132
|
-
5, // max 5 pages
|
|
133
|
-
)) {
|
|
134
|
-
console.log(anime.title.romaji);
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Request Cancellation
|
|
139
|
-
|
|
140
|
-
Scope any client instance to an `AbortSignal` for per-request cancellation:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
const controller = new AbortController();
|
|
144
|
-
const scoped = client.withSignal(controller.signal);
|
|
145
|
-
|
|
146
|
-
setTimeout(() => controller.abort(), 3_000);
|
|
147
|
-
const anime = await scoped.getMedia(1); // cancelled after 3s
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Logging
|
|
151
|
-
|
|
152
|
-
Pass any `console`-compatible logger to trace requests and cache events:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
const client = new AniListClient({ logger: console });
|
|
156
|
-
// debug: "API request" { variables: { id: 1 } }
|
|
157
|
-
// debug: "Request complete" { durationMs: 120 }
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Media Relationships
|
|
161
|
-
|
|
162
|
-
Paginated access to a media's characters and staff:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
const characters = await client.getMediaCharacters(1, {
|
|
166
|
-
page: 1,
|
|
167
|
-
perPage: 25,
|
|
168
|
-
voiceActors: true,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const staff = await client.getMediaStaff(1, { page: 1, perPage: 25 });
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Users, Characters, Studios & More
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
const user = await client.getUser("AniList");
|
|
178
|
-
const favs = await client.getUserFavorites("AniList", { perPage: 50 });
|
|
179
|
-
const char = await client.getCharacter(1, { voiceActors: true });
|
|
180
|
-
const studio = await client.getStudio(21, { media: { perPage: 50 } });
|
|
181
|
-
const schedule = await client.getWeeklySchedule();
|
|
182
|
-
const review = await client.getReview(760);
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Error Handling
|
|
186
|
-
|
|
187
|
-
All API errors throw an `AniListError` with a `status` code and the raw GraphQL `errors` array:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
import { AniListError } from "ani-client";
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
await client.getMedia(999999999);
|
|
194
|
-
} catch (e) {
|
|
195
|
-
if (e instanceof AniListError) {
|
|
196
|
-
console.error(e.message); // "Not Found"
|
|
197
|
-
console.error(e.status); // 404
|
|
198
|
-
console.error(e.errors); // raw GraphQL errors array
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
```
|
|
57
|
+
## Documentation
|
|
202
58
|
|
|
203
|
-
|
|
59
|
+
For full API reference, configuration options, caching, rate limiting, and advanced usage guides, please visit the official documentation:
|
|
204
60
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
const client = new AniListClient({
|
|
209
|
-
hooks: {
|
|
210
|
-
onRequest: (query, variables) => console.log("→", variables),
|
|
211
|
-
onResponse: (query, durationMs, fromCache) => console.log(`← ${durationMs}ms`),
|
|
212
|
-
onCacheHit: (key) => console.log("cache hit", key),
|
|
213
|
-
onRateLimit: (retryAfterMs) => console.warn(`rate limited, retrying in ${retryAfterMs}ms`),
|
|
214
|
-
onError: (error) => console.error(error.message),
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
```
|
|
61
|
+
**[📚 ani-client.js.org](https://ani-client.js.org)**
|
|
218
62
|
|
|
219
63
|
## Requirements
|
|
220
64
|
|
|
221
65
|
| Runtime | Version |
|
|
222
66
|
|----------|--------------------|
|
|
223
|
-
| Node.js | ≥
|
|
224
|
-
| Bun | ≥ 1.0 |
|
|
225
|
-
| Deno | ≥ 1.28 |
|
|
67
|
+
| Node.js | ≥ 22.13.0 |
|
|
226
68
|
| Browsers | `fetch` + `AbortController` required |
|
|
227
69
|
|
|
228
|
-
## Documentation
|
|
229
|
-
|
|
230
|
-
Full API reference, configuration options, and guides (caching, pagination, hooks, Redis, etc.):
|
|
231
|
-
|
|
232
|
-
**[ani-client.js.org](https://ani-client.js.org)**
|
|
233
|
-
|
|
234
70
|
## Community
|
|
235
71
|
|
|
236
72
|
- 💬 [Discord server](https://discord.gg/3P7twDurUD)
|
|
@@ -238,14 +74,10 @@ Full API reference, configuration options, and guides (caching, pagination, hook
|
|
|
238
74
|
|
|
239
75
|
## Contributing
|
|
240
76
|
|
|
241
|
-
Contributions are welcome
|
|
242
|
-
|
|
243
|
-
Before opening an issue or a pull request, please read:
|
|
77
|
+
Contributions are welcome! Before opening an issue or a pull request, please read:
|
|
244
78
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
245
79
|
- [SECURITY.md](SECURITY.md)
|
|
246
80
|
|
|
247
|
-
This repository also includes GitHub issue templates and a pull request template to help keep reports and contributions consistent.
|
|
248
|
-
|
|
249
81
|
## License
|
|
250
82
|
|
|
251
|
-
[MIT](LICENSE) © [gonzyui](https://
|
|
83
|
+
[MIT](LICENSE) © [gonzyui](https://gonzyuidev.xyz)
|
package/dist/cache/redis.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-
|
|
1
|
+
export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-UeRs8nqC.mjs';
|
package/dist/cache/redis.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-
|
|
1
|
+
export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-UeRs8nqC.js';
|
package/dist/cache/redis.js
CHANGED
|
@@ -8,7 +8,7 @@ var RedisCache = class {
|
|
|
8
8
|
constructor(options) {
|
|
9
9
|
this.client = options.client;
|
|
10
10
|
this.prefix = options.prefix ?? "ani:";
|
|
11
|
-
this.ttl = options.ttl
|
|
11
|
+
this.ttl = options.ttl !== void 0 ? Math.floor(options.ttl / 1e3) : 86400;
|
|
12
12
|
}
|
|
13
13
|
prefixedKey(key) {
|
|
14
14
|
return `${this.prefix}${key}`;
|
package/dist/cache/redis.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cache/redis.ts"],"names":[],"mappings":";;;AAsCO,IAAM,aAAN,MAAyC;AAAA,EAC7B,MAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAA;AAAA,EAEjB,YAAY,OAAA,EAA4B;AACtC,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,MAAA;AAChC,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,GAAA,
|
|
1
|
+
{"version":3,"sources":["../../src/cache/redis.ts"],"names":[],"mappings":";;;AAsCO,IAAM,aAAN,MAAyC;AAAA,EAC7B,MAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAA;AAAA,EAEjB,YAAY,OAAA,EAA4B;AACtC,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,MAAA;AAChC,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,GAAA,KAAQ,MAAA,GAAY,KAAK,KAAA,CAAM,OAAA,CAAQ,GAAA,GAAM,GAAI,CAAA,GAAI,KAAA;AAAA,EAC1E;AAAA,EAEQ,YAAY,GAAA,EAAqB;AACvC,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,EAAG,GAAG,CAAA,CAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,IAAO,GAAA,EAAqC;AAChD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,MAAA,CAAO,IAAI,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AACvD,IAAA,IAAI,GAAA,KAAQ,MAAM,OAAO,MAAA;AACzB,IAAA,IAAI;AACF,MAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IACvB,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,GAAA,CAAO,GAAA,EAAa,IAAA,EAAwB;AAChD,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,IAAA,CAAK,WAAA,CAAY,GAAG,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG,IAAA,EAAM,KAAK,GAAG,CAAA;AAAA,EACnF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA+B;AAC1C,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,MAAA,CAAO,IAAI,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AACzD,IAAA,OAAO,KAAA,GAAQ,CAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,YAAY,OAAA,EAAoC;AAC5D,IAAA,IAAI,IAAA,CAAK,OAAO,YAAA,EAAc;AAC5B,MAAA,MAAM,OAAiB,EAAC;AACxB,MAAA,WAAA,MAAiB,GAAA,IAAO,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,EAAE,OAAO,OAAA,EAAS,KAAA,EAAO,GAAA,EAAK,CAAA,EAAG;AAChF,QAAA,IAAA,CAAK,KAAK,GAAG,CAAA;AAAA,MACf;AACA,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA;AAAA,EACjC;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,YAAY,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAA,CAAG,CAAA;AACrD,IAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,MAAA,MAAM,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,IAAI,CAAA;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,IAAA,GAAwB;AAC1B,IAAA,OAAO,KAAK,OAAA,EAAQ;AAAA,EACtB;AAAA;AAAA,EAGA,MAAc,OAAA,GAA2B;AACvC,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,YAAY,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAA,CAAG,CAAA;AACrD,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA,EAEA,MAAM,IAAA,GAA0B;AAC9B,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,YAAY,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAA,CAAG,CAAA;AACpD,IAAA,OAAO,GAAA,CAAI,IAAI,CAAC,CAAA,KAAM,EAAE,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,MAAM,CAAC,CAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,WAAW,OAAA,EAA2C;AAC1D,IAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAC/B,MAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,WAAA,CAAY,GAAG,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA,CAAG,CAAA;AAChE,MAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AAC9B,MAAA,OAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,IAAI,CAAA;AAAA,IAChC;AAEA,IAAA,MAAM,UAAU,MAAM,IAAA,CAAK,YAAY,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAA,CAAG,CAAA;AACxD,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,CAAE,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,MAAM,CAAC,CAAC,CAAA;AAChF,IAAA,IAAI,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AAClC,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,QAAQ,CAAA;AAAA,EACpC;AACF","file":"redis.js","sourcesContent":["import type { CacheAdapter } from \"../types\";\n\n/**\n * Minimal interface representing a Redis client.\n * Compatible with both `ioredis` and `redis` (node-redis v4+).\n */\nexport interface RedisLikeClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n del(...keys: (string | string[])[]): Promise<number>;\n keys(pattern: string): Promise<string[]>;\n /** Optional SCAN-based iteration — used when available to avoid blocking the server. */\n scanIterator?(options: { MATCH: string; COUNT?: number }): AsyncIterable<string>;\n}\n\nexport interface RedisCacheOptions {\n /** A Redis client instance (ioredis or node-redis). */\n client: RedisLikeClient;\n /** Key prefix to namespace ani-client entries (default: `\"ani:\"`) */\n prefix?: string;\n /** TTL in milliseconds (default: 86 400 000 = 24h) */\n ttl?: number;\n}\n\n/**\n * Redis-backed cache adapter for AniListClient.\n *\n * @example\n * ```ts\n * import Redis from \"ioredis\";\n * import { AniListClient, RedisCache } from \"ani-client\";\n *\n * const redis = new Redis();\n * const client = new AniListClient({\n * cacheAdapter: new RedisCache({ client: redis }),\n * });\n * ```\n */\nexport class RedisCache implements CacheAdapter {\n private readonly client: RedisLikeClient;\n private readonly prefix: string;\n private readonly ttl: number;\n\n constructor(options: RedisCacheOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"ani:\";\n this.ttl = options.ttl !== undefined ? Math.floor(options.ttl / 1000) : 86_400;\n }\n\n private prefixedKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async get<T>(key: string): Promise<T | undefined> {\n const raw = await this.client.get(this.prefixedKey(key));\n if (raw === null) return undefined;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return undefined;\n }\n }\n\n async set<T>(key: string, data: T): Promise<void> {\n await this.client.set(this.prefixedKey(key), JSON.stringify(data), \"EX\", this.ttl);\n }\n\n async delete(key: string): Promise<boolean> {\n const count = await this.client.del(this.prefixedKey(key));\n return count > 0;\n }\n\n /**\n * Collect keys matching a pattern. Uses SCAN when available, falls back to KEYS.\n *\n * **Warning:** The `KEYS` fallback is O(N) and blocks the Redis server.\n * Provide a client with `scanIterator` support for production use.\n * @internal\n */\n private async collectKeys(pattern: string): Promise<string[]> {\n if (this.client.scanIterator) {\n const keys: string[] = [];\n for await (const key of this.client.scanIterator({ MATCH: pattern, COUNT: 100 })) {\n keys.push(key);\n }\n return keys;\n }\n return this.client.keys(pattern);\n }\n\n async clear(): Promise<void> {\n const keys = await this.collectKeys(`${this.prefix}*`);\n if (keys.length > 0) {\n await this.client.del(...keys);\n }\n }\n\n /**\n * Get the actual number of keys with this prefix in Redis.\n */\n get size(): Promise<number> {\n return this.getSize();\n }\n\n /** @internal */\n private async getSize(): Promise<number> {\n const keys = await this.collectKeys(`${this.prefix}*`);\n return keys.length;\n }\n\n async keys(): Promise<string[]> {\n const raw = await this.collectKeys(`${this.prefix}*`);\n return raw.map((k) => k.slice(this.prefix.length));\n }\n\n /**\n * Remove all entries whose key matches the given pattern.\n *\n * - **String**: treated as a substring match (e.g. `\"Media\"` removes all keys containing `\"Media\"`).\n * - **RegExp**: tested against each key directly.\n *\n * @param pattern — A string (substring match) or RegExp.\n * @returns Number of entries removed.\n */\n async invalidate(pattern: string | RegExp): Promise<number> {\n if (typeof pattern === \"string\") {\n const keys = await this.collectKeys(`${this.prefix}*${pattern}*`);\n if (keys.length === 0) return 0;\n return this.client.del(...keys);\n }\n\n const allKeys = await this.collectKeys(`${this.prefix}*`);\n const matching = allKeys.filter((k) => pattern.test(k.slice(this.prefix.length)));\n if (matching.length === 0) return 0;\n return this.client.del(...matching);\n }\n}\n"]}
|
package/dist/cache/redis.mjs
CHANGED
|
@@ -6,7 +6,7 @@ var RedisCache = class {
|
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.client = options.client;
|
|
8
8
|
this.prefix = options.prefix ?? "ani:";
|
|
9
|
-
this.ttl = options.ttl
|
|
9
|
+
this.ttl = options.ttl !== void 0 ? Math.floor(options.ttl / 1e3) : 86400;
|
|
10
10
|
}
|
|
11
11
|
prefixedKey(key) {
|
|
12
12
|
return `${this.prefix}${key}`;
|
package/dist/cache/redis.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cache/redis.ts"],"names":[],"mappings":";AAsCO,IAAM,aAAN,MAAyC;AAAA,EAC7B,MAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAA;AAAA,EAEjB,YAAY,OAAA,EAA4B;AACtC,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,MAAA;AAChC,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,GAAA,
|
|
1
|
+
{"version":3,"sources":["../../src/cache/redis.ts"],"names":[],"mappings":"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","file":"redis.mjs","sourcesContent":["import type { CacheAdapter } from \"../types\";\n\n/**\n * Minimal interface representing a Redis client.\n * Compatible with both `ioredis` and `redis` (node-redis v4+).\n */\nexport interface RedisLikeClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n del(...keys: (string | string[])[]): Promise<number>;\n keys(pattern: string): Promise<string[]>;\n /** Optional SCAN-based iteration — used when available to avoid blocking the server. */\n scanIterator?(options: { MATCH: string; COUNT?: number }): AsyncIterable<string>;\n}\n\nexport interface RedisCacheOptions {\n /** A Redis client instance (ioredis or node-redis). */\n client: RedisLikeClient;\n /** Key prefix to namespace ani-client entries (default: `\"ani:\"`) */\n prefix?: string;\n /** TTL in milliseconds (default: 86 400 000 = 24h) */\n ttl?: number;\n}\n\n/**\n * Redis-backed cache adapter for AniListClient.\n *\n * @example\n * ```ts\n * import Redis from \"ioredis\";\n * import { AniListClient, RedisCache } from \"ani-client\";\n *\n * const redis = new Redis();\n * const client = new AniListClient({\n * cacheAdapter: new RedisCache({ client: redis }),\n * });\n * ```\n */\nexport class RedisCache implements CacheAdapter {\n private readonly client: RedisLikeClient;\n private readonly prefix: string;\n private readonly ttl: number;\n\n constructor(options: RedisCacheOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"ani:\";\n this.ttl = options.ttl !== undefined ? Math.floor(options.ttl / 1000) : 86_400;\n }\n\n private prefixedKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async get<T>(key: string): Promise<T | undefined> {\n const raw = await this.client.get(this.prefixedKey(key));\n if (raw === null) return undefined;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return undefined;\n }\n }\n\n async set<T>(key: string, data: T): Promise<void> {\n await this.client.set(this.prefixedKey(key), JSON.stringify(data), \"EX\", this.ttl);\n }\n\n async delete(key: string): Promise<boolean> {\n const count = await this.client.del(this.prefixedKey(key));\n return count > 0;\n }\n\n /**\n * Collect keys matching a pattern. Uses SCAN when available, falls back to KEYS.\n *\n * **Warning:** The `KEYS` fallback is O(N) and blocks the Redis server.\n * Provide a client with `scanIterator` support for production use.\n * @internal\n */\n private async collectKeys(pattern: string): Promise<string[]> {\n if (this.client.scanIterator) {\n const keys: string[] = [];\n for await (const key of this.client.scanIterator({ MATCH: pattern, COUNT: 100 })) {\n keys.push(key);\n }\n return keys;\n }\n return this.client.keys(pattern);\n }\n\n async clear(): Promise<void> {\n const keys = await this.collectKeys(`${this.prefix}*`);\n if (keys.length > 0) {\n await this.client.del(...keys);\n }\n }\n\n /**\n * Get the actual number of keys with this prefix in Redis.\n */\n get size(): Promise<number> {\n return this.getSize();\n }\n\n /** @internal */\n private async getSize(): Promise<number> {\n const keys = await this.collectKeys(`${this.prefix}*`);\n return keys.length;\n }\n\n async keys(): Promise<string[]> {\n const raw = await this.collectKeys(`${this.prefix}*`);\n return raw.map((k) => k.slice(this.prefix.length));\n }\n\n /**\n * Remove all entries whose key matches the given pattern.\n *\n * - **String**: treated as a substring match (e.g. `\"Media\"` removes all keys containing `\"Media\"`).\n * - **RegExp**: tested against each key directly.\n *\n * @param pattern — A string (substring match) or RegExp.\n * @returns Number of entries removed.\n */\n async invalidate(pattern: string | RegExp): Promise<number> {\n if (typeof pattern === \"string\") {\n const keys = await this.collectKeys(`${this.prefix}*${pattern}*`);\n if (keys.length === 0) return 0;\n return this.client.del(...keys);\n }\n\n const allKeys = await this.collectKeys(`${this.prefix}*`);\n const matching = allKeys.filter((k) => pattern.test(k.slice(this.prefix.length)));\n if (matching.length === 0) return 0;\n return this.client.del(...matching);\n }\n}\n"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FuzzyDate, P as PageInfo, E as ExternalLink, C as CacheAdapter, a as CacheOptions, b as PagedResult, A as AniListClientOptions, R as RateLimitInfo, c as ResponseMeta, d as RateLimitOptions } from './redis-
|
|
2
|
-
export { e as AniListHooks, L as Logger, f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from './redis-
|
|
1
|
+
import { F as FuzzyDate, P as PageInfo, E as ExternalLink, C as CacheAdapter, a as CacheOptions, b as PagedResult, A as AniListClientOptions, R as RateLimitInfo, c as ResponseMeta, d as RateLimitOptions } from './redis-UeRs8nqC.mjs';
|
|
2
|
+
export { e as AniListHooks, L as Logger, f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from './redis-UeRs8nqC.mjs';
|
|
3
3
|
|
|
4
4
|
declare enum MediaListStatus {
|
|
5
5
|
CURRENT = "CURRENT",
|
|
@@ -931,10 +931,10 @@ declare class NormalizedCache implements CacheAdapter {
|
|
|
931
931
|
private readonly swrMs;
|
|
932
932
|
private readonly queryStore;
|
|
933
933
|
private readonly entityStore;
|
|
934
|
+
private readonly refCount;
|
|
934
935
|
private _hits;
|
|
935
936
|
private _misses;
|
|
936
937
|
private _stales;
|
|
937
|
-
private gcTimeout?;
|
|
938
938
|
constructor(options?: CacheOptions);
|
|
939
939
|
static key(query: string, variables: Record<string, unknown>): string;
|
|
940
940
|
/** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
|
|
@@ -947,6 +947,7 @@ declare class NormalizedCache implements CacheAdapter {
|
|
|
947
947
|
} | undefined;
|
|
948
948
|
get<T>(key: string): T | undefined;
|
|
949
949
|
set<T>(key: string, data: T): void;
|
|
950
|
+
private deleteQueryEntry;
|
|
950
951
|
delete(key: string): boolean;
|
|
951
952
|
clear(): void;
|
|
952
953
|
get size(): number;
|
|
@@ -956,12 +957,6 @@ declare class NormalizedCache implements CacheAdapter {
|
|
|
956
957
|
entitiesCount: number;
|
|
957
958
|
};
|
|
958
959
|
resetStats(): void;
|
|
959
|
-
private scheduleGc;
|
|
960
|
-
/**
|
|
961
|
-
* Garbage-collect orphaned entities that are no longer referenced by any query.
|
|
962
|
-
* Called automatically on LRU eviction to prevent unbounded entity store growth.
|
|
963
|
-
*/
|
|
964
|
-
gc(): number;
|
|
965
960
|
}
|
|
966
961
|
|
|
967
962
|
/** Cache performance statistics. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FuzzyDate, P as PageInfo, E as ExternalLink, C as CacheAdapter, a as CacheOptions, b as PagedResult, A as AniListClientOptions, R as RateLimitInfo, c as ResponseMeta, d as RateLimitOptions } from './redis-
|
|
2
|
-
export { e as AniListHooks, L as Logger, f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from './redis-
|
|
1
|
+
import { F as FuzzyDate, P as PageInfo, E as ExternalLink, C as CacheAdapter, a as CacheOptions, b as PagedResult, A as AniListClientOptions, R as RateLimitInfo, c as ResponseMeta, d as RateLimitOptions } from './redis-UeRs8nqC.js';
|
|
2
|
+
export { e as AniListHooks, L as Logger, f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from './redis-UeRs8nqC.js';
|
|
3
3
|
|
|
4
4
|
declare enum MediaListStatus {
|
|
5
5
|
CURRENT = "CURRENT",
|
|
@@ -931,10 +931,10 @@ declare class NormalizedCache implements CacheAdapter {
|
|
|
931
931
|
private readonly swrMs;
|
|
932
932
|
private readonly queryStore;
|
|
933
933
|
private readonly entityStore;
|
|
934
|
+
private readonly refCount;
|
|
934
935
|
private _hits;
|
|
935
936
|
private _misses;
|
|
936
937
|
private _stales;
|
|
937
|
-
private gcTimeout?;
|
|
938
938
|
constructor(options?: CacheOptions);
|
|
939
939
|
static key(query: string, variables: Record<string, unknown>): string;
|
|
940
940
|
/** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
|
|
@@ -947,6 +947,7 @@ declare class NormalizedCache implements CacheAdapter {
|
|
|
947
947
|
} | undefined;
|
|
948
948
|
get<T>(key: string): T | undefined;
|
|
949
949
|
set<T>(key: string, data: T): void;
|
|
950
|
+
private deleteQueryEntry;
|
|
950
951
|
delete(key: string): boolean;
|
|
951
952
|
clear(): void;
|
|
952
953
|
get size(): number;
|
|
@@ -956,12 +957,6 @@ declare class NormalizedCache implements CacheAdapter {
|
|
|
956
957
|
entitiesCount: number;
|
|
957
958
|
};
|
|
958
959
|
resetStats(): void;
|
|
959
|
-
private scheduleGc;
|
|
960
|
-
/**
|
|
961
|
-
* Garbage-collect orphaned entities that are no longer referenced by any query.
|
|
962
|
-
* Called automatically on LRU eviction to prevent unbounded entity store growth.
|
|
963
|
-
*/
|
|
964
|
-
gc(): number;
|
|
965
960
|
}
|
|
966
961
|
|
|
967
962
|
/** Cache performance statistics. */
|
package/dist/index.js
CHANGED
|
@@ -117,8 +117,17 @@ function validateIds(ids, label = "id") {
|
|
|
117
117
|
function sortObjectKeys(obj) {
|
|
118
118
|
if (obj === null || typeof obj !== "object") return obj;
|
|
119
119
|
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
|
|
120
|
+
const keys = Object.keys(obj);
|
|
121
|
+
if (keys.length === 0) return obj;
|
|
122
|
+
if (keys.length === 1) {
|
|
123
|
+
const key = keys[0];
|
|
124
|
+
const val = obj[key];
|
|
125
|
+
const sortedVal = sortObjectKeys(val);
|
|
126
|
+
if (val === sortedVal) return obj;
|
|
127
|
+
return { [key]: sortedVal };
|
|
128
|
+
}
|
|
120
129
|
const sorted = {};
|
|
121
|
-
for (const key of
|
|
130
|
+
for (const key of keys.sort()) {
|
|
122
131
|
sorted[key] = sortObjectKeys(obj[key]);
|
|
123
132
|
}
|
|
124
133
|
return sorted;
|
|
@@ -132,10 +141,10 @@ var NormalizedCache = class {
|
|
|
132
141
|
swrMs;
|
|
133
142
|
queryStore = /* @__PURE__ */ new Map();
|
|
134
143
|
entityStore = /* @__PURE__ */ new Map();
|
|
144
|
+
refCount = /* @__PURE__ */ new Map();
|
|
135
145
|
_hits = 0;
|
|
136
146
|
_misses = 0;
|
|
137
147
|
_stales = 0;
|
|
138
|
-
gcTimeout;
|
|
139
148
|
constructor(options = {}) {
|
|
140
149
|
this.ttl = options.ttl ?? 24 * 60 * 60 * 1e3;
|
|
141
150
|
this.maxSize = options.maxSize ?? 500;
|
|
@@ -147,11 +156,11 @@ var NormalizedCache = class {
|
|
|
147
156
|
return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
|
|
148
157
|
}
|
|
149
158
|
/** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
|
|
150
|
-
normalize(data, seen = /* @__PURE__ */ new WeakSet()) {
|
|
159
|
+
normalize(data, refsOut, seen = /* @__PURE__ */ new WeakSet()) {
|
|
151
160
|
if (Array.isArray(data)) {
|
|
152
161
|
if (seen.has(data)) return null;
|
|
153
162
|
seen.add(data);
|
|
154
|
-
return data.map((item) => this.normalize(item, seen));
|
|
163
|
+
return data.map((item) => this.normalize(item, refsOut, seen));
|
|
155
164
|
}
|
|
156
165
|
if (data !== null && typeof data === "object") {
|
|
157
166
|
if (seen.has(data)) return null;
|
|
@@ -159,17 +168,22 @@ var NormalizedCache = class {
|
|
|
159
168
|
const obj = data;
|
|
160
169
|
if (typeof obj.__typename === "string" && (typeof obj.id === "number" || typeof obj.id === "string")) {
|
|
161
170
|
const ref = `${obj.__typename}:${obj.id}`;
|
|
171
|
+
refsOut.add(ref);
|
|
162
172
|
const normalizedObj = {};
|
|
163
|
-
for (const
|
|
164
|
-
|
|
173
|
+
for (const k in obj) {
|
|
174
|
+
if (Object.hasOwn(obj, k)) {
|
|
175
|
+
normalizedObj[k] = this.normalize(obj[k], refsOut, seen);
|
|
176
|
+
}
|
|
165
177
|
}
|
|
166
178
|
const existing = this.entityStore.get(ref) || {};
|
|
167
179
|
this.entityStore.set(ref, { ...existing, ...normalizedObj });
|
|
168
180
|
return { __ref: ref };
|
|
169
181
|
}
|
|
170
182
|
const result = {};
|
|
171
|
-
for (const
|
|
172
|
-
|
|
183
|
+
for (const k in obj) {
|
|
184
|
+
if (Object.hasOwn(obj, k)) {
|
|
185
|
+
result[k] = this.normalize(obj[k], refsOut, seen);
|
|
186
|
+
}
|
|
173
187
|
}
|
|
174
188
|
return result;
|
|
175
189
|
}
|
|
@@ -195,10 +209,12 @@ var NormalizedCache = class {
|
|
|
195
209
|
return result2;
|
|
196
210
|
}
|
|
197
211
|
const result = {};
|
|
198
|
-
for (const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
for (const k in obj) {
|
|
213
|
+
if (Object.hasOwn(obj, k)) {
|
|
214
|
+
const denormalized = this.denormalize(obj[k], seen);
|
|
215
|
+
if (denormalized === void 0) return void 0;
|
|
216
|
+
result[k] = denormalized;
|
|
217
|
+
}
|
|
202
218
|
}
|
|
203
219
|
return result;
|
|
204
220
|
}
|
|
@@ -217,14 +233,14 @@ var NormalizedCache = class {
|
|
|
217
233
|
if (this.swrMs > 0 && now <= entry.expiresAt + this.swrMs) {
|
|
218
234
|
isStale = true;
|
|
219
235
|
} else {
|
|
220
|
-
this.
|
|
236
|
+
this.deleteQueryEntry(key, entry);
|
|
221
237
|
this._misses++;
|
|
222
238
|
return void 0;
|
|
223
239
|
}
|
|
224
240
|
}
|
|
225
241
|
const denormalized = this.denormalize(entry.data);
|
|
226
242
|
if (denormalized === void 0) {
|
|
227
|
-
this.
|
|
243
|
+
this.deleteQueryEntry(key, entry);
|
|
228
244
|
this._misses++;
|
|
229
245
|
return void 0;
|
|
230
246
|
}
|
|
@@ -243,27 +259,46 @@ var NormalizedCache = class {
|
|
|
243
259
|
}
|
|
244
260
|
set(key, data) {
|
|
245
261
|
if (!this.enabled) return;
|
|
246
|
-
const
|
|
247
|
-
this.
|
|
262
|
+
const refs = /* @__PURE__ */ new Set();
|
|
263
|
+
const normalizedData = this.normalize(data, refs);
|
|
264
|
+
const existing = this.queryStore.get(key);
|
|
265
|
+
if (existing) {
|
|
266
|
+
this.deleteQueryEntry(key, existing);
|
|
267
|
+
}
|
|
248
268
|
if (this.maxSize > 0 && this.queryStore.size >= this.maxSize) {
|
|
249
269
|
const firstKey = this.queryStore.keys().next().value;
|
|
250
270
|
if (firstKey !== void 0) {
|
|
251
|
-
this.queryStore.
|
|
271
|
+
const firstEntry = this.queryStore.get(firstKey);
|
|
272
|
+
if (firstEntry) this.deleteQueryEntry(firstKey, firstEntry);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const ref of refs) {
|
|
276
|
+
this.refCount.set(ref, (this.refCount.get(ref) ?? 0) + 1);
|
|
277
|
+
}
|
|
278
|
+
this.queryStore.set(key, { data: normalizedData, refs, expiresAt: Date.now() + this.ttl });
|
|
279
|
+
}
|
|
280
|
+
deleteQueryEntry(key, entry) {
|
|
281
|
+
this.queryStore.delete(key);
|
|
282
|
+
for (const ref of entry.refs) {
|
|
283
|
+
const count = (this.refCount.get(ref) ?? 0) - 1;
|
|
284
|
+
if (count <= 0) {
|
|
285
|
+
this.refCount.delete(ref);
|
|
286
|
+
this.entityStore.delete(ref);
|
|
287
|
+
} else {
|
|
288
|
+
this.refCount.set(ref, count);
|
|
252
289
|
}
|
|
253
|
-
this.scheduleGc();
|
|
254
290
|
}
|
|
255
|
-
this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
|
|
256
291
|
}
|
|
257
292
|
delete(key) {
|
|
258
|
-
|
|
293
|
+
const entry = this.queryStore.get(key);
|
|
294
|
+
if (!entry) return false;
|
|
295
|
+
this.deleteQueryEntry(key, entry);
|
|
296
|
+
return true;
|
|
259
297
|
}
|
|
260
298
|
clear() {
|
|
261
|
-
if (this.gcTimeout) {
|
|
262
|
-
clearTimeout(this.gcTimeout);
|
|
263
|
-
this.gcTimeout = void 0;
|
|
264
|
-
}
|
|
265
299
|
this.queryStore.clear();
|
|
266
300
|
this.entityStore.clear();
|
|
301
|
+
this.refCount.clear();
|
|
267
302
|
this._hits = 0;
|
|
268
303
|
this._misses = 0;
|
|
269
304
|
this._stales = 0;
|
|
@@ -280,7 +315,9 @@ var NormalizedCache = class {
|
|
|
280
315
|
for (const key of this.queryStore.keys()) {
|
|
281
316
|
if (test(key)) toDelete.push(key);
|
|
282
317
|
}
|
|
283
|
-
for (const key of toDelete)
|
|
318
|
+
for (const key of toDelete) {
|
|
319
|
+
this.delete(key);
|
|
320
|
+
}
|
|
284
321
|
return toDelete.length;
|
|
285
322
|
}
|
|
286
323
|
get stats() {
|
|
@@ -298,53 +335,6 @@ var NormalizedCache = class {
|
|
|
298
335
|
this._misses = 0;
|
|
299
336
|
this._stales = 0;
|
|
300
337
|
}
|
|
301
|
-
scheduleGc() {
|
|
302
|
-
if (this.gcTimeout) return;
|
|
303
|
-
this.gcTimeout = setTimeout(() => {
|
|
304
|
-
this.gc();
|
|
305
|
-
this.gcTimeout = void 0;
|
|
306
|
-
}, 500);
|
|
307
|
-
if (typeof this.gcTimeout.unref === "function") {
|
|
308
|
-
this.gcTimeout.unref();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Garbage-collect orphaned entities that are no longer referenced by any query.
|
|
313
|
-
* Called automatically on LRU eviction to prevent unbounded entity store growth.
|
|
314
|
-
*/
|
|
315
|
-
gc() {
|
|
316
|
-
const referencedRefs = /* @__PURE__ */ new Set();
|
|
317
|
-
const collectRefs = (data) => {
|
|
318
|
-
if (Array.isArray(data)) {
|
|
319
|
-
for (const item of data) collectRefs(item);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (data !== null && typeof data === "object") {
|
|
323
|
-
const obj = data;
|
|
324
|
-
if (typeof obj.__ref === "string") {
|
|
325
|
-
referencedRefs.add(obj.__ref);
|
|
326
|
-
const entity = this.entityStore.get(obj.__ref);
|
|
327
|
-
if (entity && !referencedRefs.has(`_visited:${obj.__ref}`)) {
|
|
328
|
-
referencedRefs.add(`_visited:${obj.__ref}`);
|
|
329
|
-
collectRefs(entity);
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
for (const v of Object.values(obj)) collectRefs(v);
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
for (const entry of this.queryStore.values()) {
|
|
337
|
-
collectRefs(entry.data);
|
|
338
|
-
}
|
|
339
|
-
let removed = 0;
|
|
340
|
-
for (const ref of this.entityStore.keys()) {
|
|
341
|
-
if (!referencedRefs.has(ref)) {
|
|
342
|
-
this.entityStore.delete(ref);
|
|
343
|
-
removed++;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return removed;
|
|
347
|
-
}
|
|
348
338
|
};
|
|
349
339
|
|
|
350
340
|
// src/cache/index.ts
|
|
@@ -485,7 +475,7 @@ var RedisCache = class {
|
|
|
485
475
|
constructor(options) {
|
|
486
476
|
this.client = options.client;
|
|
487
477
|
this.prefix = options.prefix ?? "ani:";
|
|
488
|
-
this.ttl = options.ttl
|
|
478
|
+
this.ttl = options.ttl !== void 0 ? Math.floor(options.ttl / 1e3) : 86400;
|
|
489
479
|
}
|
|
490
480
|
prefixedKey(key) {
|
|
491
481
|
return `${this.prefix}${key}`;
|
|
@@ -2541,7 +2531,7 @@ function mapFavorites(fav) {
|
|
|
2541
2531
|
|
|
2542
2532
|
// src/client/index.ts
|
|
2543
2533
|
var DEFAULT_API_URL = "https://graphql.anilist.co";
|
|
2544
|
-
var LIB_VERSION = "2.
|
|
2534
|
+
var LIB_VERSION = "2.3.0" ;
|
|
2545
2535
|
var AniListClient = class {
|
|
2546
2536
|
apiUrl;
|
|
2547
2537
|
headers;
|
|
@@ -2654,7 +2644,8 @@ var AniListClient = class {
|
|
|
2654
2644
|
throw error;
|
|
2655
2645
|
}
|
|
2656
2646
|
if (!res.ok || json.errors) {
|
|
2657
|
-
const
|
|
2647
|
+
const msgs = (json.errors || []).map((e) => e.message).filter(Boolean);
|
|
2648
|
+
const message = msgs.length > 0 ? msgs.join(" | ") : `AniList API error (HTTP ${res.status})`;
|
|
2658
2649
|
const error = new AniListError(message, res.status, json.errors ?? []);
|
|
2659
2650
|
this.logger?.error("Request failed", { error: error.message, status: error.status });
|
|
2660
2651
|
this.hooks.onError?.(error, query, variables);
|