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/dist/index.mjs CHANGED
@@ -345,16 +345,15 @@ query {
345
345
  isAdult
346
346
  }
347
347
  }`;
348
-
349
- // src/errors/index.ts
350
- var AniListError = class extends Error {
351
- constructor(message, status, errors = []) {
352
- super(message);
353
- this.name = "AniListError";
354
- this.status = status;
355
- this.errors = errors;
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() + "|" + JSON.stringify(variables, Object.keys(variables).sort());
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
- const res = await fetch(url, init);
440
- if (res.status !== 429) {
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["Authorization"] = `Bearer ${options.token}`;
545
+ this.headers.Authorization = `Bearer ${options.token}`;
468
546
  }
469
- this.cache = new MemoryCache(options.cache);
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.cache.get(cacheKey);
478
- if (cached !== void 0) return cached;
479
- const res = await this.rateLimiter.fetchWithRetry(this.apiUrl, {
480
- method: "POST",
481
- headers: this.headers,
482
- body: JSON.stringify({ query, variables })
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.cache.set(cacheKey, data);
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 variables = {
520
- search: options.query,
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
- const data = await this.request(QUERY_TRENDING, { type, page, perPage });
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 variables = {
559
- search: options.query,
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 variables = {
579
- search: options.query,
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
- const data = await this.request(QUERY_AIRING_SCHEDULE, variables);
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
- const variables = {
654
- page: options.page ?? 1,
655
- perPage: options.perPage ?? 20
656
- };
657
- const data = await this.request(QUERY_RECENT_CHAPTERS, variables);
658
- return { pageInfo: data.Page.pageInfo, results: data.Page.media };
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
- const variables = {
676
- type: options.type,
677
- sort: options.sort ?? ["POPULARITY_DESC"],
678
- page: options.page ?? 1,
679
- perPage: options.perPage ?? 20
680
- };
681
- const data = await this.request(QUERY_PLANNING, variables);
682
- return { pageInfo: data.Page.pageInfo, results: data.Page.media };
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
- const variables = {
734
- season: options.season,
735
- seasonYear: options.seasonYear,
736
- type: options.type ?? "ANIME",
737
- sort: options.sort ?? ["POPULARITY_DESC"],
738
- page: options.page ?? 1,
739
- perPage: options.perPage ?? 20
740
- };
741
- const data = await this.request(QUERY_MEDIA_BY_SEASON, variables);
742
- return { pageInfo: data.Page.pageInfo, results: data.Page.media };
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
- const variables = {
773
- userId: options.userId,
774
- userName: options.userName,
775
- type: options.type,
776
- status: options.status,
777
- sort: options.sort,
778
- page: options.page ?? 1,
779
- perPage: options.perPage ?? 20
780
- };
781
- const data = await this.request(QUERY_USER_MEDIA_LIST, variables);
782
- return { pageInfo: data.Page.pageInfo, results: data.Page.mediaList };
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
- const variables = {
808
- search: options.query,
809
- page: options.page ?? 1,
810
- perPage: options.perPage ?? 20
811
- };
812
- const data = await this.request(QUERY_STUDIO_SEARCH, variables);
813
- return { pageInfo: data.Page.pageInfo, results: data.Page.studios };
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 = Infinity) {
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.cache.clear();
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.cache.size;
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