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 CHANGED
@@ -8,14 +8,12 @@
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
9
 
10
10
  > A fully typed, zero-dependency client for the [AniList](https://anilist.co) GraphQL API.
11
- > Supports Node.js, Bun, Deno, and modern browsers.
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, Bun, Deno, and modern browsers
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
- ### Auto-Pagination
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
- ### Lifecycle Hooks
59
+ For full API reference, configuration options, caching, rate limiting, and advanced usage guides, please visit the official documentation:
204
60
 
205
- Intercept requests, responses, cache events, and errors:
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 | ≥ 20 |
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://github.com/gonzyui)
83
+ [MIT](LICENSE) © [gonzyui](https://gonzyuidev.xyz)
@@ -1 +1 @@
1
- export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-AFbnh0Xa.mjs';
1
+ export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-UeRs8nqC.mjs';
@@ -1 +1 @@
1
- export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-AFbnh0Xa.js';
1
+ export { f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from '../redis-UeRs8nqC.js';
@@ -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 ?? 86400;
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}`;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cache/redis.ts"],"names":[],"mappings":"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","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 seconds (default: 86 400 = 24 h) */\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 ?? 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"]}
1
+ {"version":3,"sources":["../../src/cache/redis.ts"],"names":[],"mappings":"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","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"]}
@@ -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 ?? 86400;
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}`;
@@ -1 +1 @@
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 seconds (default: 86 400 = 24 h) */\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 ?? 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"]}
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-AFbnh0Xa.mjs';
2
- export { e as AniListHooks, L as Logger, f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from './redis-AFbnh0Xa.mjs';
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-AFbnh0Xa.js';
2
- export { e as AniListHooks, L as Logger, f as RedisCache, g as RedisCacheOptions, h as RedisLikeClient } from './redis-AFbnh0Xa.js';
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 Object.keys(obj).sort()) {
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 [k, v] of Object.entries(obj)) {
164
- normalizedObj[k] = this.normalize(v, seen);
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 [k, v] of Object.entries(obj)) {
172
- result[k] = this.normalize(v, seen);
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 [k, v] of Object.entries(obj)) {
199
- const denormalized = this.denormalize(v, seen);
200
- if (denormalized === void 0) return void 0;
201
- result[k] = denormalized;
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.queryStore.delete(key);
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.queryStore.delete(key);
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 normalizedData = this.normalize(data);
247
- this.queryStore.delete(key);
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.delete(firstKey);
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
- return this.queryStore.delete(key);
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) this.queryStore.delete(key);
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 ?? 86400;
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.2.0" ;
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 message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
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);