ani-client 1.7.0 → 1.8.1

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
@@ -127,28 +127,52 @@ function sortObjectKeys(obj) {
127
127
  // src/cache/index.ts
128
128
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
129
129
  var MemoryCache = class {
130
+ ttl;
131
+ maxSize;
132
+ enabled;
133
+ swrMs;
134
+ store = /* @__PURE__ */ new Map();
135
+ _hits = 0;
136
+ _misses = 0;
137
+ _stales = 0;
130
138
  constructor(options = {}) {
131
- this.store = /* @__PURE__ */ new Map();
132
139
  this.ttl = options.ttl ?? ONE_DAY_MS;
133
140
  this.maxSize = options.maxSize ?? 500;
134
141
  this.enabled = options.enabled ?? true;
142
+ this.swrMs = options.staleWhileRevalidateMs ?? 0;
135
143
  }
136
144
  /** Build a deterministic cache key from a query + variables pair. */
137
145
  static key(query, variables) {
138
146
  const normalized = normalizeQuery(query);
139
147
  return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
140
148
  }
141
- /** Retrieve a cached value, or `undefined` if missing / expired. */
149
+ /**
150
+ * Retrieve a cached value, or `undefined` if missing / expired.
151
+ * With stale-while-revalidate enabled, returns stale data within the grace window
152
+ * and flags it so the caller can refresh in the background.
153
+ */
142
154
  get(key) {
143
155
  if (!this.enabled) return void 0;
144
156
  const entry = this.store.get(key);
145
- if (!entry) return void 0;
146
- if (Date.now() > entry.expiresAt) {
157
+ if (!entry) {
158
+ this._misses++;
159
+ return void 0;
160
+ }
161
+ const now = Date.now();
162
+ if (now > entry.expiresAt) {
163
+ if (this.swrMs > 0 && now <= entry.expiresAt + this.swrMs) {
164
+ this.store.delete(key);
165
+ this.store.set(key, entry);
166
+ this._stales++;
167
+ return entry.data;
168
+ }
147
169
  this.store.delete(key);
170
+ this._misses++;
148
171
  return void 0;
149
172
  }
150
173
  this.store.delete(key);
151
174
  this.store.set(key, entry);
175
+ this._hits++;
152
176
  return entry.data;
153
177
  }
154
178
  /** Store a value in the cache. */
@@ -165,9 +189,12 @@ var MemoryCache = class {
165
189
  delete(key) {
166
190
  return this.store.delete(key);
167
191
  }
168
- /** Clear the entire cache. */
192
+ /** Clear the entire cache and reset statistics. */
169
193
  clear() {
170
194
  this.store.clear();
195
+ this._hits = 0;
196
+ this._misses = 0;
197
+ this._stales = 0;
171
198
  }
172
199
  /** Number of entries currently stored. */
173
200
  get size() {
@@ -177,6 +204,32 @@ var MemoryCache = class {
177
204
  keys() {
178
205
  return [...this.store.keys()];
179
206
  }
207
+ /**
208
+ * Get cache performance statistics.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const cache = new MemoryCache();
213
+ * // ... after some usage ...
214
+ * console.log(cache.stats);
215
+ * // { hits: 42, misses: 8, stales: 0, hitRate: 0.84 }
216
+ * ```
217
+ */
218
+ get stats() {
219
+ const total = this._hits + this._misses + this._stales;
220
+ return {
221
+ hits: this._hits,
222
+ misses: this._misses,
223
+ stales: this._stales,
224
+ hitRate: total === 0 ? Number.NaN : this._hits / total
225
+ };
226
+ }
227
+ /** Reset cache statistics without clearing stored data. */
228
+ resetStats() {
229
+ this._hits = 0;
230
+ this._misses = 0;
231
+ this._stales = 0;
232
+ }
180
233
  /**
181
234
  * Remove all entries whose key matches the given pattern.
182
235
  *
@@ -197,8 +250,101 @@ var MemoryCache = class {
197
250
  }
198
251
  };
199
252
 
253
+ // src/cache/redis.ts
254
+ var RedisCache = class {
255
+ client;
256
+ prefix;
257
+ ttl;
258
+ constructor(options) {
259
+ this.client = options.client;
260
+ this.prefix = options.prefix ?? "ani:";
261
+ this.ttl = options.ttl ?? 86400;
262
+ }
263
+ prefixedKey(key) {
264
+ return `${this.prefix}${key}`;
265
+ }
266
+ async get(key) {
267
+ const raw = await this.client.get(this.prefixedKey(key));
268
+ if (raw === null) return void 0;
269
+ try {
270
+ return JSON.parse(raw);
271
+ } catch {
272
+ return void 0;
273
+ }
274
+ }
275
+ async set(key, data) {
276
+ await this.client.set(this.prefixedKey(key), JSON.stringify(data), "EX", this.ttl);
277
+ }
278
+ async delete(key) {
279
+ const count = await this.client.del(this.prefixedKey(key));
280
+ return count > 0;
281
+ }
282
+ /**
283
+ * Collect keys matching a pattern. Uses SCAN when available, falls back to KEYS.
284
+ *
285
+ * **Warning:** The `KEYS` fallback is O(N) and blocks the Redis server.
286
+ * Provide a client with `scanIterator` support for production use.
287
+ * @internal
288
+ */
289
+ async collectKeys(pattern) {
290
+ if (this.client.scanIterator) {
291
+ const keys = [];
292
+ for await (const key of this.client.scanIterator({ MATCH: pattern, COUNT: 100 })) {
293
+ keys.push(key);
294
+ }
295
+ return keys;
296
+ }
297
+ return this.client.keys(pattern);
298
+ }
299
+ async clear() {
300
+ const keys = await this.collectKeys(`${this.prefix}*`);
301
+ if (keys.length > 0) {
302
+ await this.client.del(...keys);
303
+ }
304
+ }
305
+ /**
306
+ * Get the actual number of keys with this prefix in Redis.
307
+ */
308
+ get size() {
309
+ return this.getSize();
310
+ }
311
+ /** @internal */
312
+ async getSize() {
313
+ const keys = await this.collectKeys(`${this.prefix}*`);
314
+ return keys.length;
315
+ }
316
+ async keys() {
317
+ const raw = await this.collectKeys(`${this.prefix}*`);
318
+ return raw.map((k) => k.slice(this.prefix.length));
319
+ }
320
+ /**
321
+ * Remove all entries whose key matches the given pattern.
322
+ *
323
+ * - **String**: treated as a substring match (e.g. `"Media"` removes all keys containing `"Media"`).
324
+ * - **RegExp**: tested against each key directly.
325
+ *
326
+ * @param pattern — A string (substring match) or RegExp.
327
+ * @returns Number of entries removed.
328
+ */
329
+ async invalidate(pattern) {
330
+ if (typeof pattern === "string") {
331
+ const keys = await this.collectKeys(`${this.prefix}*${pattern}*`);
332
+ if (keys.length === 0) return 0;
333
+ return this.client.del(...keys);
334
+ }
335
+ const allKeys = await this.collectKeys(`${this.prefix}*`);
336
+ const matching = allKeys.filter((k) => pattern.test(k.slice(this.prefix.length)));
337
+ if (matching.length === 0) return 0;
338
+ return this.client.del(...matching);
339
+ }
340
+ };
341
+
200
342
  // src/errors/index.ts
201
343
  var AniListError = class _AniListError extends Error {
344
+ /** HTTP status code returned by the API */
345
+ status;
346
+ /** Raw error body from the API response */
347
+ errors;
202
348
  constructor(message, status, errors = []) {
203
349
  super(message);
204
350
  this.name = "AniListError";
@@ -212,6 +358,32 @@ var AniListError = class _AniListError extends Error {
212
358
  };
213
359
 
214
360
  // src/queries/fragments.ts
361
+ var MEDIA_FIELDS_LIGHT = `
362
+ id
363
+ idMal
364
+ title { romaji english native userPreferred }
365
+ type
366
+ format
367
+ status
368
+ coverImage { large medium color }
369
+ bannerImage
370
+ genres
371
+ averageScore
372
+ popularity
373
+ favourites
374
+ isAdult
375
+ siteUrl
376
+ season
377
+ seasonYear
378
+ episodes
379
+ chapters
380
+ nextAiringEpisode {
381
+ id
382
+ airingAt
383
+ episode
384
+ timeUntilAiring
385
+ }
386
+ `;
215
387
  var MEDIA_FIELDS_BASE = `
216
388
  id
217
389
  idMal
@@ -631,6 +803,12 @@ query ($season: MediaSeason!, $seasonYear: Int!, $type: MediaType, $sort: [Media
631
803
  }
632
804
  }
633
805
  }`;
806
+ var QUERY_MEDIA_BY_MAL_ID = `
807
+ query ($idMal: Int!, $type: MediaType) {
808
+ Media(idMal: $idMal, type: $type) {
809
+ ${MEDIA_FIELDS}
810
+ }
811
+ }`;
634
812
  var QUERY_RECOMMENDATIONS = `
635
813
  query ($mediaId: Int!, $page: Int, $perPage: Int, $sort: [RecommendationSort]) {
636
814
  Media(id: $mediaId) {
@@ -666,143 +844,6 @@ query ($mediaId: Int!, $page: Int, $perPage: Int, $sort: [RecommendationSort]) {
666
844
  }
667
845
  }`;
668
846
 
669
- // src/queries/character.ts
670
- var QUERY_CHARACTER_BY_ID = `
671
- query ($id: Int!) {
672
- Character(id: $id) {
673
- ${CHARACTER_FIELDS}
674
- }
675
- }`;
676
- var QUERY_CHARACTER_BY_ID_WITH_VA = `
677
- query ($id: Int!) {
678
- Character(id: $id) {
679
- ${CHARACTER_FIELDS_WITH_VA}
680
- }
681
- }`;
682
- var QUERY_CHARACTER_SEARCH = `
683
- query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
684
- Page(page: $page, perPage: $perPage) {
685
- pageInfo { total perPage currentPage lastPage hasNextPage }
686
- characters(search: $search, sort: $sort) {
687
- ${CHARACTER_FIELDS}
688
- }
689
- }
690
- }`;
691
- var QUERY_CHARACTER_SEARCH_WITH_VA = `
692
- query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
693
- Page(page: $page, perPage: $perPage) {
694
- pageInfo { total perPage currentPage lastPage hasNextPage }
695
- characters(search: $search, sort: $sort) {
696
- ${CHARACTER_FIELDS_WITH_VA}
697
- }
698
- }
699
- }`;
700
-
701
- // src/queries/staff.ts
702
- var QUERY_STAFF_BY_ID = `
703
- query ($id: Int!) {
704
- Staff(id: $id) {
705
- ${STAFF_FIELDS}
706
- }
707
- }`;
708
- var QUERY_STAFF_BY_ID_WITH_MEDIA = `
709
- query ($id: Int!, $perPage: Int) {
710
- Staff(id: $id) {
711
- ${STAFF_FIELDS}
712
- ${STAFF_MEDIA_FIELDS}
713
- }
714
- }`;
715
- var QUERY_STAFF_SEARCH = `
716
- query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
717
- Page(page: $page, perPage: $perPage) {
718
- pageInfo { total perPage currentPage lastPage hasNextPage }
719
- staff(search: $search, sort: $sort) {
720
- ${STAFF_FIELDS}
721
- }
722
- }
723
- }`;
724
-
725
- // src/queries/user.ts
726
- var QUERY_USER_BY_ID = `
727
- query ($id: Int!) {
728
- User(id: $id) {
729
- ${USER_FIELDS}
730
- }
731
- }`;
732
- var QUERY_USER_BY_NAME = `
733
- query ($name: String!) {
734
- User(name: $name) {
735
- ${USER_FIELDS}
736
- }
737
- }`;
738
- var QUERY_USER_SEARCH = `
739
- query ($search: String, $sort: [UserSort], $page: Int, $perPage: Int) {
740
- Page(page: $page, perPage: $perPage) {
741
- pageInfo { total perPage currentPage lastPage hasNextPage }
742
- users(search: $search, sort: $sort) {
743
- ${USER_FIELDS}
744
- }
745
- }
746
- }`;
747
- var QUERY_USER_MEDIA_LIST = `
748
- query ($userId: Int, $userName: String, $type: MediaType!, $status: MediaListStatus, $sort: [MediaListSort], $page: Int, $perPage: Int) {
749
- Page(page: $page, perPage: $perPage) {
750
- pageInfo { total perPage currentPage lastPage hasNextPage }
751
- mediaList(userId: $userId, userName: $userName, type: $type, status: $status, sort: $sort) {
752
- ${MEDIA_LIST_FIELDS}
753
- }
754
- }
755
- }`;
756
- var QUERY_USER_FAVORITES_BY_ID = `
757
- query ($id: Int!) {
758
- User(id: $id) {
759
- id
760
- name
761
- ${USER_FAVORITES_FIELDS}
762
- }
763
- }`;
764
- var QUERY_USER_FAVORITES_BY_NAME = `
765
- query ($name: String!) {
766
- User(name: $name) {
767
- id
768
- name
769
- ${USER_FAVORITES_FIELDS}
770
- }
771
- }`;
772
-
773
- // src/queries/studio.ts
774
- var QUERY_STUDIO_BY_ID = `
775
- query ($id: Int!) {
776
- Studio(id: $id) {
777
- ${STUDIO_FIELDS}
778
- }
779
- }`;
780
- var QUERY_STUDIO_SEARCH = `
781
- query ($search: String, $sort: [StudioSort], $page: Int, $perPage: Int) {
782
- Page(page: $page, perPage: $perPage) {
783
- pageInfo { total perPage currentPage lastPage hasNextPage }
784
- studios(search: $search, sort: $sort) {
785
- ${STUDIO_FIELDS}
786
- }
787
- }
788
- }`;
789
-
790
- // src/queries/metadata.ts
791
- var QUERY_GENRES = `
792
- query {
793
- GenreCollection
794
- }`;
795
- var QUERY_TAGS = `
796
- query {
797
- MediaTagCollection {
798
- id
799
- name
800
- description
801
- category
802
- isAdult
803
- }
804
- }`;
805
-
806
847
  // src/queries/builders.ts
807
848
  function buildMediaByIdQuery(include) {
808
849
  if (!include) return QUERY_MEDIA_BY_ID;
@@ -908,6 +949,115 @@ var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS,
908
949
  var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
909
950
  var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
910
951
 
952
+ // src/queries/character.ts
953
+ var QUERY_CHARACTER_BY_ID = `
954
+ query ($id: Int!) {
955
+ Character(id: $id) {
956
+ ${CHARACTER_FIELDS}
957
+ }
958
+ }`;
959
+ var QUERY_CHARACTER_BY_ID_WITH_VA = `
960
+ query ($id: Int!) {
961
+ Character(id: $id) {
962
+ ${CHARACTER_FIELDS_WITH_VA}
963
+ }
964
+ }`;
965
+ var QUERY_CHARACTER_SEARCH = `
966
+ query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
967
+ Page(page: $page, perPage: $perPage) {
968
+ pageInfo { total perPage currentPage lastPage hasNextPage }
969
+ characters(search: $search, sort: $sort) {
970
+ ${CHARACTER_FIELDS}
971
+ }
972
+ }
973
+ }`;
974
+ var QUERY_CHARACTER_SEARCH_WITH_VA = `
975
+ query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
976
+ Page(page: $page, perPage: $perPage) {
977
+ pageInfo { total perPage currentPage lastPage hasNextPage }
978
+ characters(search: $search, sort: $sort) {
979
+ ${CHARACTER_FIELDS_WITH_VA}
980
+ }
981
+ }
982
+ }`;
983
+
984
+ // src/queries/metadata.ts
985
+ var QUERY_GENRES = `
986
+ query {
987
+ GenreCollection
988
+ }`;
989
+ var QUERY_TAGS = `
990
+ query {
991
+ MediaTagCollection {
992
+ id
993
+ name
994
+ description
995
+ category
996
+ isAdult
997
+ }
998
+ }`;
999
+
1000
+ // src/queries/staff.ts
1001
+ var QUERY_STAFF_BY_ID = `
1002
+ query ($id: Int!) {
1003
+ Staff(id: $id) {
1004
+ ${STAFF_FIELDS}
1005
+ }
1006
+ }`;
1007
+ var QUERY_STAFF_BY_ID_WITH_MEDIA = `
1008
+ query ($id: Int!, $perPage: Int) {
1009
+ Staff(id: $id) {
1010
+ ${STAFF_FIELDS}
1011
+ ${STAFF_MEDIA_FIELDS}
1012
+ }
1013
+ }`;
1014
+ var QUERY_STAFF_SEARCH = `
1015
+ query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
1016
+ Page(page: $page, perPage: $perPage) {
1017
+ pageInfo { total perPage currentPage lastPage hasNextPage }
1018
+ staff(search: $search, sort: $sort) {
1019
+ ${STAFF_FIELDS}
1020
+ }
1021
+ }
1022
+ }`;
1023
+
1024
+ // src/queries/studio.ts
1025
+ var QUERY_STUDIO_BY_ID = `
1026
+ query ($id: Int!) {
1027
+ Studio(id: $id) {
1028
+ ${STUDIO_FIELDS}
1029
+ }
1030
+ }`;
1031
+ function buildStudioByIdQuery(mediaPerPage) {
1032
+ if (mediaPerPage === void 0) return QUERY_STUDIO_BY_ID;
1033
+ const pp = clampPerPage(mediaPerPage);
1034
+ return `
1035
+ query ($id: Int!) {
1036
+ Studio(id: $id) {
1037
+ id
1038
+ name
1039
+ isAnimationStudio
1040
+ siteUrl
1041
+ favourites
1042
+ media(page: 1, perPage: ${pp}, sort: POPULARITY_DESC) {
1043
+ pageInfo { total perPage currentPage lastPage hasNextPage }
1044
+ nodes {
1045
+ ${MEDIA_FIELDS_LIGHT}
1046
+ }
1047
+ }
1048
+ }
1049
+ }`;
1050
+ }
1051
+ var QUERY_STUDIO_SEARCH = `
1052
+ query ($search: String, $sort: [StudioSort], $page: Int, $perPage: Int) {
1053
+ Page(page: $page, perPage: $perPage) {
1054
+ pageInfo { total perPage currentPage lastPage hasNextPage }
1055
+ studios(search: $search, sort: $sort) {
1056
+ ${STUDIO_FIELDS}
1057
+ }
1058
+ }
1059
+ }`;
1060
+
911
1061
  // src/queries/thread.ts
912
1062
  var QUERY_THREAD_BY_ID = `
913
1063
  query ($id: Int!) {
@@ -925,13 +1075,128 @@ query ($search: String, $mediaCategoryId: Int, $categoryId: Int, $sort: [ThreadS
925
1075
  }
926
1076
  }`;
927
1077
 
1078
+ // src/queries/user.ts
1079
+ var QUERY_USER_BY_ID = `
1080
+ query ($id: Int!) {
1081
+ User(id: $id) {
1082
+ ${USER_FIELDS}
1083
+ }
1084
+ }`;
1085
+ var QUERY_USER_BY_NAME = `
1086
+ query ($name: String!) {
1087
+ User(name: $name) {
1088
+ ${USER_FIELDS}
1089
+ }
1090
+ }`;
1091
+ var QUERY_USER_SEARCH = `
1092
+ query ($search: String, $sort: [UserSort], $page: Int, $perPage: Int) {
1093
+ Page(page: $page, perPage: $perPage) {
1094
+ pageInfo { total perPage currentPage lastPage hasNextPage }
1095
+ users(search: $search, sort: $sort) {
1096
+ ${USER_FIELDS}
1097
+ }
1098
+ }
1099
+ }`;
1100
+ var QUERY_USER_MEDIA_LIST = `
1101
+ query ($userId: Int, $userName: String, $type: MediaType!, $status: MediaListStatus, $sort: [MediaListSort], $page: Int, $perPage: Int) {
1102
+ Page(page: $page, perPage: $perPage) {
1103
+ pageInfo { total perPage currentPage lastPage hasNextPage }
1104
+ mediaList(userId: $userId, userName: $userName, type: $type, status: $status, sort: $sort) {
1105
+ ${MEDIA_LIST_FIELDS}
1106
+ }
1107
+ }
1108
+ }`;
1109
+ var QUERY_USER_FAVORITES_BY_ID = `
1110
+ query ($id: Int!) {
1111
+ User(id: $id) {
1112
+ id
1113
+ name
1114
+ ${USER_FAVORITES_FIELDS}
1115
+ }
1116
+ }`;
1117
+ var QUERY_USER_FAVORITES_BY_NAME = `
1118
+ query ($name: String!) {
1119
+ User(name: $name) {
1120
+ id
1121
+ name
1122
+ ${USER_FAVORITES_FIELDS}
1123
+ }
1124
+ }`;
1125
+ function buildUserFavoritesQuery(idOrName, perPage = 25) {
1126
+ const pp = clampPerPage(perPage);
1127
+ const varDecl = idOrName === "id" ? "$id: Int!" : "$name: String!";
1128
+ const selector = idOrName === "id" ? "id: $id" : "name: $name";
1129
+ return `
1130
+ query (${varDecl}) {
1131
+ User(${selector}) {
1132
+ id
1133
+ name
1134
+ favourites {
1135
+ anime(perPage: ${pp}) {
1136
+ nodes {
1137
+ id
1138
+ title { romaji english native userPreferred }
1139
+ coverImage { large medium }
1140
+ type
1141
+ format
1142
+ siteUrl
1143
+ }
1144
+ }
1145
+ manga(perPage: ${pp}) {
1146
+ nodes {
1147
+ id
1148
+ title { romaji english native userPreferred }
1149
+ coverImage { large medium }
1150
+ type
1151
+ format
1152
+ siteUrl
1153
+ }
1154
+ }
1155
+ characters(perPage: ${pp}) {
1156
+ nodes {
1157
+ id
1158
+ name { full native }
1159
+ image { large medium }
1160
+ siteUrl
1161
+ }
1162
+ }
1163
+ staff(perPage: ${pp}) {
1164
+ nodes {
1165
+ id
1166
+ name { full native }
1167
+ image { large medium }
1168
+ siteUrl
1169
+ }
1170
+ }
1171
+ studios(perPage: ${pp}) {
1172
+ nodes {
1173
+ id
1174
+ name
1175
+ siteUrl
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ }`;
1181
+ }
1182
+
928
1183
  // src/rate-limiter/index.ts
929
1184
  var RateLimiter = class {
1185
+ maxRequests;
1186
+ windowMs;
1187
+ maxRetries;
1188
+ retryDelayMs;
1189
+ enabled;
1190
+ timeoutMs;
1191
+ retryOnNetworkError;
1192
+ retryStrategy;
1193
+ /** @internal — sliding window: circular buffer of timestamps */
1194
+ timestamps;
1195
+ head = 0;
1196
+ count = 0;
1197
+ /** @internal — active sleep timers for cleanup */
1198
+ activeTimers = /* @__PURE__ */ new Set();
930
1199
  constructor(options = {}) {
931
- this.head = 0;
932
- this.count = 0;
933
- /** @internal — active sleep timers for cleanup */
934
- this.activeTimers = /* @__PURE__ */ new Set();
935
1200
  this.maxRequests = options.maxRequests ?? 85;
936
1201
  this.windowMs = options.windowMs ?? 6e4;
937
1202
  this.maxRetries = options.maxRetries ?? 3;
@@ -997,7 +1262,7 @@ var RateLimiter = class {
997
1262
  }
998
1263
  }
999
1264
  if (lastResponse) return lastResponse;
1000
- throw lastError;
1265
+ throw lastError ?? new Error(`Request failed after ${this.maxRetries} retries`);
1001
1266
  }
1002
1267
  /** @internal — Exponential backoff with jitter, capped at 30s (or custom strategy) */
1003
1268
  exponentialDelay(attempt) {
@@ -1072,6 +1337,68 @@ async function searchCharacters(client, options = {}) {
1072
1337
  return client.pagedRequest(gqlQuery, { search, sort, page, perPage: clampPerPage(perPage) }, "characters");
1073
1338
  }
1074
1339
 
1340
+ // src/types/character.ts
1341
+ var CharacterSort = /* @__PURE__ */ ((CharacterSort2) => {
1342
+ CharacterSort2["ID"] = "ID";
1343
+ CharacterSort2["ID_DESC"] = "ID_DESC";
1344
+ CharacterSort2["ROLE"] = "ROLE";
1345
+ CharacterSort2["ROLE_DESC"] = "ROLE_DESC";
1346
+ CharacterSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
1347
+ CharacterSort2["FAVOURITES"] = "FAVOURITES";
1348
+ CharacterSort2["FAVOURITES_DESC"] = "FAVOURITES_DESC";
1349
+ return CharacterSort2;
1350
+ })(CharacterSort || {});
1351
+ var CharacterRole = /* @__PURE__ */ ((CharacterRole2) => {
1352
+ CharacterRole2["MAIN"] = "MAIN";
1353
+ CharacterRole2["SUPPORTING"] = "SUPPORTING";
1354
+ CharacterRole2["BACKGROUND"] = "BACKGROUND";
1355
+ return CharacterRole2;
1356
+ })(CharacterRole || {});
1357
+
1358
+ // src/types/lists.ts
1359
+ var MediaListStatus = /* @__PURE__ */ ((MediaListStatus2) => {
1360
+ MediaListStatus2["CURRENT"] = "CURRENT";
1361
+ MediaListStatus2["PLANNING"] = "PLANNING";
1362
+ MediaListStatus2["COMPLETED"] = "COMPLETED";
1363
+ MediaListStatus2["DROPPED"] = "DROPPED";
1364
+ MediaListStatus2["PAUSED"] = "PAUSED";
1365
+ MediaListStatus2["REPEATING"] = "REPEATING";
1366
+ return MediaListStatus2;
1367
+ })(MediaListStatus || {});
1368
+ var MediaListSort = /* @__PURE__ */ ((MediaListSort2) => {
1369
+ MediaListSort2["MEDIA_ID"] = "MEDIA_ID";
1370
+ MediaListSort2["MEDIA_ID_DESC"] = "MEDIA_ID_DESC";
1371
+ MediaListSort2["SCORE"] = "SCORE";
1372
+ MediaListSort2["SCORE_DESC"] = "SCORE_DESC";
1373
+ MediaListSort2["STATUS"] = "STATUS";
1374
+ MediaListSort2["STATUS_DESC"] = "STATUS_DESC";
1375
+ MediaListSort2["PROGRESS"] = "PROGRESS";
1376
+ MediaListSort2["PROGRESS_DESC"] = "PROGRESS_DESC";
1377
+ MediaListSort2["PROGRESS_VOLUMES"] = "PROGRESS_VOLUMES";
1378
+ MediaListSort2["PROGRESS_VOLUMES_DESC"] = "PROGRESS_VOLUMES_DESC";
1379
+ MediaListSort2["REPEAT"] = "REPEAT";
1380
+ MediaListSort2["REPEAT_DESC"] = "REPEAT_DESC";
1381
+ MediaListSort2["PRIORITY"] = "PRIORITY";
1382
+ MediaListSort2["PRIORITY_DESC"] = "PRIORITY_DESC";
1383
+ MediaListSort2["STARTED_ON"] = "STARTED_ON";
1384
+ MediaListSort2["STARTED_ON_DESC"] = "STARTED_ON_DESC";
1385
+ MediaListSort2["FINISHED_ON"] = "FINISHED_ON";
1386
+ MediaListSort2["FINISHED_ON_DESC"] = "FINISHED_ON_DESC";
1387
+ MediaListSort2["ADDED_TIME"] = "ADDED_TIME";
1388
+ MediaListSort2["ADDED_TIME_DESC"] = "ADDED_TIME_DESC";
1389
+ MediaListSort2["UPDATED_TIME"] = "UPDATED_TIME";
1390
+ MediaListSort2["UPDATED_TIME_DESC"] = "UPDATED_TIME_DESC";
1391
+ MediaListSort2["MEDIA_TITLE_ROMAJI"] = "MEDIA_TITLE_ROMAJI";
1392
+ MediaListSort2["MEDIA_TITLE_ROMAJI_DESC"] = "MEDIA_TITLE_ROMAJI_DESC";
1393
+ MediaListSort2["MEDIA_TITLE_ENGLISH"] = "MEDIA_TITLE_ENGLISH";
1394
+ MediaListSort2["MEDIA_TITLE_ENGLISH_DESC"] = "MEDIA_TITLE_ENGLISH_DESC";
1395
+ MediaListSort2["MEDIA_TITLE_NATIVE"] = "MEDIA_TITLE_NATIVE";
1396
+ MediaListSort2["MEDIA_TITLE_NATIVE_DESC"] = "MEDIA_TITLE_NATIVE_DESC";
1397
+ MediaListSort2["MEDIA_POPULARITY"] = "MEDIA_POPULARITY";
1398
+ MediaListSort2["MEDIA_POPULARITY_DESC"] = "MEDIA_POPULARITY_DESC";
1399
+ return MediaListSort2;
1400
+ })(MediaListSort || {});
1401
+
1075
1402
  // src/types/media.ts
1076
1403
  var MediaType = /* @__PURE__ */ ((MediaType2) => {
1077
1404
  MediaType2["ANIME"] = "ANIME";
@@ -1195,24 +1522,6 @@ var RecommendationSort = /* @__PURE__ */ ((RecommendationSort2) => {
1195
1522
  return RecommendationSort2;
1196
1523
  })(RecommendationSort || {});
1197
1524
 
1198
- // src/types/character.ts
1199
- var CharacterSort = /* @__PURE__ */ ((CharacterSort2) => {
1200
- CharacterSort2["ID"] = "ID";
1201
- CharacterSort2["ID_DESC"] = "ID_DESC";
1202
- CharacterSort2["ROLE"] = "ROLE";
1203
- CharacterSort2["ROLE_DESC"] = "ROLE_DESC";
1204
- CharacterSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
1205
- CharacterSort2["FAVOURITES"] = "FAVOURITES";
1206
- CharacterSort2["FAVOURITES_DESC"] = "FAVOURITES_DESC";
1207
- return CharacterSort2;
1208
- })(CharacterSort || {});
1209
- var CharacterRole = /* @__PURE__ */ ((CharacterRole2) => {
1210
- CharacterRole2["MAIN"] = "MAIN";
1211
- CharacterRole2["SUPPORTING"] = "SUPPORTING";
1212
- CharacterRole2["BACKGROUND"] = "BACKGROUND";
1213
- return CharacterRole2;
1214
- })(CharacterRole || {});
1215
-
1216
1525
  // src/types/staff.ts
1217
1526
  var StaffSort = /* @__PURE__ */ ((StaffSort2) => {
1218
1527
  StaffSort2["ID"] = "ID";
@@ -1228,20 +1537,6 @@ var StaffSort = /* @__PURE__ */ ((StaffSort2) => {
1228
1537
  return StaffSort2;
1229
1538
  })(StaffSort || {});
1230
1539
 
1231
- // src/types/user.ts
1232
- var UserSort = /* @__PURE__ */ ((UserSort2) => {
1233
- UserSort2["ID"] = "ID";
1234
- UserSort2["ID_DESC"] = "ID_DESC";
1235
- UserSort2["USERNAME"] = "USERNAME";
1236
- UserSort2["USERNAME_DESC"] = "USERNAME_DESC";
1237
- UserSort2["WATCHED_TIME"] = "WATCHED_TIME";
1238
- UserSort2["WATCHED_TIME_DESC"] = "WATCHED_TIME_DESC";
1239
- UserSort2["CHAPTERS_READ"] = "CHAPTERS_READ";
1240
- UserSort2["CHAPTERS_READ_DESC"] = "CHAPTERS_READ_DESC";
1241
- UserSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
1242
- return UserSort2;
1243
- })(UserSort || {});
1244
-
1245
1540
  // src/types/studio.ts
1246
1541
  var StudioSort = /* @__PURE__ */ ((StudioSort2) => {
1247
1542
  StudioSort2["ID"] = "ID";
@@ -1254,50 +1549,6 @@ var StudioSort = /* @__PURE__ */ ((StudioSort2) => {
1254
1549
  return StudioSort2;
1255
1550
  })(StudioSort || {});
1256
1551
 
1257
- // src/types/lists.ts
1258
- var MediaListStatus = /* @__PURE__ */ ((MediaListStatus2) => {
1259
- MediaListStatus2["CURRENT"] = "CURRENT";
1260
- MediaListStatus2["PLANNING"] = "PLANNING";
1261
- MediaListStatus2["COMPLETED"] = "COMPLETED";
1262
- MediaListStatus2["DROPPED"] = "DROPPED";
1263
- MediaListStatus2["PAUSED"] = "PAUSED";
1264
- MediaListStatus2["REPEATING"] = "REPEATING";
1265
- return MediaListStatus2;
1266
- })(MediaListStatus || {});
1267
- var MediaListSort = /* @__PURE__ */ ((MediaListSort2) => {
1268
- MediaListSort2["MEDIA_ID"] = "MEDIA_ID";
1269
- MediaListSort2["MEDIA_ID_DESC"] = "MEDIA_ID_DESC";
1270
- MediaListSort2["SCORE"] = "SCORE";
1271
- MediaListSort2["SCORE_DESC"] = "SCORE_DESC";
1272
- MediaListSort2["STATUS"] = "STATUS";
1273
- MediaListSort2["STATUS_DESC"] = "STATUS_DESC";
1274
- MediaListSort2["PROGRESS"] = "PROGRESS";
1275
- MediaListSort2["PROGRESS_DESC"] = "PROGRESS_DESC";
1276
- MediaListSort2["PROGRESS_VOLUMES"] = "PROGRESS_VOLUMES";
1277
- MediaListSort2["PROGRESS_VOLUMES_DESC"] = "PROGRESS_VOLUMES_DESC";
1278
- MediaListSort2["REPEAT"] = "REPEAT";
1279
- MediaListSort2["REPEAT_DESC"] = "REPEAT_DESC";
1280
- MediaListSort2["PRIORITY"] = "PRIORITY";
1281
- MediaListSort2["PRIORITY_DESC"] = "PRIORITY_DESC";
1282
- MediaListSort2["STARTED_ON"] = "STARTED_ON";
1283
- MediaListSort2["STARTED_ON_DESC"] = "STARTED_ON_DESC";
1284
- MediaListSort2["FINISHED_ON"] = "FINISHED_ON";
1285
- MediaListSort2["FINISHED_ON_DESC"] = "FINISHED_ON_DESC";
1286
- MediaListSort2["ADDED_TIME"] = "ADDED_TIME";
1287
- MediaListSort2["ADDED_TIME_DESC"] = "ADDED_TIME_DESC";
1288
- MediaListSort2["UPDATED_TIME"] = "UPDATED_TIME";
1289
- MediaListSort2["UPDATED_TIME_DESC"] = "UPDATED_TIME_DESC";
1290
- MediaListSort2["MEDIA_TITLE_ROMAJI"] = "MEDIA_TITLE_ROMAJI";
1291
- MediaListSort2["MEDIA_TITLE_ROMAJI_DESC"] = "MEDIA_TITLE_ROMAJI_DESC";
1292
- MediaListSort2["MEDIA_TITLE_ENGLISH"] = "MEDIA_TITLE_ENGLISH";
1293
- MediaListSort2["MEDIA_TITLE_ENGLISH_DESC"] = "MEDIA_TITLE_ENGLISH_DESC";
1294
- MediaListSort2["MEDIA_TITLE_NATIVE"] = "MEDIA_TITLE_NATIVE";
1295
- MediaListSort2["MEDIA_TITLE_NATIVE_DESC"] = "MEDIA_TITLE_NATIVE_DESC";
1296
- MediaListSort2["MEDIA_POPULARITY"] = "MEDIA_POPULARITY";
1297
- MediaListSort2["MEDIA_POPULARITY_DESC"] = "MEDIA_POPULARITY_DESC";
1298
- return MediaListSort2;
1299
- })(MediaListSort || {});
1300
-
1301
1552
  // src/types/thread.ts
1302
1553
  var ThreadSort = /* @__PURE__ */ ((ThreadSort2) => {
1303
1554
  ThreadSort2["ID"] = "ID";
@@ -1319,6 +1570,20 @@ var ThreadSort = /* @__PURE__ */ ((ThreadSort2) => {
1319
1570
  return ThreadSort2;
1320
1571
  })(ThreadSort || {});
1321
1572
 
1573
+ // src/types/user.ts
1574
+ var UserSort = /* @__PURE__ */ ((UserSort2) => {
1575
+ UserSort2["ID"] = "ID";
1576
+ UserSort2["ID_DESC"] = "ID_DESC";
1577
+ UserSort2["USERNAME"] = "USERNAME";
1578
+ UserSort2["USERNAME_DESC"] = "USERNAME_DESC";
1579
+ UserSort2["WATCHED_TIME"] = "WATCHED_TIME";
1580
+ UserSort2["WATCHED_TIME_DESC"] = "WATCHED_TIME_DESC";
1581
+ UserSort2["CHAPTERS_READ"] = "CHAPTERS_READ";
1582
+ UserSort2["CHAPTERS_READ_DESC"] = "CHAPTERS_READ_DESC";
1583
+ UserSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
1584
+ return UserSort2;
1585
+ })(UserSort || {});
1586
+
1322
1587
  // src/client/media.ts
1323
1588
  async function getMedia(client, id, include) {
1324
1589
  validateId(id, "mediaId");
@@ -1326,6 +1591,14 @@ async function getMedia(client, id, include) {
1326
1591
  const data = await client.request(query, { id });
1327
1592
  return data.Media;
1328
1593
  }
1594
+ async function getMediaByMalId(client, malId, type) {
1595
+ validateId(malId, "malId");
1596
+ const data = await client.request(QUERY_MEDIA_BY_MAL_ID, {
1597
+ idMal: malId,
1598
+ type
1599
+ });
1600
+ return data.Media;
1601
+ }
1329
1602
  async function searchMedia(client, options = {}) {
1330
1603
  const { query: search, page = 1, perPage = 20, genres, tags, genresExclude, tagsExclude, ...filters } = options;
1331
1604
  return client.pagedRequest(
@@ -1446,7 +1719,7 @@ async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
1446
1719
  for await (const episode of iterator) {
1447
1720
  const epDate = new Date(episode.airingAt * 1e3);
1448
1721
  const dayName = names[epDate.getUTCDay()];
1449
- schedule[dayName].push(episode);
1722
+ if (dayName) schedule[dayName].push(episode);
1450
1723
  }
1451
1724
  return schedule;
1452
1725
  }
@@ -1472,8 +1745,14 @@ async function searchStaff(client, options = {}) {
1472
1745
  }
1473
1746
 
1474
1747
  // src/client/studio.ts
1475
- async function getStudio(client, id) {
1748
+ async function getStudio(client, id, include) {
1476
1749
  validateId(id, "studioId");
1750
+ if (include?.media) {
1751
+ const perPage = typeof include.media === "object" ? include.media.perPage : void 0;
1752
+ const query = buildStudioByIdQuery(perPage);
1753
+ const data2 = await client.request(query, { id });
1754
+ return data2.Studio;
1755
+ }
1477
1756
  const data = await client.request(QUERY_STUDIO_BY_ID, { id });
1478
1757
  return data.Studio;
1479
1758
  }
@@ -1529,7 +1808,7 @@ async function searchUsers(client, options = {}) {
1529
1808
  }
1530
1809
  async function getUserMediaList(client, options) {
1531
1810
  if (!options.userId && !options.userName) {
1532
- throw new AniListError("getUserMediaList requires either userId or userName", 0, []);
1811
+ throw new TypeError("getUserMediaList requires either userId or userName");
1533
1812
  }
1534
1813
  if (options.userId) {
1535
1814
  validateId(options.userId, "userId");
@@ -1548,15 +1827,18 @@ async function getUserMediaList(client, options) {
1548
1827
  "mediaList"
1549
1828
  );
1550
1829
  }
1551
- async function getUserFavorites(client, idOrName) {
1830
+ async function getUserFavorites(client, idOrName, options) {
1831
+ const useBuilder = options?.perPage !== void 0;
1552
1832
  if (typeof idOrName === "number") {
1553
1833
  validateId(idOrName, "userId");
1554
- const data2 = await client.request(QUERY_USER_FAVORITES_BY_ID, {
1834
+ const query2 = useBuilder ? buildUserFavoritesQuery("id", options.perPage) : QUERY_USER_FAVORITES_BY_ID;
1835
+ const data2 = await client.request(query2, {
1555
1836
  id: idOrName
1556
1837
  });
1557
1838
  return mapFavorites(data2.User.favourites);
1558
1839
  }
1559
- const data = await client.request(QUERY_USER_FAVORITES_BY_NAME, {
1840
+ const query = useBuilder ? buildUserFavoritesQuery("name", options.perPage) : QUERY_USER_FAVORITES_BY_NAME;
1841
+ const data = await client.request(query, {
1560
1842
  name: idOrName
1561
1843
  });
1562
1844
  return mapFavorites(data.User.favourites);
@@ -1573,10 +1855,19 @@ function mapFavorites(fav) {
1573
1855
 
1574
1856
  // src/client/index.ts
1575
1857
  var DEFAULT_API_URL = "https://graphql.anilist.co";
1576
- var LIB_VERSION = "1.7.0" ;
1858
+ var LIB_VERSION = "1.8.1" ;
1577
1859
  var AniListClient = class {
1860
+ apiUrl;
1861
+ headers;
1862
+ cacheAdapter;
1863
+ rateLimiter;
1864
+ hooks;
1865
+ logger;
1866
+ signal;
1867
+ inFlight = /* @__PURE__ */ new Map();
1868
+ _rateLimitInfo;
1869
+ _lastRequestMeta;
1578
1870
  constructor(options = {}) {
1579
- this.inFlight = /* @__PURE__ */ new Map();
1580
1871
  this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
1581
1872
  this.headers = {
1582
1873
  "Content-Type": "application/json",
@@ -1589,6 +1880,7 @@ var AniListClient = class {
1589
1880
  this.cacheAdapter = options.cacheAdapter ?? new MemoryCache(options.cache);
1590
1881
  this.rateLimiter = new RateLimiter(options.rateLimit);
1591
1882
  this.hooks = options.hooks ?? {};
1883
+ this.logger = options.logger;
1592
1884
  this.signal = options.signal;
1593
1885
  }
1594
1886
  /**
@@ -1611,6 +1903,7 @@ var AniListClient = class {
1611
1903
  const cached = await this.cacheAdapter.get(cacheKey);
1612
1904
  if (cached !== void 0) {
1613
1905
  this.hooks.onCacheHit?.(cacheKey);
1906
+ this.logger?.debug("Cache hit", { cacheKey });
1614
1907
  const meta = { durationMs: 0, fromCache: true };
1615
1908
  this._lastRequestMeta = meta;
1616
1909
  this.hooks.onResponse?.(query, 0, true);
@@ -1630,6 +1923,7 @@ var AniListClient = class {
1630
1923
  async executeRequest(query, variables, cacheKey) {
1631
1924
  const start = Date.now();
1632
1925
  this.hooks.onRequest?.(query, variables);
1926
+ this.logger?.debug("API request", { variables });
1633
1927
  const minifiedQuery = normalizeQuery(query);
1634
1928
  let res;
1635
1929
  try {
@@ -1645,13 +1939,23 @@ var AniListClient = class {
1645
1939
  );
1646
1940
  } catch (err) {
1647
1941
  const error = err instanceof AniListError ? err : new AniListError(err.message ?? "Network request failed", 0, [err]);
1942
+ this.logger?.error("Request failed", { error: error.message, status: error.status });
1943
+ this.hooks.onError?.(error, query, variables);
1944
+ throw error;
1945
+ }
1946
+ let json;
1947
+ try {
1948
+ json = await res.json();
1949
+ } catch {
1950
+ const error = new AniListError(`Non-JSON response from AniList (HTTP ${res.status})`, res.status, []);
1951
+ this.logger?.error("Request failed", { error: error.message, status: error.status });
1648
1952
  this.hooks.onError?.(error, query, variables);
1649
1953
  throw error;
1650
1954
  }
1651
- const json = await res.json();
1652
1955
  if (!res.ok || json.errors) {
1653
1956
  const message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
1654
1957
  const error = new AniListError(message, res.status, json.errors ?? []);
1958
+ this.logger?.error("Request failed", { error: error.message, status: error.status });
1655
1959
  this.hooks.onError?.(error, query, variables);
1656
1960
  throw error;
1657
1961
  }
@@ -1670,6 +1974,7 @@ var AniListClient = class {
1670
1974
  await this.cacheAdapter.set(cacheKey, data);
1671
1975
  const meta = { durationMs, fromCache: false, rateLimitInfo: this._rateLimitInfo };
1672
1976
  this._lastRequestMeta = meta;
1977
+ this.logger?.debug("Request complete", { durationMs, rateLimitInfo: this._rateLimitInfo });
1673
1978
  this.hooks.onResponse?.(query, durationMs, false, this._rateLimitInfo);
1674
1979
  return data;
1675
1980
  }
@@ -1732,6 +2037,15 @@ var AniListClient = class {
1732
2037
  async getAiredChapters(options = {}) {
1733
2038
  return this.getRecentlyUpdatedManga(options);
1734
2039
  }
2040
+ /**
2041
+ * Fetch a media entry by its MyAnimeList (MAL) ID.
2042
+ *
2043
+ * @param malId - The MyAnimeList ID
2044
+ * @param type - Optional media type to disambiguate (some MAL IDs map to both ANIME and MANGA)
2045
+ */
2046
+ async getMediaByMalId(malId, type) {
2047
+ return getMediaByMalId(this, malId, type);
2048
+ }
1735
2049
  /** Get the detailed schedule for the current week, sorted by day. */
1736
2050
  async getWeeklySchedule(date) {
1737
2051
  return getWeeklySchedule(this, date);
@@ -1784,20 +2098,32 @@ var AniListClient = class {
1784
2098
  * Fetch a user's favorite anime, manga, characters, staff, and studios.
1785
2099
  *
1786
2100
  * @param idOrName - AniList user ID (number) or username (string)
2101
+ * @param options - Optional pagination options (perPage per category)
1787
2102
  * @returns The user's favorites grouped by category
1788
2103
  *
1789
2104
  * @example
1790
2105
  * ```typescript
1791
2106
  * const favs = await client.getUserFavorites("AniList");
1792
2107
  * favs.anime.forEach(a => console.log(a.title.romaji));
2108
+ *
2109
+ * // Fetch more results per category
2110
+ * const moreResults = await client.getUserFavorites(1, { perPage: 50 });
1793
2111
  * ```
1794
2112
  */
1795
- async getUserFavorites(idOrName) {
1796
- return getUserFavorites(this, idOrName);
2113
+ async getUserFavorites(idOrName, options) {
2114
+ return getUserFavorites(this, idOrName, options);
1797
2115
  }
1798
- /** Fetch a studio by its AniList ID. */
1799
- async getStudio(id) {
1800
- return getStudio(this, id);
2116
+ /**
2117
+ * Fetch a studio by its AniList ID.
2118
+ * Pass `include` to customise the number of media returned.
2119
+ *
2120
+ * @example
2121
+ * ```typescript
2122
+ * const studio = await client.getStudio(21, { media: { perPage: 50 } });
2123
+ * ```
2124
+ */
2125
+ async getStudio(id, include) {
2126
+ return getStudio(this, id, include);
1801
2127
  }
1802
2128
  /** Search for studios by name. */
1803
2129
  async searchStudios(options = {}) {
@@ -1847,21 +2173,24 @@ var AniListClient = class {
1847
2173
  async getMediaBatch(ids) {
1848
2174
  if (ids.length === 0) return [];
1849
2175
  validateIds(ids, "mediaId");
1850
- if (ids.length === 1) return [await this.getMedia(ids[0])];
2176
+ const [singleMediaId] = ids;
2177
+ if (ids.length === 1 && singleMediaId !== void 0) return [await this.getMedia(singleMediaId)];
1851
2178
  return this.executeBatch(ids, buildBatchMediaQuery, "m");
1852
2179
  }
1853
2180
  /** Fetch multiple characters in a single API request. */
1854
2181
  async getCharacterBatch(ids) {
1855
2182
  if (ids.length === 0) return [];
1856
2183
  validateIds(ids, "characterId");
1857
- if (ids.length === 1) return [await this.getCharacter(ids[0])];
2184
+ const [singleCharId] = ids;
2185
+ if (ids.length === 1 && singleCharId !== void 0) return [await this.getCharacter(singleCharId)];
1858
2186
  return this.executeBatch(ids, buildBatchCharacterQuery, "c");
1859
2187
  }
1860
2188
  /** Fetch multiple staff members in a single API request. */
1861
2189
  async getStaffBatch(ids) {
1862
2190
  if (ids.length === 0) return [];
1863
2191
  validateIds(ids, "staffId");
1864
- if (ids.length === 1) return [await this.getStaff(ids[0])];
2192
+ const [singleStaffId] = ids;
2193
+ if (ids.length === 1 && singleStaffId !== void 0) return [await this.getStaff(singleStaffId)];
1865
2194
  return this.executeBatch(ids, buildBatchStaffQuery, "s");
1866
2195
  }
1867
2196
  /** @internal */
@@ -1906,88 +2235,23 @@ var AniListClient = class {
1906
2235
  this.inFlight.clear();
1907
2236
  this.rateLimiter.dispose();
1908
2237
  }
1909
- };
1910
-
1911
- // src/cache/redis.ts
1912
- var RedisCache = class {
1913
- constructor(options) {
1914
- this.client = options.client;
1915
- this.prefix = options.prefix ?? "ani:";
1916
- this.ttl = options.ttl ?? 86400;
1917
- }
1918
- prefixedKey(key) {
1919
- return `${this.prefix}${key}`;
1920
- }
1921
- async get(key) {
1922
- const raw = await this.client.get(this.prefixedKey(key));
1923
- if (raw === null) return void 0;
1924
- try {
1925
- return JSON.parse(raw);
1926
- } catch {
1927
- return void 0;
1928
- }
1929
- }
1930
- async set(key, data) {
1931
- await this.client.set(this.prefixedKey(key), JSON.stringify(data), "EX", this.ttl);
1932
- }
1933
- async delete(key) {
1934
- const count = await this.client.del(this.prefixedKey(key));
1935
- return count > 0;
1936
- }
1937
2238
  /**
1938
- * Collect keys matching a pattern. Uses SCAN when available, falls back to KEYS.
2239
+ * Return a scoped view of this client where every request uses the given `AbortSignal`.
2240
+ * The returned object shares the same cache, rate limiter, and hooks.
1939
2241
  *
1940
- * **Warning:** The `KEYS` fallback is O(N) and blocks the Redis server.
1941
- * Provide a client with `scanIterator` support for production use.
1942
- * @internal
1943
- */
1944
- async collectKeys(pattern) {
1945
- if (this.client.scanIterator) {
1946
- const keys = [];
1947
- for await (const key of this.client.scanIterator({ MATCH: pattern, COUNT: 100 })) {
1948
- keys.push(key);
1949
- }
1950
- return keys;
1951
- }
1952
- return this.client.keys(pattern);
1953
- }
1954
- async clear() {
1955
- const keys = await this.collectKeys(`${this.prefix}*`);
1956
- if (keys.length > 0) {
1957
- await this.client.del(...keys);
1958
- }
1959
- }
1960
- /**
1961
- * Get the actual number of keys with this prefix in Redis.
1962
- */
1963
- get size() {
1964
- return this.getSize();
1965
- }
1966
- /** @internal */
1967
- async getSize() {
1968
- const keys = await this.collectKeys(`${this.prefix}*`);
1969
- return keys.length;
1970
- }
1971
- async keys() {
1972
- const raw = await this.collectKeys(`${this.prefix}*`);
1973
- return raw.map((k) => k.slice(this.prefix.length));
1974
- }
1975
- /**
1976
- * Remove all entries whose key matches the given glob pattern.
2242
+ * @example
2243
+ * ```ts
2244
+ * const controller = new AbortController();
2245
+ * const media = await client.withSignal(controller.signal).getMedia(1);
1977
2246
  *
1978
- * @param pattern A glob pattern (e.g. `"*Media*"`)
1979
- * @returns Number of entries removed.
2247
+ * // Cancel all in-flight requests made through the scoped view
2248
+ * controller.abort();
2249
+ * ```
1980
2250
  */
1981
- async invalidate(pattern) {
1982
- if (typeof pattern === "string") {
1983
- const keys = await this.collectKeys(`${this.prefix}${pattern}`);
1984
- if (keys.length === 0) return 0;
1985
- return this.client.del(...keys);
1986
- }
1987
- const allKeys = await this.collectKeys(`${this.prefix}*`);
1988
- const matching = allKeys.filter((k) => pattern.test(k.slice(this.prefix.length)));
1989
- if (matching.length === 0) return 0;
1990
- return this.client.del(...matching);
2251
+ withSignal(signal) {
2252
+ const scoped = Object.create(this);
2253
+ Object.defineProperty(scoped, "signal", { value: signal, configurable: true });
2254
+ return scoped;
1991
2255
  }
1992
2256
  };
1993
2257