ani-client 2.2.1 → 2.4.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 +9 -191
- package/dist/index.d.mts +5 -7
- package/dist/index.d.ts +5 -7
- package/dist/index.js +139 -76
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +139 -76
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
// src/utils/dataloader.ts
|
|
2
|
+
var BatchLoader = class {
|
|
3
|
+
constructor(batchFetch, maxWaitMs = 50) {
|
|
4
|
+
this.batchFetch = batchFetch;
|
|
5
|
+
this.maxWaitMs = maxWaitMs;
|
|
6
|
+
}
|
|
7
|
+
queue = /* @__PURE__ */ new Map();
|
|
8
|
+
timeout = null;
|
|
9
|
+
/**
|
|
10
|
+
* Queue an ID to be fetched in the next batch.
|
|
11
|
+
* Returns a Promise that resolves when the batch request completes.
|
|
12
|
+
*/
|
|
13
|
+
async load(id) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let callbacks = this.queue.get(id);
|
|
16
|
+
if (!callbacks) {
|
|
17
|
+
callbacks = [];
|
|
18
|
+
this.queue.set(id, callbacks);
|
|
19
|
+
}
|
|
20
|
+
callbacks.push({ resolve, reject });
|
|
21
|
+
if (this.timeout === null) {
|
|
22
|
+
this.timeout = setTimeout(() => this.dispatch(), this.maxWaitMs);
|
|
23
|
+
if (typeof this.timeout.unref === "function") {
|
|
24
|
+
this.timeout.unref();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async dispatch() {
|
|
30
|
+
this.timeout = null;
|
|
31
|
+
if (this.queue.size === 0) return;
|
|
32
|
+
const currentQueue = this.queue;
|
|
33
|
+
this.queue = /* @__PURE__ */ new Map();
|
|
34
|
+
const ids = Array.from(currentQueue.keys());
|
|
35
|
+
try {
|
|
36
|
+
const results = await this.batchFetch(ids);
|
|
37
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const item of results) {
|
|
39
|
+
resultMap.set(item.id, item);
|
|
40
|
+
}
|
|
41
|
+
for (const [id, callbacks] of currentQueue.entries()) {
|
|
42
|
+
const result = resultMap.get(id);
|
|
43
|
+
if (result) {
|
|
44
|
+
for (const cb of callbacks) cb.resolve(result);
|
|
45
|
+
} else {
|
|
46
|
+
const err = new Error(`Item with ID ${id} not found in batch response`);
|
|
47
|
+
for (const cb of callbacks) cb.reject(err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
for (const callbacks of currentQueue.values()) {
|
|
52
|
+
for (const cb of callbacks) cb.reject(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
1
58
|
// src/utils/markdown.ts
|
|
2
59
|
function isSafeUrl(url) {
|
|
3
60
|
return /^https?:\/\//i.test(url);
|
|
@@ -115,8 +172,17 @@ function validateIds(ids, label = "id") {
|
|
|
115
172
|
function sortObjectKeys(obj) {
|
|
116
173
|
if (obj === null || typeof obj !== "object") return obj;
|
|
117
174
|
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
|
|
175
|
+
const keys = Object.keys(obj);
|
|
176
|
+
if (keys.length === 0) return obj;
|
|
177
|
+
if (keys.length === 1) {
|
|
178
|
+
const key = keys[0];
|
|
179
|
+
const val = obj[key];
|
|
180
|
+
const sortedVal = sortObjectKeys(val);
|
|
181
|
+
if (val === sortedVal) return obj;
|
|
182
|
+
return { [key]: sortedVal };
|
|
183
|
+
}
|
|
118
184
|
const sorted = {};
|
|
119
|
-
for (const key of
|
|
185
|
+
for (const key of keys.sort()) {
|
|
120
186
|
sorted[key] = sortObjectKeys(obj[key]);
|
|
121
187
|
}
|
|
122
188
|
return sorted;
|
|
@@ -130,10 +196,10 @@ var NormalizedCache = class {
|
|
|
130
196
|
swrMs;
|
|
131
197
|
queryStore = /* @__PURE__ */ new Map();
|
|
132
198
|
entityStore = /* @__PURE__ */ new Map();
|
|
199
|
+
refCount = /* @__PURE__ */ new Map();
|
|
133
200
|
_hits = 0;
|
|
134
201
|
_misses = 0;
|
|
135
202
|
_stales = 0;
|
|
136
|
-
gcTimeout;
|
|
137
203
|
constructor(options = {}) {
|
|
138
204
|
this.ttl = options.ttl ?? 24 * 60 * 60 * 1e3;
|
|
139
205
|
this.maxSize = options.maxSize ?? 500;
|
|
@@ -145,11 +211,11 @@ var NormalizedCache = class {
|
|
|
145
211
|
return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
|
|
146
212
|
}
|
|
147
213
|
/** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
|
|
148
|
-
normalize(data, seen = /* @__PURE__ */ new WeakSet()) {
|
|
214
|
+
normalize(data, refsOut, seen = /* @__PURE__ */ new WeakSet()) {
|
|
149
215
|
if (Array.isArray(data)) {
|
|
150
216
|
if (seen.has(data)) return null;
|
|
151
217
|
seen.add(data);
|
|
152
|
-
return data.map((item) => this.normalize(item, seen));
|
|
218
|
+
return data.map((item) => this.normalize(item, refsOut, seen));
|
|
153
219
|
}
|
|
154
220
|
if (data !== null && typeof data === "object") {
|
|
155
221
|
if (seen.has(data)) return null;
|
|
@@ -157,17 +223,22 @@ var NormalizedCache = class {
|
|
|
157
223
|
const obj = data;
|
|
158
224
|
if (typeof obj.__typename === "string" && (typeof obj.id === "number" || typeof obj.id === "string")) {
|
|
159
225
|
const ref = `${obj.__typename}:${obj.id}`;
|
|
226
|
+
refsOut.add(ref);
|
|
160
227
|
const normalizedObj = {};
|
|
161
|
-
for (const
|
|
162
|
-
|
|
228
|
+
for (const k in obj) {
|
|
229
|
+
if (Object.hasOwn(obj, k)) {
|
|
230
|
+
normalizedObj[k] = this.normalize(obj[k], refsOut, seen);
|
|
231
|
+
}
|
|
163
232
|
}
|
|
164
233
|
const existing = this.entityStore.get(ref) || {};
|
|
165
234
|
this.entityStore.set(ref, { ...existing, ...normalizedObj });
|
|
166
235
|
return { __ref: ref };
|
|
167
236
|
}
|
|
168
237
|
const result = {};
|
|
169
|
-
for (const
|
|
170
|
-
|
|
238
|
+
for (const k in obj) {
|
|
239
|
+
if (Object.hasOwn(obj, k)) {
|
|
240
|
+
result[k] = this.normalize(obj[k], refsOut, seen);
|
|
241
|
+
}
|
|
171
242
|
}
|
|
172
243
|
return result;
|
|
173
244
|
}
|
|
@@ -193,10 +264,12 @@ var NormalizedCache = class {
|
|
|
193
264
|
return result2;
|
|
194
265
|
}
|
|
195
266
|
const result = {};
|
|
196
|
-
for (const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
267
|
+
for (const k in obj) {
|
|
268
|
+
if (Object.hasOwn(obj, k)) {
|
|
269
|
+
const denormalized = this.denormalize(obj[k], seen);
|
|
270
|
+
if (denormalized === void 0) return void 0;
|
|
271
|
+
result[k] = denormalized;
|
|
272
|
+
}
|
|
200
273
|
}
|
|
201
274
|
return result;
|
|
202
275
|
}
|
|
@@ -215,14 +288,14 @@ var NormalizedCache = class {
|
|
|
215
288
|
if (this.swrMs > 0 && now <= entry.expiresAt + this.swrMs) {
|
|
216
289
|
isStale = true;
|
|
217
290
|
} else {
|
|
218
|
-
this.
|
|
291
|
+
this.deleteQueryEntry(key, entry);
|
|
219
292
|
this._misses++;
|
|
220
293
|
return void 0;
|
|
221
294
|
}
|
|
222
295
|
}
|
|
223
296
|
const denormalized = this.denormalize(entry.data);
|
|
224
297
|
if (denormalized === void 0) {
|
|
225
|
-
this.
|
|
298
|
+
this.deleteQueryEntry(key, entry);
|
|
226
299
|
this._misses++;
|
|
227
300
|
return void 0;
|
|
228
301
|
}
|
|
@@ -241,27 +314,46 @@ var NormalizedCache = class {
|
|
|
241
314
|
}
|
|
242
315
|
set(key, data) {
|
|
243
316
|
if (!this.enabled) return;
|
|
244
|
-
const
|
|
245
|
-
this.
|
|
317
|
+
const refs = /* @__PURE__ */ new Set();
|
|
318
|
+
const normalizedData = this.normalize(data, refs);
|
|
319
|
+
const existing = this.queryStore.get(key);
|
|
320
|
+
if (existing) {
|
|
321
|
+
this.deleteQueryEntry(key, existing);
|
|
322
|
+
}
|
|
246
323
|
if (this.maxSize > 0 && this.queryStore.size >= this.maxSize) {
|
|
247
324
|
const firstKey = this.queryStore.keys().next().value;
|
|
248
325
|
if (firstKey !== void 0) {
|
|
249
|
-
this.queryStore.
|
|
326
|
+
const firstEntry = this.queryStore.get(firstKey);
|
|
327
|
+
if (firstEntry) this.deleteQueryEntry(firstKey, firstEntry);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const ref of refs) {
|
|
331
|
+
this.refCount.set(ref, (this.refCount.get(ref) ?? 0) + 1);
|
|
332
|
+
}
|
|
333
|
+
this.queryStore.set(key, { data: normalizedData, refs, expiresAt: Date.now() + this.ttl });
|
|
334
|
+
}
|
|
335
|
+
deleteQueryEntry(key, entry) {
|
|
336
|
+
this.queryStore.delete(key);
|
|
337
|
+
for (const ref of entry.refs) {
|
|
338
|
+
const count = (this.refCount.get(ref) ?? 0) - 1;
|
|
339
|
+
if (count <= 0) {
|
|
340
|
+
this.refCount.delete(ref);
|
|
341
|
+
this.entityStore.delete(ref);
|
|
342
|
+
} else {
|
|
343
|
+
this.refCount.set(ref, count);
|
|
250
344
|
}
|
|
251
|
-
this.scheduleGc();
|
|
252
345
|
}
|
|
253
|
-
this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
|
|
254
346
|
}
|
|
255
347
|
delete(key) {
|
|
256
|
-
|
|
348
|
+
const entry = this.queryStore.get(key);
|
|
349
|
+
if (!entry) return false;
|
|
350
|
+
this.deleteQueryEntry(key, entry);
|
|
351
|
+
return true;
|
|
257
352
|
}
|
|
258
353
|
clear() {
|
|
259
|
-
if (this.gcTimeout) {
|
|
260
|
-
clearTimeout(this.gcTimeout);
|
|
261
|
-
this.gcTimeout = void 0;
|
|
262
|
-
}
|
|
263
354
|
this.queryStore.clear();
|
|
264
355
|
this.entityStore.clear();
|
|
356
|
+
this.refCount.clear();
|
|
265
357
|
this._hits = 0;
|
|
266
358
|
this._misses = 0;
|
|
267
359
|
this._stales = 0;
|
|
@@ -278,7 +370,9 @@ var NormalizedCache = class {
|
|
|
278
370
|
for (const key of this.queryStore.keys()) {
|
|
279
371
|
if (test(key)) toDelete.push(key);
|
|
280
372
|
}
|
|
281
|
-
for (const key of toDelete)
|
|
373
|
+
for (const key of toDelete) {
|
|
374
|
+
this.delete(key);
|
|
375
|
+
}
|
|
282
376
|
return toDelete.length;
|
|
283
377
|
}
|
|
284
378
|
get stats() {
|
|
@@ -296,53 +390,6 @@ var NormalizedCache = class {
|
|
|
296
390
|
this._misses = 0;
|
|
297
391
|
this._stales = 0;
|
|
298
392
|
}
|
|
299
|
-
scheduleGc() {
|
|
300
|
-
if (this.gcTimeout) return;
|
|
301
|
-
this.gcTimeout = setTimeout(() => {
|
|
302
|
-
this.gc();
|
|
303
|
-
this.gcTimeout = void 0;
|
|
304
|
-
}, 500);
|
|
305
|
-
if (typeof this.gcTimeout.unref === "function") {
|
|
306
|
-
this.gcTimeout.unref();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Garbage-collect orphaned entities that are no longer referenced by any query.
|
|
311
|
-
* Called automatically on LRU eviction to prevent unbounded entity store growth.
|
|
312
|
-
*/
|
|
313
|
-
gc() {
|
|
314
|
-
const referencedRefs = /* @__PURE__ */ new Set();
|
|
315
|
-
const collectRefs = (data) => {
|
|
316
|
-
if (Array.isArray(data)) {
|
|
317
|
-
for (const item of data) collectRefs(item);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
if (data !== null && typeof data === "object") {
|
|
321
|
-
const obj = data;
|
|
322
|
-
if (typeof obj.__ref === "string") {
|
|
323
|
-
referencedRefs.add(obj.__ref);
|
|
324
|
-
const entity = this.entityStore.get(obj.__ref);
|
|
325
|
-
if (entity && !referencedRefs.has(`_visited:${obj.__ref}`)) {
|
|
326
|
-
referencedRefs.add(`_visited:${obj.__ref}`);
|
|
327
|
-
collectRefs(entity);
|
|
328
|
-
}
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
for (const v of Object.values(obj)) collectRefs(v);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
for (const entry of this.queryStore.values()) {
|
|
335
|
-
collectRefs(entry.data);
|
|
336
|
-
}
|
|
337
|
-
let removed = 0;
|
|
338
|
-
for (const ref of this.entityStore.keys()) {
|
|
339
|
-
if (!referencedRefs.has(ref)) {
|
|
340
|
-
this.entityStore.delete(ref);
|
|
341
|
-
removed++;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return removed;
|
|
345
|
-
}
|
|
346
393
|
};
|
|
347
394
|
|
|
348
395
|
// src/cache/index.ts
|
|
@@ -2539,7 +2586,7 @@ function mapFavorites(fav) {
|
|
|
2539
2586
|
|
|
2540
2587
|
// src/client/index.ts
|
|
2541
2588
|
var DEFAULT_API_URL = "https://graphql.anilist.co";
|
|
2542
|
-
var LIB_VERSION = "2.
|
|
2589
|
+
var LIB_VERSION = "2.4.0" ;
|
|
2543
2590
|
var AniListClient = class {
|
|
2544
2591
|
apiUrl;
|
|
2545
2592
|
headers;
|
|
@@ -2551,6 +2598,9 @@ var AniListClient = class {
|
|
|
2551
2598
|
inFlight = /* @__PURE__ */ new Map();
|
|
2552
2599
|
_rateLimitInfo;
|
|
2553
2600
|
_lastRequestMeta;
|
|
2601
|
+
mediaLoader;
|
|
2602
|
+
characterLoader;
|
|
2603
|
+
staffLoader;
|
|
2554
2604
|
constructor(options = {}) {
|
|
2555
2605
|
this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
|
|
2556
2606
|
this.headers = {
|
|
@@ -2566,6 +2616,9 @@ var AniListClient = class {
|
|
|
2566
2616
|
this.hooks = options.hooks ?? {};
|
|
2567
2617
|
this.logger = options.logger;
|
|
2568
2618
|
this.signal = options.signal;
|
|
2619
|
+
this.mediaLoader = new BatchLoader((ids) => this.getMediaBatch(ids), 50);
|
|
2620
|
+
this.characterLoader = new BatchLoader((ids) => this.getCharacterBatch(ids), 50);
|
|
2621
|
+
this.staffLoader = new BatchLoader((ids) => this.getStaffBatch(ids), 50);
|
|
2569
2622
|
}
|
|
2570
2623
|
/**
|
|
2571
2624
|
* The current rate limit information from the last API response.
|
|
@@ -2696,6 +2749,9 @@ var AniListClient = class {
|
|
|
2696
2749
|
* @param include - Optional related data to include
|
|
2697
2750
|
*/
|
|
2698
2751
|
async getMedia(id, include) {
|
|
2752
|
+
if (!include) {
|
|
2753
|
+
return this.mediaLoader.load(id);
|
|
2754
|
+
}
|
|
2699
2755
|
return getMedia(this, id, include);
|
|
2700
2756
|
}
|
|
2701
2757
|
async getMediaCharacters(mediaId, options = {}) {
|
|
@@ -2764,6 +2820,9 @@ var AniListClient = class {
|
|
|
2764
2820
|
}
|
|
2765
2821
|
/** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
|
|
2766
2822
|
async getCharacter(id, include) {
|
|
2823
|
+
if (!include) {
|
|
2824
|
+
return this.characterLoader.load(id);
|
|
2825
|
+
}
|
|
2767
2826
|
return getCharacter(this, id, include);
|
|
2768
2827
|
}
|
|
2769
2828
|
/** Search for characters by name. */
|
|
@@ -2772,6 +2831,9 @@ var AniListClient = class {
|
|
|
2772
2831
|
}
|
|
2773
2832
|
/** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
|
|
2774
2833
|
async getStaff(id, include) {
|
|
2834
|
+
if (!include) {
|
|
2835
|
+
return this.staffLoader.load(id);
|
|
2836
|
+
}
|
|
2775
2837
|
return getStaff(this, id, include);
|
|
2776
2838
|
}
|
|
2777
2839
|
/** Search for staff (voice actors, directors, etc.). */
|
|
@@ -2891,7 +2953,7 @@ var AniListClient = class {
|
|
|
2891
2953
|
if (ids.length === 0) return [];
|
|
2892
2954
|
validateIds(ids, "mediaId");
|
|
2893
2955
|
const [singleMediaId] = ids;
|
|
2894
|
-
if (ids.length === 1 && singleMediaId !== void 0) return [await
|
|
2956
|
+
if (ids.length === 1 && singleMediaId !== void 0) return [await getMedia(this, singleMediaId)];
|
|
2895
2957
|
return this.executeBatch(ids, buildBatchMediaQuery, "m");
|
|
2896
2958
|
}
|
|
2897
2959
|
/** Fetch multiple characters in a single API request. */
|
|
@@ -2899,7 +2961,8 @@ var AniListClient = class {
|
|
|
2899
2961
|
if (ids.length === 0) return [];
|
|
2900
2962
|
validateIds(ids, "characterId");
|
|
2901
2963
|
const [singleCharId] = ids;
|
|
2902
|
-
if (ids.length === 1 && singleCharId !== void 0)
|
|
2964
|
+
if (ids.length === 1 && singleCharId !== void 0)
|
|
2965
|
+
return [await getCharacter(this, singleCharId)];
|
|
2903
2966
|
return this.executeBatch(ids, buildBatchCharacterQuery, "c");
|
|
2904
2967
|
}
|
|
2905
2968
|
/** Fetch multiple staff members in a single API request. */
|
|
@@ -2907,7 +2970,7 @@ var AniListClient = class {
|
|
|
2907
2970
|
if (ids.length === 0) return [];
|
|
2908
2971
|
validateIds(ids, "staffId");
|
|
2909
2972
|
const [singleStaffId] = ids;
|
|
2910
|
-
if (ids.length === 1 && singleStaffId !== void 0) return [await
|
|
2973
|
+
if (ids.length === 1 && singleStaffId !== void 0) return [await getStaff(this, singleStaffId)];
|
|
2911
2974
|
return this.executeBatch(ids, buildBatchStaffQuery, "s");
|
|
2912
2975
|
}
|
|
2913
2976
|
/** @internal */
|