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