ani-client 2.2.1 → 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 +7 -190
- package/dist/index.d.mts +2 -7
- package/dist/index.d.ts +2 -7
- package/dist/index.js +63 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +63 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,12 +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
|
|
11
|
+
> Supports Node.js and modern browsers.
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- **Zero dependencies** — uses the native `fetch` API
|
|
16
|
-
- **Universal** — Node.js ≥ 20,
|
|
16
|
+
- **Universal** — Node.js ≥ 20, and modern browsers
|
|
17
17
|
- **Dual format** — ships ESM + CJS with full `.d.ts` declarations
|
|
18
18
|
- **LRU cache** with TTL, stale-while-revalidate, and hit/miss stats
|
|
19
19
|
- **Rate-limit protection** with exponential backoff, retries, and custom strategies
|
|
@@ -52,180 +52,21 @@ const results = await client.searchMedia({
|
|
|
52
52
|
genres: ["Action"],
|
|
53
53
|
perPage: 10,
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
// Lookup by MyAnimeList ID
|
|
57
|
-
const fma = await client.getMediaByMalId(5114);
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Usage
|
|
61
|
-
|
|
62
|
-
### Caching
|
|
63
|
-
|
|
64
|
-
The client caches every response in memory by default. You can tune TTL, capacity, and enable stale-while-revalidate:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
const client = new AniListClient({
|
|
68
|
-
cache: {
|
|
69
|
-
ttl: 1000 * 60 * 5, // 5 min TTL
|
|
70
|
-
maxSize: 200, // LRU capacity
|
|
71
|
-
staleWhileRevalidateMs: 60_000, // serve stale for 1 min after expiry
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
For distributed setups, swap to the built-in Redis adapter:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { AniListClient, RedisCache } from "ani-client";
|
|
80
|
-
import { createClient } from "redis";
|
|
81
|
-
|
|
82
|
-
const redis = createClient();
|
|
83
|
-
await redis.connect();
|
|
84
|
-
|
|
85
|
-
const client = new AniListClient({
|
|
86
|
-
cacheAdapter: new RedisCache(redis),
|
|
87
|
-
});
|
|
88
55
|
```
|
|
89
56
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
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:
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
const client = new AniListClient({
|
|
96
|
-
rateLimit: {
|
|
97
|
-
maxRequests: 25,
|
|
98
|
-
windowMs: 60_000,
|
|
99
|
-
maxRetries: 3,
|
|
100
|
-
retryOnNetworkError: true,
|
|
101
|
-
retryStrategy: (attempt) => (attempt + 1) * 1000, // linear backoff
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Inspect the rate limit state after any request
|
|
106
|
-
console.log(client.rateLimitInfo);
|
|
107
|
-
// { remaining: 22, limit: 25, reset: 1741104000 }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Batch Requests
|
|
111
|
-
|
|
112
|
-
Fetch multiple entries in a single API call (chunks of 50):
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
const anime = await client.getMediaBatch([1, 5, 6, 20]);
|
|
116
|
-
const characters = await client.getCharacterBatch([1, 2, 3]);
|
|
117
|
-
const staff = await client.getStaffBatch([95269, 95270]);
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Auto-Pagination
|
|
121
|
-
|
|
122
|
-
Use the built-in async iterator to walk through all pages automatically:
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
for await (const anime of client.paginate(
|
|
126
|
-
(page) => client.searchMedia({ query: "Gundam", page, perPage: 50 }),
|
|
127
|
-
5, // max 5 pages
|
|
128
|
-
)) {
|
|
129
|
-
console.log(anime.title.romaji);
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Request Cancellation
|
|
134
|
-
|
|
135
|
-
Scope any client instance to an `AbortSignal` for per-request cancellation:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const controller = new AbortController();
|
|
139
|
-
const scoped = client.withSignal(controller.signal);
|
|
140
|
-
|
|
141
|
-
setTimeout(() => controller.abort(), 3_000);
|
|
142
|
-
const anime = await scoped.getMedia(1); // cancelled after 3s
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Logging
|
|
146
|
-
|
|
147
|
-
Pass any `console`-compatible logger to trace requests and cache events:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
const client = new AniListClient({ logger: console });
|
|
151
|
-
// debug: "API request" { variables: { id: 1 } }
|
|
152
|
-
// debug: "Request complete" { durationMs: 120 }
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Media Relationships
|
|
156
|
-
|
|
157
|
-
Paginated access to a media's characters and staff:
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const characters = await client.getMediaCharacters(1, {
|
|
161
|
-
page: 1,
|
|
162
|
-
perPage: 25,
|
|
163
|
-
voiceActors: true,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const staff = await client.getMediaStaff(1, { page: 1, perPage: 25 });
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Users, Characters, Studios & More
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
const user = await client.getUser("AniList");
|
|
173
|
-
const favs = await client.getUserFavorites("AniList", { perPage: 50 });
|
|
174
|
-
const char = await client.getCharacter(1, { voiceActors: true });
|
|
175
|
-
const studio = await client.getStudio(21, { media: { perPage: 50 } });
|
|
176
|
-
const schedule = await client.getWeeklySchedule();
|
|
177
|
-
const review = await client.getReview(760);
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Error Handling
|
|
181
|
-
|
|
182
|
-
All API errors throw an `AniListError` with a `status` code and the raw GraphQL `errors` array:
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
import { AniListError } from "ani-client";
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
await client.getMedia(999999999);
|
|
189
|
-
} catch (e) {
|
|
190
|
-
if (e instanceof AniListError) {
|
|
191
|
-
console.error(e.message); // "Not Found"
|
|
192
|
-
console.error(e.status); // 404
|
|
193
|
-
console.error(e.errors); // raw GraphQL errors array
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Lifecycle Hooks
|
|
57
|
+
## Documentation
|
|
199
58
|
|
|
200
|
-
|
|
59
|
+
For full API reference, configuration options, caching, rate limiting, and advanced usage guides, please visit the official documentation:
|
|
201
60
|
|
|
202
|
-
|
|
203
|
-
const client = new AniListClient({
|
|
204
|
-
hooks: {
|
|
205
|
-
onRequest: (query, variables) => console.log("→", variables),
|
|
206
|
-
onResponse: (query, durationMs, fromCache) => console.log(`← ${durationMs}ms`),
|
|
207
|
-
onCacheHit: (key) => console.log("cache hit", key),
|
|
208
|
-
onRateLimit: (retryAfterMs) => console.warn(`rate limited, retrying in ${retryAfterMs}ms`),
|
|
209
|
-
onError: (error) => console.error(error.message),
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
```
|
|
61
|
+
**[📚 ani-client.js.org](https://ani-client.js.org)**
|
|
213
62
|
|
|
214
63
|
## Requirements
|
|
215
64
|
|
|
216
65
|
| Runtime | Version |
|
|
217
66
|
|----------|--------------------|
|
|
218
67
|
| Node.js | ≥ 22.13.0 |
|
|
219
|
-
| Bun | ≥ 1.0 |
|
|
220
|
-
| Deno | ≥ 1.28 |
|
|
221
68
|
| Browsers | `fetch` + `AbortController` required |
|
|
222
69
|
|
|
223
|
-
## Documentation
|
|
224
|
-
|
|
225
|
-
Full API reference, configuration options, and guides (caching, pagination, hooks, Redis, etc.):
|
|
226
|
-
|
|
227
|
-
**[ani-client.js.org](https://ani-client.js.org)**
|
|
228
|
-
|
|
229
70
|
## Community
|
|
230
71
|
|
|
231
72
|
- 💬 [Discord server](https://discord.gg/3P7twDurUD)
|
|
@@ -233,34 +74,10 @@ Full API reference, configuration options, and guides (caching, pagination, hook
|
|
|
233
74
|
|
|
234
75
|
## Contributing
|
|
235
76
|
|
|
236
|
-
Contributions are welcome
|
|
237
|
-
|
|
238
|
-
Before opening an issue or a pull request, please read:
|
|
77
|
+
Contributions are welcome! Before opening an issue or a pull request, please read:
|
|
239
78
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
240
79
|
- [SECURITY.md](SECURITY.md)
|
|
241
80
|
|
|
242
|
-
This repository also includes GitHub issue templates and a pull request template to help keep reports and contributions consistent.
|
|
243
|
-
|
|
244
|
-
## Development
|
|
245
|
-
|
|
246
|
-
Quick commands for local development and CI checks:
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
pnpm install
|
|
250
|
-
pnpm run build # build dist
|
|
251
|
-
pnpm run typecheck # TypeScript strict checks
|
|
252
|
-
pnpm run lint # lint the codebase
|
|
253
|
-
pnpm test # run unit + integration tests
|
|
254
|
-
pnpm run docs:dev # run documentation site locally
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Notes:
|
|
258
|
-
- `tsconfig.json` has `strict: true` enabled to enforce stricter TypeScript checks.
|
|
259
|
-
- Dependabot is configured to open weekly dependency PRs — you will review and merge them manually.
|
|
260
|
-
- CI should validate `build`, `typecheck`, `lint`, and `test` before merging PRs.
|
|
261
|
-
|
|
262
|
-
This repository also includes GitHub issue templates and a pull request template to help keep reports and contributions consistent.
|
|
263
|
-
|
|
264
81
|
## License
|
|
265
82
|
|
|
266
|
-
[MIT](LICENSE) © [gonzyui](https://
|
|
83
|
+
[MIT](LICENSE) © [gonzyui](https://gonzyuidev.xyz)
|
package/dist/index.d.mts
CHANGED
|
@@ -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
|
@@ -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
|
|
@@ -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;
|