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