ani-client 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,31 +1,89 @@
1
1
  // src/utils/markdown.ts
2
+ function isSafeUrl(url) {
3
+ return /^https?:\/\//i.test(url);
4
+ }
2
5
  function parseAniListMarkdown(text) {
3
6
  if (!text) return "";
4
7
  let html = text;
8
+ html = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9
+ html = html.replace(/```([\s\S]*?)```/g, (_match, code) => {
10
+ return `<pre><code>${code.trim()}</code></pre>`;
11
+ });
12
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
5
13
  html = html.replace(/~!(.*?)!~/gs, '<span class="anilist-spoiler">$1</span>');
6
14
  html = html.replace(/~~~(.*?)~~~/gs, '<div class="anilist-center">$1</div>');
7
- html = html.replace(/img(\d+)\((.*?)\)/gi, '<img src="$2" width="$1" alt="" class="anilist-image" />');
8
- html = html.replace(/img\((.*?)\)/gi, '<img src="$1" alt="" class="anilist-image" />');
9
15
  html = html.replace(
10
- /youtube\((.*?)\)/gi,
11
- '<iframe src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen class="anilist-youtube"></iframe>'
16
+ /img(\d+)\((.*?)\)/gi,
17
+ (_match, width, url) => isSafeUrl(url) ? `<img src="${url}" width="${width}" alt="" class="anilist-image" />` : ""
18
+ );
19
+ html = html.replace(
20
+ /img\((.*?)\)/gi,
21
+ (_match, url) => isSafeUrl(url) ? `<img src="${url}" alt="" class="anilist-image" />` : ""
12
22
  );
13
- html = html.replace(/webm\((.*?)\)/gi, '<video src="$1" controls class="anilist-webm"></video>');
23
+ html = html.replace(/youtube\((.*?)\)/gi, (_match, id) => {
24
+ if (!/^[\w-]+$/.test(id)) return "";
25
+ return `<iframe src="https://www.youtube.com/embed/${id}" frameborder="0" allowfullscreen class="anilist-youtube"></iframe>`;
26
+ });
27
+ html = html.replace(
28
+ /webm\((.*?)\)/gi,
29
+ (_match, url) => isSafeUrl(url) ? `<video src="${url}" controls class="anilist-webm"></video>` : ""
30
+ );
31
+ html = html.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>");
32
+ html = html.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>");
33
+ html = html.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>");
34
+ html = html.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>");
35
+ html = html.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>");
36
+ html = html.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>");
14
37
  html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
15
38
  html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
16
39
  html = html.replace(/_(.*?)_/g, "<em>$1</em>");
17
40
  html = html.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, "<em>$1</em>");
18
41
  html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
19
- html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
42
+ html = html.replace(
43
+ /\[(.*?)\]\((.*?)\)/g,
44
+ (_match, text2, url) => isSafeUrl(url) ? `<a href="${url}" target="_blank" rel="noopener noreferrer">${text2}</a>` : text2
45
+ );
20
46
  html = html.replace(/\r\n/g, "\n");
47
+ const lines = html.split("\n");
48
+ const processed = [];
49
+ let listType = null;
50
+ for (const line of lines) {
51
+ const ulMatch = line.match(/^[\s]*[-*]\s+(.*)/);
52
+ const olMatch = line.match(/^[\s]*\d+\.\s+(.*)/);
53
+ if (ulMatch) {
54
+ if (listType !== "ul") {
55
+ if (listType) processed.push(`</${listType}>`);
56
+ processed.push("<ul>");
57
+ listType = "ul";
58
+ }
59
+ processed.push(`<li>${ulMatch[1]}</li>`);
60
+ } else if (olMatch) {
61
+ if (listType !== "ol") {
62
+ if (listType) processed.push(`</${listType}>`);
63
+ processed.push("<ol>");
64
+ listType = "ol";
65
+ }
66
+ processed.push(`<li>${olMatch[1]}</li>`);
67
+ } else {
68
+ if (listType) {
69
+ processed.push(`</${listType}>`);
70
+ listType = null;
71
+ }
72
+ processed.push(line);
73
+ }
74
+ }
75
+ if (listType) processed.push(`</${listType}>`);
76
+ html = processed.join("\n");
21
77
  const paragraphs = html.split(/\n{2,}/);
22
78
  html = paragraphs.map((p) => {
23
- const withBr = p.replace(/\n/g, "<br />");
24
- if (withBr.match(/^(<div|<iframe|<video|<img)/)) {
79
+ const trimmed = p.trim();
80
+ if (!trimmed) return "";
81
+ const withBr = trimmed.replace(/\n/g, "<br />");
82
+ if (withBr.match(/^(<div|<iframe|<video|<img|<h[1-6]|<ul|<ol|<pre)/)) {
25
83
  return withBr;
26
84
  }
27
85
  return `<p>${withBr}</p>`;
28
- }).join("\n");
86
+ }).filter(Boolean).join("\n");
29
87
  return html;
30
88
  }
31
89
 
@@ -428,6 +486,48 @@ var STUDIO_FIELDS = `
428
486
  }
429
487
  }
430
488
  `;
489
+ var THREAD_FIELDS = `
490
+ id
491
+ title
492
+ body(asHtml: false)
493
+ userId
494
+ replyUserId
495
+ replyCommentId
496
+ replyCount
497
+ viewCount
498
+ isLocked
499
+ isSticky
500
+ isSubscribed
501
+ repliedAt
502
+ createdAt
503
+ updatedAt
504
+ siteUrl
505
+ user {
506
+ id
507
+ name
508
+ avatar { large medium }
509
+ }
510
+ replyUser {
511
+ id
512
+ name
513
+ avatar { large medium }
514
+ }
515
+ categories {
516
+ id
517
+ name
518
+ }
519
+ mediaCategories {
520
+ id
521
+ title { romaji english native userPreferred }
522
+ type
523
+ coverImage { large medium }
524
+ siteUrl
525
+ }
526
+ likes {
527
+ id
528
+ name
529
+ }
530
+ `;
431
531
 
432
532
  // src/queries/media.ts
433
533
  var QUERY_MEDIA_BY_ID = `
@@ -676,10 +776,10 @@ query ($id: Int!) {
676
776
  }
677
777
  }`;
678
778
  var QUERY_STUDIO_SEARCH = `
679
- query ($search: String, $page: Int, $perPage: Int) {
779
+ query ($search: String, $sort: [StudioSort], $page: Int, $perPage: Int) {
680
780
  Page(page: $page, perPage: $perPage) {
681
781
  pageInfo { total perPage currentPage lastPage hasNextPage }
682
- studios(search: $search) {
782
+ studios(search: $search, sort: $sort) {
683
783
  ${STUDIO_FIELDS}
684
784
  }
685
785
  }
@@ -710,7 +810,7 @@ function buildMediaByIdQuery(include) {
710
810
  }
711
811
  if (include.characters) {
712
812
  const opts = typeof include.characters === "object" ? include.characters : {};
713
- const perPage = opts.perPage ?? 25;
813
+ const perPage = clampPerPage(opts.perPage ?? 25);
714
814
  const sortClause = opts.sort !== false ? ", sort: [ROLE, RELEVANCE, ID]" : "";
715
815
  const voiceActorBlock = opts.voiceActors ? `
716
816
  voiceActors {
@@ -728,7 +828,7 @@ function buildMediaByIdQuery(include) {
728
828
  }
729
829
  if (include.staff) {
730
830
  const opts = typeof include.staff === "object" ? include.staff : {};
731
- const perPage = opts.perPage ?? 25;
831
+ const perPage = clampPerPage(opts.perPage ?? 25);
732
832
  const sortClause = opts.sort !== false ? ", sort: [RELEVANCE, ID]" : "";
733
833
  extra.push(`
734
834
  staff(perPage: ${perPage}${sortClause}) {
@@ -741,7 +841,9 @@ function buildMediaByIdQuery(include) {
741
841
  }`);
742
842
  }
743
843
  if (include.recommendations) {
744
- const perPage = typeof include.recommendations === "object" ? include.recommendations.perPage ?? 10 : 10;
844
+ const perPage = clampPerPage(
845
+ typeof include.recommendations === "object" ? include.recommendations.perPage ?? 10 : 10
846
+ );
745
847
  extra.push(`
746
848
  recommendations(perPage: ${perPage}, sort: [RATING_DESC]) {
747
849
  nodes {
@@ -800,53 +902,11 @@ function buildBatchQuery(ids, typeName, fields, prefix) {
800
902
  ${aliases}
801
903
  }`;
802
904
  }
803
- var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS_BASE, "m");
905
+ var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS, "m");
804
906
  var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
805
907
  var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
806
908
 
807
909
  // src/queries/thread.ts
808
- var THREAD_FIELDS = `
809
- id
810
- title
811
- body(asHtml: false)
812
- userId
813
- replyUserId
814
- replyCommentId
815
- replyCount
816
- viewCount
817
- isLocked
818
- isSticky
819
- isSubscribed
820
- repliedAt
821
- createdAt
822
- updatedAt
823
- siteUrl
824
- user {
825
- id
826
- name
827
- avatar { large medium }
828
- }
829
- replyUser {
830
- id
831
- name
832
- avatar { large medium }
833
- }
834
- categories {
835
- id
836
- name
837
- }
838
- mediaCategories {
839
- id
840
- title { romaji english native userPreferred }
841
- type
842
- coverImage { large medium }
843
- siteUrl
844
- }
845
- likes {
846
- id
847
- name
848
- }
849
- `;
850
910
  var QUERY_THREAD_BY_ID = `
851
911
  query ($id: Int!) {
852
912
  Thread(id: $id) {
@@ -868,6 +928,8 @@ var RateLimiter = class {
868
928
  constructor(options = {}) {
869
929
  this.head = 0;
870
930
  this.count = 0;
931
+ /** @internal — active sleep timers for cleanup */
932
+ this.activeTimers = /* @__PURE__ */ new Set();
871
933
  this.maxRequests = options.maxRequests ?? 85;
872
934
  this.windowMs = options.windowMs ?? 6e4;
873
935
  this.maxRetries = options.maxRetries ?? 3;
@@ -885,8 +947,7 @@ var RateLimiter = class {
885
947
  if (!this.enabled) return;
886
948
  if (this.count >= this.maxRequests) {
887
949
  const oldest = this.timestamps[this.head];
888
- const now2 = Date.now();
889
- const elapsed = now2 - oldest;
950
+ const elapsed = Date.now() - oldest;
890
951
  if (elapsed < this.windowMs) {
891
952
  const waitMs = this.windowMs - elapsed + 50;
892
953
  await this.sleep(waitMs);
@@ -950,14 +1011,32 @@ var RateLimiter = class {
950
1011
  if (this.timeoutMs <= 0) return fetch(url, init);
951
1012
  const controller = new AbortController();
952
1013
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1014
+ const signals = [controller.signal, init.signal].filter(Boolean);
1015
+ const combinedSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
953
1016
  try {
954
- return await fetch(url, { ...init, signal: controller.signal });
1017
+ return await fetch(url, { ...init, signal: combinedSignal });
955
1018
  } finally {
956
1019
  clearTimeout(timer);
957
1020
  }
958
1021
  }
959
1022
  sleep(ms) {
960
- return new Promise((resolve) => setTimeout(resolve, ms));
1023
+ return new Promise((resolve) => {
1024
+ const timer = setTimeout(() => {
1025
+ this.activeTimers.delete(timer);
1026
+ resolve();
1027
+ }, ms);
1028
+ this.activeTimers.add(timer);
1029
+ });
1030
+ }
1031
+ /** Cancel all pending sleep timers and reset internal state. */
1032
+ dispose() {
1033
+ for (const timer of this.activeTimers) {
1034
+ clearTimeout(timer);
1035
+ }
1036
+ this.activeTimers.clear();
1037
+ this.head = 0;
1038
+ this.count = 0;
1039
+ this.timestamps.fill(0);
961
1040
  }
962
1041
  };
963
1042
  var RETRYABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
@@ -1161,6 +1240,18 @@ var UserSort = /* @__PURE__ */ ((UserSort2) => {
1161
1240
  return UserSort2;
1162
1241
  })(UserSort || {});
1163
1242
 
1243
+ // src/types/studio.ts
1244
+ var StudioSort = /* @__PURE__ */ ((StudioSort2) => {
1245
+ StudioSort2["ID"] = "ID";
1246
+ StudioSort2["ID_DESC"] = "ID_DESC";
1247
+ StudioSort2["NAME"] = "NAME";
1248
+ StudioSort2["NAME_DESC"] = "NAME_DESC";
1249
+ StudioSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
1250
+ StudioSort2["FAVOURITES"] = "FAVOURITES";
1251
+ StudioSort2["FAVOURITES_DESC"] = "FAVOURITES_DESC";
1252
+ return StudioSort2;
1253
+ })(StudioSort || {});
1254
+
1164
1255
  // src/types/lists.ts
1165
1256
  var MediaListStatus = /* @__PURE__ */ ((MediaListStatus2) => {
1166
1257
  MediaListStatus2["CURRENT"] = "CURRENT";
@@ -1273,7 +1364,7 @@ async function getAiredEpisodes(client, options = {}) {
1273
1364
  "airingSchedules"
1274
1365
  );
1275
1366
  }
1276
- async function getAiredChapters(client, options = {}) {
1367
+ async function getRecentlyUpdatedManga(client, options = {}) {
1277
1368
  return client.pagedRequest(
1278
1369
  QUERY_RECENT_CHAPTERS,
1279
1370
  {
@@ -1296,6 +1387,7 @@ async function getPlanning(client, options = {}) {
1296
1387
  );
1297
1388
  }
1298
1389
  async function getRecommendations(client, mediaId, options = {}) {
1390
+ validateId(mediaId, "mediaId");
1299
1391
  const data = await client.request(QUERY_RECOMMENDATIONS, {
1300
1392
  mediaId,
1301
1393
  page: options.page ?? 1,
@@ -1331,14 +1423,12 @@ async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
1331
1423
  Saturday: [],
1332
1424
  Sunday: []
1333
1425
  };
1334
- const startOfWeek = new Date(date);
1335
- const day = startOfWeek.getDay();
1336
- const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
1337
- startOfWeek.setDate(diff);
1338
- startOfWeek.setHours(0, 0, 0, 0);
1426
+ const utcDay = date.getUTCDay();
1427
+ const diff = utcDay === 0 ? -6 : 1 - utcDay;
1428
+ const startOfWeek = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + diff, 0, 0, 0));
1339
1429
  const endOfWeek = new Date(startOfWeek);
1340
- endOfWeek.setDate(startOfWeek.getDate() + 6);
1341
- endOfWeek.setHours(23, 59, 59, 999);
1430
+ endOfWeek.setUTCDate(startOfWeek.getUTCDate() + 6);
1431
+ endOfWeek.setUTCHours(23, 59, 59, 999);
1342
1432
  const startTimestamp = Math.floor(startOfWeek.getTime() / 1e3);
1343
1433
  const endTimestamp = Math.floor(endOfWeek.getTime() / 1e3);
1344
1434
  const iterator = client.paginate(
@@ -1347,12 +1437,13 @@ async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
1347
1437
  airingAtLesser: endTimestamp,
1348
1438
  page,
1349
1439
  perPage: 50
1350
- })
1440
+ }),
1441
+ 20
1351
1442
  );
1352
1443
  const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1353
1444
  for await (const episode of iterator) {
1354
1445
  const epDate = new Date(episode.airingAt * 1e3);
1355
- const dayName = names[epDate.getDay()];
1446
+ const dayName = names[epDate.getUTCDay()];
1356
1447
  schedule[dayName].push(episode);
1357
1448
  }
1358
1449
  return schedule;
@@ -1385,12 +1476,14 @@ async function getStudio(client, id) {
1385
1476
  return data.Studio;
1386
1477
  }
1387
1478
  async function searchStudios(client, options = {}) {
1479
+ const { query: search, page = 1, perPage = 20, sort } = options;
1388
1480
  return client.pagedRequest(
1389
1481
  QUERY_STUDIO_SEARCH,
1390
1482
  {
1391
- search: options.query,
1392
- page: options.page ?? 1,
1393
- perPage: clampPerPage(options.perPage ?? 20)
1483
+ search,
1484
+ sort,
1485
+ page,
1486
+ perPage: clampPerPage(perPage)
1394
1487
  },
1395
1488
  "studios"
1396
1489
  );
@@ -1436,6 +1529,9 @@ async function getUserMediaList(client, options) {
1436
1529
  if (!options.userId && !options.userName) {
1437
1530
  throw new AniListError("getUserMediaList requires either userId or userName", 0, []);
1438
1531
  }
1532
+ if (options.userId) {
1533
+ validateId(options.userId, "userId");
1534
+ }
1439
1535
  return client.pagedRequest(
1440
1536
  QUERY_USER_MEDIA_LIST,
1441
1537
  {
@@ -1475,13 +1571,15 @@ function mapFavorites(fav) {
1475
1571
 
1476
1572
  // src/client/index.ts
1477
1573
  var DEFAULT_API_URL = "https://graphql.anilist.co";
1574
+ var LIB_VERSION = "1.7.0" ;
1478
1575
  var AniListClient = class {
1479
1576
  constructor(options = {}) {
1480
1577
  this.inFlight = /* @__PURE__ */ new Map();
1481
1578
  this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
1482
1579
  this.headers = {
1483
1580
  "Content-Type": "application/json",
1484
- Accept: "application/json"
1581
+ Accept: "application/json",
1582
+ "User-Agent": `ani-client/${LIB_VERSION}`
1485
1583
  };
1486
1584
  if (options.token) {
1487
1585
  this.headers.Authorization = `Bearer ${options.token}`;
@@ -1505,7 +1603,6 @@ var AniListClient = class {
1505
1603
  get lastRequestMeta() {
1506
1604
  return this._lastRequestMeta;
1507
1605
  }
1508
- // ── Core infrastructure (internal) ──
1509
1606
  /** @internal */
1510
1607
  async request(query, variables = {}) {
1511
1608
  const cacheKey = MemoryCache.key(query, variables);
@@ -1532,20 +1629,29 @@ var AniListClient = class {
1532
1629
  const start = Date.now();
1533
1630
  this.hooks.onRequest?.(query, variables);
1534
1631
  const minifiedQuery = normalizeQuery(query);
1535
- const res = await this.rateLimiter.fetchWithRetry(
1536
- this.apiUrl,
1537
- {
1538
- method: "POST",
1539
- headers: this.headers,
1540
- body: JSON.stringify({ query: minifiedQuery, variables }),
1541
- signal: this.signal
1542
- },
1543
- { onRetry: this.hooks.onRetry, onRateLimit: this.hooks.onRateLimit }
1544
- );
1632
+ let res;
1633
+ try {
1634
+ res = await this.rateLimiter.fetchWithRetry(
1635
+ this.apiUrl,
1636
+ {
1637
+ method: "POST",
1638
+ headers: this.headers,
1639
+ body: JSON.stringify({ query: minifiedQuery, variables }),
1640
+ signal: this.signal
1641
+ },
1642
+ { onRetry: this.hooks.onRetry, onRateLimit: this.hooks.onRateLimit }
1643
+ );
1644
+ } catch (err) {
1645
+ const error = err instanceof AniListError ? err : new AniListError(err.message ?? "Network request failed", 0, [err]);
1646
+ this.hooks.onError?.(error, query, variables);
1647
+ throw error;
1648
+ }
1545
1649
  const json = await res.json();
1546
1650
  if (!res.ok || json.errors) {
1547
1651
  const message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
1548
- throw new AniListError(message, res.status, json.errors ?? []);
1652
+ const error = new AniListError(message, res.status, json.errors ?? []);
1653
+ this.hooks.onError?.(error, query, variables);
1654
+ throw error;
1549
1655
  }
1550
1656
  const rlLimit = res.headers.get("X-RateLimit-Limit");
1551
1657
  const rlRemaining = res.headers.get("X-RateLimit-Remaining");
@@ -1574,7 +1680,6 @@ var AniListClient = class {
1574
1680
  }
1575
1681
  return { pageInfo: data.Page.pageInfo, results };
1576
1682
  }
1577
- // ── Media ──
1578
1683
  /**
1579
1684
  * Fetch a single media entry by its AniList ID.
1580
1685
  *
@@ -1611,9 +1716,19 @@ var AniListClient = class {
1611
1716
  async getAiredEpisodes(options = {}) {
1612
1717
  return getAiredEpisodes(this, options);
1613
1718
  }
1614
- /** Get currently releasing manga. */
1719
+ /**
1720
+ * Get currently releasing manga sorted by most recently updated.
1721
+ *
1722
+ * @param options - Pagination parameters
1723
+ */
1724
+ async getRecentlyUpdatedManga(options = {}) {
1725
+ return getRecentlyUpdatedManga(this, options);
1726
+ }
1727
+ /**
1728
+ * @deprecated Use `getRecentlyUpdatedManga` instead. This alias will be removed in v2.
1729
+ */
1615
1730
  async getAiredChapters(options = {}) {
1616
- return getAiredChapters(this, options);
1731
+ return this.getRecentlyUpdatedManga(options);
1617
1732
  }
1618
1733
  /** Get the detailed schedule for the current week, sorted by day. */
1619
1734
  async getWeeklySchedule(date) {
@@ -1631,7 +1746,6 @@ var AniListClient = class {
1631
1746
  async getMediaBySeason(options) {
1632
1747
  return getMediaBySeason(this, options);
1633
1748
  }
1634
- // ── Characters ──
1635
1749
  /** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
1636
1750
  async getCharacter(id, include) {
1637
1751
  return getCharacter(this, id, include);
@@ -1640,7 +1754,6 @@ var AniListClient = class {
1640
1754
  async searchCharacters(options = {}) {
1641
1755
  return searchCharacters(this, options);
1642
1756
  }
1643
- // ── Staff ──
1644
1757
  /** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
1645
1758
  async getStaff(id, include) {
1646
1759
  return getStaff(this, id, include);
@@ -1649,7 +1762,6 @@ var AniListClient = class {
1649
1762
  async searchStaff(options = {}) {
1650
1763
  return searchStaff(this, options);
1651
1764
  }
1652
- // ── Users ──
1653
1765
  /**
1654
1766
  * Fetch a user by AniList ID or username.
1655
1767
  *
@@ -1681,7 +1793,6 @@ var AniListClient = class {
1681
1793
  async getUserFavorites(idOrName) {
1682
1794
  return getUserFavorites(this, idOrName);
1683
1795
  }
1684
- // ── Studios ──
1685
1796
  /** Fetch a studio by its AniList ID. */
1686
1797
  async getStudio(id) {
1687
1798
  return getStudio(this, id);
@@ -1690,8 +1801,6 @@ var AniListClient = class {
1690
1801
  async searchStudios(options = {}) {
1691
1802
  return searchStudios(this, options);
1692
1803
  }
1693
- // ── Metadata ──
1694
- // ── Threads ──
1695
1804
  /** Fetch a forum thread by its AniList ID. */
1696
1805
  async getThread(id) {
1697
1806
  return getThread(this, id);
@@ -1710,12 +1819,10 @@ var AniListClient = class {
1710
1819
  const data = await this.request(QUERY_TAGS);
1711
1820
  return data.MediaTagCollection;
1712
1821
  }
1713
- // ── Raw query ──
1714
1822
  /** Execute an arbitrary GraphQL query against the AniList API. */
1715
1823
  async raw(query, variables) {
1716
1824
  return this.request(query, variables ?? {});
1717
1825
  }
1718
- // ── Pagination ──
1719
1826
  /**
1720
1827
  * Auto-paginating async iterator. Yields individual items across all pages.
1721
1828
  *
@@ -1734,7 +1841,6 @@ var AniListClient = class {
1734
1841
  page++;
1735
1842
  }
1736
1843
  }
1737
- // ── Batch queries ──
1738
1844
  /** Fetch multiple media entries in a single API request. */
1739
1845
  async getMediaBatch(ids) {
1740
1846
  if (ids.length === 0) return [];
@@ -1768,13 +1874,12 @@ var AniListClient = class {
1768
1874
  );
1769
1875
  return chunkResults.flat();
1770
1876
  }
1771
- // ── Cache management ──
1772
1877
  /** Clear the entire response cache. */
1773
1878
  async clearCache() {
1774
1879
  await this.cacheAdapter.clear();
1775
1880
  }
1776
1881
  /** Number of entries currently in the cache. */
1777
- get cacheSize() {
1882
+ async cacheSize() {
1778
1883
  return this.cacheAdapter.size;
1779
1884
  }
1780
1885
  /** Remove cache entries whose key matches the given pattern. */
@@ -1797,6 +1902,7 @@ var AniListClient = class {
1797
1902
  async destroy() {
1798
1903
  await this.cacheAdapter.clear();
1799
1904
  this.inFlight.clear();
1905
+ this.rateLimiter.dispose();
1800
1906
  }
1801
1907
  };
1802
1908
 
@@ -1883,6 +1989,6 @@ var RedisCache = class {
1883
1989
  }
1884
1990
  };
1885
1991
 
1886
- export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaSource, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort, RedisCache, StaffSort, ThreadSort, UserSort, parseAniListMarkdown };
1992
+ export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaSource, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort, RedisCache, StaffSort, StudioSort, ThreadSort, UserSort, parseAniListMarkdown };
1887
1993
  //# sourceMappingURL=index.mjs.map
1888
1994
  //# sourceMappingURL=index.mjs.map