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