ani-client 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -296
- package/dist/index.d.mts +170 -11
- package/dist/index.d.ts +170 -11
- package/dist/index.js +347 -115
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +347 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -15
package/dist/index.mjs
CHANGED
|
@@ -345,16 +345,15 @@ query {
|
|
|
345
345
|
isAdult
|
|
346
346
|
}
|
|
347
347
|
}`;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
};
|
|
348
|
+
function buildBatchQuery(ids, typeName, fields, prefix) {
|
|
349
|
+
const aliases = ids.map((id, i) => `${prefix}${i}: ${typeName}(id: ${id}) { ${fields} }`).join("\n ");
|
|
350
|
+
return `query {
|
|
351
|
+
${aliases}
|
|
352
|
+
}`;
|
|
353
|
+
}
|
|
354
|
+
var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS, "m");
|
|
355
|
+
var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
|
|
356
|
+
var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
|
|
358
357
|
|
|
359
358
|
// src/cache/index.ts
|
|
360
359
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -367,7 +366,7 @@ var MemoryCache = class {
|
|
|
367
366
|
}
|
|
368
367
|
/** Build a deterministic cache key from a query + variables pair. */
|
|
369
368
|
static key(query, variables) {
|
|
370
|
-
return query.trim()
|
|
369
|
+
return `${query.trim()}|${JSON.stringify(variables, Object.keys(variables).sort())}`;
|
|
371
370
|
}
|
|
372
371
|
/** Retrieve a cached value, or `undefined` if missing / expired. */
|
|
373
372
|
get(key) {
|
|
@@ -378,11 +377,14 @@ var MemoryCache = class {
|
|
|
378
377
|
this.store.delete(key);
|
|
379
378
|
return void 0;
|
|
380
379
|
}
|
|
380
|
+
this.store.delete(key);
|
|
381
|
+
this.store.set(key, entry);
|
|
381
382
|
return entry.data;
|
|
382
383
|
}
|
|
383
384
|
/** Store a value in the cache. */
|
|
384
385
|
set(key, data) {
|
|
385
386
|
if (!this.enabled) return;
|
|
387
|
+
this.store.delete(key);
|
|
386
388
|
if (this.maxSize > 0 && this.store.size >= this.maxSize) {
|
|
387
389
|
const firstKey = this.store.keys().next().value;
|
|
388
390
|
if (firstKey !== void 0) this.store.delete(firstKey);
|
|
@@ -401,6 +403,37 @@ var MemoryCache = class {
|
|
|
401
403
|
get size() {
|
|
402
404
|
return this.store.size;
|
|
403
405
|
}
|
|
406
|
+
/** Return an iterator over all cache keys. */
|
|
407
|
+
keys() {
|
|
408
|
+
return this.store.keys();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Remove all entries whose key matches the given pattern.
|
|
412
|
+
*
|
|
413
|
+
* @param pattern — A string (converted to RegExp) or RegExp.
|
|
414
|
+
* @returns Number of entries removed.
|
|
415
|
+
*/
|
|
416
|
+
invalidate(pattern) {
|
|
417
|
+
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
418
|
+
let count = 0;
|
|
419
|
+
for (const key of [...this.store.keys()]) {
|
|
420
|
+
if (regex.test(key)) {
|
|
421
|
+
this.store.delete(key);
|
|
422
|
+
count++;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return count;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// src/errors/index.ts
|
|
430
|
+
var AniListError = class extends Error {
|
|
431
|
+
constructor(message, status, errors = []) {
|
|
432
|
+
super(message);
|
|
433
|
+
this.name = "AniListError";
|
|
434
|
+
this.status = status;
|
|
435
|
+
this.errors = errors;
|
|
436
|
+
}
|
|
404
437
|
};
|
|
405
438
|
|
|
406
439
|
// src/rate-limiter/index.ts
|
|
@@ -413,6 +446,8 @@ var RateLimiter = class {
|
|
|
413
446
|
this.maxRetries = options.maxRetries ?? 3;
|
|
414
447
|
this.retryDelayMs = options.retryDelayMs ?? 2e3;
|
|
415
448
|
this.enabled = options.enabled ?? true;
|
|
449
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
450
|
+
this.retryOnNetworkError = options.retryOnNetworkError ?? true;
|
|
416
451
|
}
|
|
417
452
|
/**
|
|
418
453
|
* Wait until it's safe to make a request (respects rate limit window).
|
|
@@ -430,66 +465,141 @@ var RateLimiter = class {
|
|
|
430
465
|
this.timestamps.push(Date.now());
|
|
431
466
|
}
|
|
432
467
|
/**
|
|
433
|
-
* Execute a fetch with automatic retry on 429 responses.
|
|
468
|
+
* Execute a fetch with automatic retry on 429 responses and network errors.
|
|
434
469
|
*/
|
|
435
|
-
async fetchWithRetry(url, init) {
|
|
470
|
+
async fetchWithRetry(url, init, hooks) {
|
|
436
471
|
await this.acquire();
|
|
437
472
|
let lastResponse;
|
|
473
|
+
let lastError;
|
|
438
474
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return res;
|
|
475
|
+
try {
|
|
476
|
+
const res = await this.fetchWithTimeout(url, init);
|
|
477
|
+
if (res.status !== 429) return res;
|
|
478
|
+
lastResponse = res;
|
|
479
|
+
if (attempt === this.maxRetries) break;
|
|
480
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
481
|
+
const delayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : this.retryDelayMs * (attempt + 1);
|
|
482
|
+
hooks?.onRateLimit?.(delayMs);
|
|
483
|
+
hooks?.onRetry?.(attempt + 1, "HTTP 429", delayMs);
|
|
484
|
+
await this.sleep(delayMs);
|
|
485
|
+
await this.acquire();
|
|
486
|
+
} catch (err) {
|
|
487
|
+
lastError = err;
|
|
488
|
+
if (this.retryOnNetworkError && isNetworkError(err) && attempt < this.maxRetries) {
|
|
489
|
+
const delayMs = this.retryDelayMs * (attempt + 1);
|
|
490
|
+
hooks?.onRetry?.(attempt + 1, `Network error: ${err.message}`, delayMs);
|
|
491
|
+
await this.sleep(delayMs);
|
|
492
|
+
await this.acquire();
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
throw err;
|
|
442
496
|
}
|
|
443
|
-
lastResponse = res;
|
|
444
|
-
if (attempt === this.maxRetries) break;
|
|
445
|
-
const retryAfter = res.headers.get("Retry-After");
|
|
446
|
-
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : this.retryDelayMs * (attempt + 1);
|
|
447
|
-
await this.sleep(delayMs);
|
|
448
|
-
await this.acquire();
|
|
449
497
|
}
|
|
450
|
-
return lastResponse;
|
|
498
|
+
if (lastResponse) return lastResponse;
|
|
499
|
+
throw lastError;
|
|
500
|
+
}
|
|
501
|
+
/** @internal */
|
|
502
|
+
async fetchWithTimeout(url, init) {
|
|
503
|
+
if (this.timeoutMs <= 0) return fetch(url, init);
|
|
504
|
+
const controller = new AbortController();
|
|
505
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
506
|
+
try {
|
|
507
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
508
|
+
} finally {
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
}
|
|
451
511
|
}
|
|
452
512
|
sleep(ms) {
|
|
453
513
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
454
514
|
}
|
|
455
515
|
};
|
|
516
|
+
var RETRYABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
517
|
+
"ECONNRESET",
|
|
518
|
+
"ECONNREFUSED",
|
|
519
|
+
"ETIMEDOUT",
|
|
520
|
+
"ENOTFOUND",
|
|
521
|
+
"EAI_AGAIN",
|
|
522
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
523
|
+
"UND_ERR_SOCKET"
|
|
524
|
+
]);
|
|
525
|
+
function isNetworkError(err) {
|
|
526
|
+
if (err instanceof TypeError && err.message === "fetch failed") return true;
|
|
527
|
+
const code = err?.code;
|
|
528
|
+
if (code && RETRYABLE_NETWORK_CODES.has(code)) return true;
|
|
529
|
+
const cause = err?.cause?.code;
|
|
530
|
+
if (cause && RETRYABLE_NETWORK_CODES.has(cause)) return true;
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
456
533
|
|
|
457
534
|
// src/client/index.ts
|
|
458
535
|
var DEFAULT_API_URL = "https://graphql.anilist.co";
|
|
459
536
|
var AniListClient = class {
|
|
460
537
|
constructor(options = {}) {
|
|
538
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
461
539
|
this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
|
|
462
540
|
this.headers = {
|
|
463
541
|
"Content-Type": "application/json",
|
|
464
542
|
Accept: "application/json"
|
|
465
543
|
};
|
|
466
544
|
if (options.token) {
|
|
467
|
-
this.headers
|
|
545
|
+
this.headers.Authorization = `Bearer ${options.token}`;
|
|
468
546
|
}
|
|
469
|
-
this.
|
|
547
|
+
this.cacheAdapter = options.cacheAdapter ?? new MemoryCache(options.cache);
|
|
470
548
|
this.rateLimiter = new RateLimiter(options.rateLimit);
|
|
549
|
+
this.hooks = options.hooks ?? {};
|
|
471
550
|
}
|
|
472
551
|
/**
|
|
473
552
|
* @internal
|
|
474
553
|
*/
|
|
475
554
|
async request(query, variables = {}) {
|
|
476
555
|
const cacheKey = MemoryCache.key(query, variables);
|
|
477
|
-
const cached = this.
|
|
478
|
-
if (cached !== void 0)
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
556
|
+
const cached = await this.cacheAdapter.get(cacheKey);
|
|
557
|
+
if (cached !== void 0) {
|
|
558
|
+
this.hooks.onCacheHit?.(cacheKey);
|
|
559
|
+
this.hooks.onResponse?.(query, 0, true);
|
|
560
|
+
return cached;
|
|
561
|
+
}
|
|
562
|
+
const existing = this.inFlight.get(cacheKey);
|
|
563
|
+
if (existing) return existing;
|
|
564
|
+
const promise = this.executeRequest(query, variables, cacheKey);
|
|
565
|
+
this.inFlight.set(cacheKey, promise);
|
|
566
|
+
try {
|
|
567
|
+
return await promise;
|
|
568
|
+
} finally {
|
|
569
|
+
this.inFlight.delete(cacheKey);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/** @internal */
|
|
573
|
+
async executeRequest(query, variables, cacheKey) {
|
|
574
|
+
const start = Date.now();
|
|
575
|
+
this.hooks.onRequest?.(query, variables);
|
|
576
|
+
const res = await this.rateLimiter.fetchWithRetry(
|
|
577
|
+
this.apiUrl,
|
|
578
|
+
{
|
|
579
|
+
method: "POST",
|
|
580
|
+
headers: this.headers,
|
|
581
|
+
body: JSON.stringify({ query, variables })
|
|
582
|
+
},
|
|
583
|
+
{ onRetry: this.hooks.onRetry, onRateLimit: this.hooks.onRateLimit }
|
|
584
|
+
);
|
|
484
585
|
const json = await res.json();
|
|
485
586
|
if (!res.ok || json.errors) {
|
|
486
587
|
const message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
|
|
487
588
|
throw new AniListError(message, res.status, json.errors ?? []);
|
|
488
589
|
}
|
|
489
590
|
const data = json.data;
|
|
490
|
-
this.
|
|
591
|
+
await this.cacheAdapter.set(cacheKey, data);
|
|
592
|
+
this.hooks.onResponse?.(query, Date.now() - start, false);
|
|
491
593
|
return data;
|
|
492
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* @internal
|
|
597
|
+
* Shorthand for paginated queries that follow the `Page { pageInfo, <field>[] }` pattern.
|
|
598
|
+
*/
|
|
599
|
+
async pagedRequest(query, variables, field) {
|
|
600
|
+
const data = await this.request(query, variables);
|
|
601
|
+
return { pageInfo: data.Page.pageInfo, results: data.Page[field] };
|
|
602
|
+
}
|
|
493
603
|
/**
|
|
494
604
|
* Fetch a single media entry by its AniList ID.
|
|
495
605
|
*
|
|
@@ -516,22 +626,8 @@ var AniListClient = class {
|
|
|
516
626
|
* ```
|
|
517
627
|
*/
|
|
518
628
|
async searchMedia(options = {}) {
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
type: options.type,
|
|
522
|
-
format: options.format,
|
|
523
|
-
status: options.status,
|
|
524
|
-
season: options.season,
|
|
525
|
-
seasonYear: options.seasonYear,
|
|
526
|
-
genre: options.genre,
|
|
527
|
-
tag: options.tag,
|
|
528
|
-
isAdult: options.isAdult,
|
|
529
|
-
sort: options.sort,
|
|
530
|
-
page: options.page ?? 1,
|
|
531
|
-
perPage: options.perPage ?? 20
|
|
532
|
-
};
|
|
533
|
-
const data = await this.request(QUERY_MEDIA_SEARCH, variables);
|
|
534
|
-
return { pageInfo: data.Page.pageInfo, results: data.Page.media };
|
|
629
|
+
const { query: search, page = 1, perPage = 20, ...filters } = options;
|
|
630
|
+
return this.pagedRequest(QUERY_MEDIA_SEARCH, { search, ...filters, page, perPage }, "media");
|
|
535
631
|
}
|
|
536
632
|
/**
|
|
537
633
|
* Get currently trending anime or manga.
|
|
@@ -541,8 +637,7 @@ var AniListClient = class {
|
|
|
541
637
|
* @param perPage - Results per page (default 20, max 50)
|
|
542
638
|
*/
|
|
543
639
|
async getTrending(type = "ANIME", page = 1, perPage = 20) {
|
|
544
|
-
|
|
545
|
-
return { pageInfo: data.Page.pageInfo, results: data.Page.media };
|
|
640
|
+
return this.pagedRequest(QUERY_TRENDING, { type, page, perPage }, "media");
|
|
546
641
|
}
|
|
547
642
|
/**
|
|
548
643
|
* Fetch a character by AniList ID.
|
|
@@ -555,14 +650,8 @@ var AniListClient = class {
|
|
|
555
650
|
* Search for characters by name.
|
|
556
651
|
*/
|
|
557
652
|
async searchCharacters(options = {}) {
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
sort: options.sort,
|
|
561
|
-
page: options.page ?? 1,
|
|
562
|
-
perPage: options.perPage ?? 20
|
|
563
|
-
};
|
|
564
|
-
const data = await this.request(QUERY_CHARACTER_SEARCH, variables);
|
|
565
|
-
return { pageInfo: data.Page.pageInfo, results: data.Page.characters };
|
|
653
|
+
const { query: search, page = 1, perPage = 20, ...rest } = options;
|
|
654
|
+
return this.pagedRequest(QUERY_CHARACTER_SEARCH, { search, ...rest, page, perPage }, "characters");
|
|
566
655
|
}
|
|
567
656
|
/**
|
|
568
657
|
* Fetch a staff member by AniList ID.
|
|
@@ -575,13 +664,8 @@ var AniListClient = class {
|
|
|
575
664
|
* Search for staff (voice actors, directors, etc.).
|
|
576
665
|
*/
|
|
577
666
|
async searchStaff(options = {}) {
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
page: options.page ?? 1,
|
|
581
|
-
perPage: options.perPage ?? 20
|
|
582
|
-
};
|
|
583
|
-
const data = await this.request(QUERY_STAFF_SEARCH, variables);
|
|
584
|
-
return { pageInfo: data.Page.pageInfo, results: data.Page.staff };
|
|
667
|
+
const { query: search, page = 1, perPage = 20 } = options;
|
|
668
|
+
return this.pagedRequest(QUERY_STAFF_SEARCH, { search, page, perPage }, "staff");
|
|
585
669
|
}
|
|
586
670
|
/**
|
|
587
671
|
* Fetch a user by AniList ID.
|
|
@@ -632,8 +716,7 @@ var AniListClient = class {
|
|
|
632
716
|
page: options.page ?? 1,
|
|
633
717
|
perPage: options.perPage ?? 20
|
|
634
718
|
};
|
|
635
|
-
|
|
636
|
-
return { pageInfo: data.Page.pageInfo, results: data.Page.airingSchedules };
|
|
719
|
+
return this.pagedRequest(QUERY_AIRING_SCHEDULE, variables, "airingSchedules");
|
|
637
720
|
}
|
|
638
721
|
/**
|
|
639
722
|
* Get manga that are currently releasing, sorted by most recently updated.
|
|
@@ -650,12 +733,14 @@ var AniListClient = class {
|
|
|
650
733
|
* ```
|
|
651
734
|
*/
|
|
652
735
|
async getAiredChapters(options = {}) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
736
|
+
return this.pagedRequest(
|
|
737
|
+
QUERY_RECENT_CHAPTERS,
|
|
738
|
+
{
|
|
739
|
+
page: options.page ?? 1,
|
|
740
|
+
perPage: options.perPage ?? 20
|
|
741
|
+
},
|
|
742
|
+
"media"
|
|
743
|
+
);
|
|
659
744
|
}
|
|
660
745
|
/**
|
|
661
746
|
* Get upcoming (not yet released) anime and/or manga, sorted by popularity.
|
|
@@ -672,14 +757,16 @@ var AniListClient = class {
|
|
|
672
757
|
* ```
|
|
673
758
|
*/
|
|
674
759
|
async getPlanning(options = {}) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
760
|
+
return this.pagedRequest(
|
|
761
|
+
QUERY_PLANNING,
|
|
762
|
+
{
|
|
763
|
+
type: options.type,
|
|
764
|
+
sort: options.sort ?? ["POPULARITY_DESC"],
|
|
765
|
+
page: options.page ?? 1,
|
|
766
|
+
perPage: options.perPage ?? 20
|
|
767
|
+
},
|
|
768
|
+
"media"
|
|
769
|
+
);
|
|
683
770
|
}
|
|
684
771
|
/**
|
|
685
772
|
* Get recommendations for a specific media.
|
|
@@ -730,16 +817,18 @@ var AniListClient = class {
|
|
|
730
817
|
* ```
|
|
731
818
|
*/
|
|
732
819
|
async getMediaBySeason(options) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
820
|
+
return this.pagedRequest(
|
|
821
|
+
QUERY_MEDIA_BY_SEASON,
|
|
822
|
+
{
|
|
823
|
+
season: options.season,
|
|
824
|
+
seasonYear: options.seasonYear,
|
|
825
|
+
type: options.type ?? "ANIME",
|
|
826
|
+
sort: options.sort ?? ["POPULARITY_DESC"],
|
|
827
|
+
page: options.page ?? 1,
|
|
828
|
+
perPage: options.perPage ?? 20
|
|
829
|
+
},
|
|
830
|
+
"media"
|
|
831
|
+
);
|
|
743
832
|
}
|
|
744
833
|
/**
|
|
745
834
|
* Get a user's anime or manga list.
|
|
@@ -769,17 +858,19 @@ var AniListClient = class {
|
|
|
769
858
|
if (!options.userId && !options.userName) {
|
|
770
859
|
throw new Error("Either userId or userName must be provided");
|
|
771
860
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
861
|
+
return this.pagedRequest(
|
|
862
|
+
QUERY_USER_MEDIA_LIST,
|
|
863
|
+
{
|
|
864
|
+
userId: options.userId,
|
|
865
|
+
userName: options.userName,
|
|
866
|
+
type: options.type,
|
|
867
|
+
status: options.status,
|
|
868
|
+
sort: options.sort,
|
|
869
|
+
page: options.page ?? 1,
|
|
870
|
+
perPage: options.perPage ?? 20
|
|
871
|
+
},
|
|
872
|
+
"mediaList"
|
|
873
|
+
);
|
|
783
874
|
}
|
|
784
875
|
/**
|
|
785
876
|
* Fetch a studio by its AniList ID.
|
|
@@ -804,13 +895,15 @@ var AniListClient = class {
|
|
|
804
895
|
* ```
|
|
805
896
|
*/
|
|
806
897
|
async searchStudios(options = {}) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
898
|
+
return this.pagedRequest(
|
|
899
|
+
QUERY_STUDIO_SEARCH,
|
|
900
|
+
{
|
|
901
|
+
search: options.query,
|
|
902
|
+
page: options.page ?? 1,
|
|
903
|
+
perPage: options.perPage ?? 20
|
|
904
|
+
},
|
|
905
|
+
"studios"
|
|
906
|
+
);
|
|
814
907
|
}
|
|
815
908
|
/**
|
|
816
909
|
* Get all available genres on AniList.
|
|
@@ -858,7 +951,7 @@ var AniListClient = class {
|
|
|
858
951
|
* }
|
|
859
952
|
* ```
|
|
860
953
|
*/
|
|
861
|
-
async *paginate(fetchPage, maxPages =
|
|
954
|
+
async *paginate(fetchPage, maxPages = Number.POSITIVE_INFINITY) {
|
|
862
955
|
let page = 1;
|
|
863
956
|
let hasNext = true;
|
|
864
957
|
while (hasNext && page <= maxPages) {
|
|
@@ -870,17 +963,155 @@ var AniListClient = class {
|
|
|
870
963
|
page++;
|
|
871
964
|
}
|
|
872
965
|
}
|
|
966
|
+
// ── Batch queries ──
|
|
967
|
+
/**
|
|
968
|
+
* Fetch multiple media entries in a single API request.
|
|
969
|
+
* Uses GraphQL aliases to batch up to 50 IDs per call.
|
|
970
|
+
*
|
|
971
|
+
* @param ids - Array of AniList media IDs
|
|
972
|
+
* @returns Array of media objects (same order as input IDs)
|
|
973
|
+
*/
|
|
974
|
+
async getMediaBatch(ids) {
|
|
975
|
+
if (ids.length === 0) return [];
|
|
976
|
+
if (ids.length === 1) return [await this.getMedia(ids[0])];
|
|
977
|
+
return this.executeBatch(ids, buildBatchMediaQuery, "m");
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Fetch multiple characters in a single API request.
|
|
981
|
+
*
|
|
982
|
+
* @param ids - Array of AniList character IDs
|
|
983
|
+
* @returns Array of character objects (same order as input IDs)
|
|
984
|
+
*/
|
|
985
|
+
async getCharacterBatch(ids) {
|
|
986
|
+
if (ids.length === 0) return [];
|
|
987
|
+
if (ids.length === 1) return [await this.getCharacter(ids[0])];
|
|
988
|
+
return this.executeBatch(ids, buildBatchCharacterQuery, "c");
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Fetch multiple staff members in a single API request.
|
|
992
|
+
*
|
|
993
|
+
* @param ids - Array of AniList staff IDs
|
|
994
|
+
* @returns Array of staff objects (same order as input IDs)
|
|
995
|
+
*/
|
|
996
|
+
async getStaffBatch(ids) {
|
|
997
|
+
if (ids.length === 0) return [];
|
|
998
|
+
if (ids.length === 1) return [await this.getStaff(ids[0])];
|
|
999
|
+
return this.executeBatch(ids, buildBatchStaffQuery, "s");
|
|
1000
|
+
}
|
|
1001
|
+
/** @internal */
|
|
1002
|
+
async executeBatch(ids, buildQuery, prefix) {
|
|
1003
|
+
const chunks = this.chunk(ids, 50);
|
|
1004
|
+
const results = [];
|
|
1005
|
+
for (const chunk of chunks) {
|
|
1006
|
+
const query = buildQuery(chunk);
|
|
1007
|
+
const data = await this.request(query);
|
|
1008
|
+
results.push(...chunk.map((_, i) => data[`${prefix}${i}`]));
|
|
1009
|
+
}
|
|
1010
|
+
return results;
|
|
1011
|
+
}
|
|
1012
|
+
/** @internal */
|
|
1013
|
+
chunk(arr, size) {
|
|
1014
|
+
const chunks = [];
|
|
1015
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
1016
|
+
chunks.push(arr.slice(i, i + size));
|
|
1017
|
+
}
|
|
1018
|
+
return chunks;
|
|
1019
|
+
}
|
|
1020
|
+
// ── Cache management ──
|
|
873
1021
|
/**
|
|
874
1022
|
* Clear the entire response cache.
|
|
875
1023
|
*/
|
|
876
|
-
clearCache() {
|
|
877
|
-
this.
|
|
1024
|
+
async clearCache() {
|
|
1025
|
+
await this.cacheAdapter.clear();
|
|
878
1026
|
}
|
|
879
1027
|
/**
|
|
880
|
-
* Number of entries currently in the cache.
|
|
1028
|
+
* Number of entries currently in the cache (sync).
|
|
1029
|
+
* For async adapters like Redis, this may be approximate.
|
|
881
1030
|
*/
|
|
882
1031
|
get cacheSize() {
|
|
883
|
-
return this.
|
|
1032
|
+
return this.cacheAdapter.size;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Remove cache entries whose key matches the given pattern.
|
|
1036
|
+
*
|
|
1037
|
+
* @param pattern — A string (converted to RegExp) or RegExp
|
|
1038
|
+
* @returns Number of entries removed
|
|
1039
|
+
*/
|
|
1040
|
+
async invalidateCache(pattern) {
|
|
1041
|
+
if (this.cacheAdapter.invalidate) {
|
|
1042
|
+
return this.cacheAdapter.invalidate(pattern);
|
|
1043
|
+
}
|
|
1044
|
+
const allKeys = await this.cacheAdapter.keys();
|
|
1045
|
+
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
1046
|
+
let count = 0;
|
|
1047
|
+
for (const key of allKeys) {
|
|
1048
|
+
if (regex.test(key)) {
|
|
1049
|
+
await this.cacheAdapter.delete(key);
|
|
1050
|
+
count++;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return count;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// src/cache/redis.ts
|
|
1058
|
+
var RedisCache = class {
|
|
1059
|
+
constructor(options) {
|
|
1060
|
+
this.client = options.client;
|
|
1061
|
+
this.prefix = options.prefix ?? "ani:";
|
|
1062
|
+
this.ttl = options.ttl ?? 86400;
|
|
1063
|
+
}
|
|
1064
|
+
prefixedKey(key) {
|
|
1065
|
+
return `${this.prefix}${key}`;
|
|
1066
|
+
}
|
|
1067
|
+
async get(key) {
|
|
1068
|
+
const raw = await this.client.get(this.prefixedKey(key));
|
|
1069
|
+
if (raw === null) return void 0;
|
|
1070
|
+
try {
|
|
1071
|
+
return JSON.parse(raw);
|
|
1072
|
+
} catch {
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async set(key, data) {
|
|
1077
|
+
await this.client.set(this.prefixedKey(key), JSON.stringify(data), "EX", this.ttl);
|
|
1078
|
+
}
|
|
1079
|
+
async delete(key) {
|
|
1080
|
+
const count = await this.client.del(this.prefixedKey(key));
|
|
1081
|
+
return count > 0;
|
|
1082
|
+
}
|
|
1083
|
+
async clear() {
|
|
1084
|
+
const keys = await this.client.keys(`${this.prefix}*`);
|
|
1085
|
+
if (keys.length > 0) {
|
|
1086
|
+
await this.client.del(...keys);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Returns -1 because Redis keys can expire silently via TTL.
|
|
1091
|
+
* Use `getSize()` for an accurate count.
|
|
1092
|
+
*/
|
|
1093
|
+
get size() {
|
|
1094
|
+
return -1;
|
|
1095
|
+
}
|
|
1096
|
+
/** Get the actual number of keys with this prefix in Redis. */
|
|
1097
|
+
async getSize() {
|
|
1098
|
+
const keys = await this.client.keys(`${this.prefix}*`);
|
|
1099
|
+
return keys.length;
|
|
1100
|
+
}
|
|
1101
|
+
async keys() {
|
|
1102
|
+
const raw = await this.client.keys(`${this.prefix}*`);
|
|
1103
|
+
return raw.map((k) => k.slice(this.prefix.length));
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Remove all entries whose key matches the given glob pattern.
|
|
1107
|
+
*
|
|
1108
|
+
* @param pattern — A glob pattern (e.g. `"*Media*"`)
|
|
1109
|
+
* @returns Number of entries removed.
|
|
1110
|
+
*/
|
|
1111
|
+
async invalidate(pattern) {
|
|
1112
|
+
const keys = await this.client.keys(`${this.prefix}${pattern}`);
|
|
1113
|
+
if (keys.length === 0) return 0;
|
|
1114
|
+
return this.client.del(...keys);
|
|
884
1115
|
}
|
|
885
1116
|
};
|
|
886
1117
|
|
|
@@ -1022,6 +1253,6 @@ var MediaListSort = /* @__PURE__ */ ((MediaListSort2) => {
|
|
|
1022
1253
|
return MediaListSort2;
|
|
1023
1254
|
})(MediaListSort || {});
|
|
1024
1255
|
|
|
1025
|
-
export { AiringSort, AniListClient, AniListError, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort };
|
|
1256
|
+
export { AiringSort, AniListClient, AniListError, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort, RedisCache };
|
|
1026
1257
|
//# sourceMappingURL=index.mjs.map
|
|
1027
1258
|
//# sourceMappingURL=index.mjs.map
|