ani-client 2.3.0 → 2.5.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,3 +1,78 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/utils/dataloader.ts
8
+ var BatchLoader = class {
9
+ constructor(batchFetch, maxWaitMs = 50, maxBatchSize = 50) {
10
+ this.batchFetch = batchFetch;
11
+ this.maxWaitMs = maxWaitMs;
12
+ this.maxBatchSize = maxBatchSize;
13
+ }
14
+ queue = /* @__PURE__ */ new Map();
15
+ timeout = null;
16
+ /**
17
+ * Queue an ID to be fetched in the next batch.
18
+ * Returns a Promise that resolves when the batch request completes.
19
+ */
20
+ async load(id) {
21
+ return new Promise((resolve, reject) => {
22
+ let callbacks = this.queue.get(id);
23
+ if (!callbacks) {
24
+ callbacks = [];
25
+ this.queue.set(id, callbacks);
26
+ }
27
+ callbacks.push({ resolve, reject });
28
+ if (this.queue.size >= this.maxBatchSize) {
29
+ if (this.timeout) {
30
+ clearTimeout(this.timeout);
31
+ this.timeout = null;
32
+ }
33
+ void this.dispatch();
34
+ return;
35
+ }
36
+ if (this.timeout === null) {
37
+ this.timeout = setTimeout(() => {
38
+ void this.dispatch();
39
+ }, this.maxWaitMs);
40
+ const timer = this.timeout;
41
+ if (typeof timer.unref === "function") {
42
+ timer.unref();
43
+ }
44
+ }
45
+ });
46
+ }
47
+ async dispatch() {
48
+ this.timeout = null;
49
+ if (this.queue.size === 0) return;
50
+ const currentQueue = this.queue;
51
+ this.queue = /* @__PURE__ */ new Map();
52
+ const ids = Array.from(currentQueue.keys());
53
+ try {
54
+ const results = await this.batchFetch(ids);
55
+ const resultMap = /* @__PURE__ */ new Map();
56
+ for (const item of results) {
57
+ resultMap.set(item.id, item);
58
+ }
59
+ for (const [id, callbacks] of currentQueue.entries()) {
60
+ const result = resultMap.get(id);
61
+ if (result) {
62
+ for (const cb of callbacks) cb.resolve(result);
63
+ } else {
64
+ const err = new Error(`Item with ID ${id} not found in batch response`);
65
+ for (const cb of callbacks) cb.reject(err);
66
+ }
67
+ }
68
+ } catch (err) {
69
+ for (const callbacks of currentQueue.values()) {
70
+ for (const cb of callbacks) cb.reject(err);
71
+ }
72
+ }
73
+ }
74
+ };
75
+
1
76
  // src/utils/markdown.ts
2
77
  function isSafeUrl(url) {
3
78
  return /^https?:\/\//i.test(url);
@@ -573,6 +648,25 @@ var AniListError = class _AniListError extends Error {
573
648
  };
574
649
 
575
650
  // src/queries/fragments.ts
651
+ var fragments_exports = {};
652
+ __export(fragments_exports, {
653
+ CHARACTER_FIELDS: () => CHARACTER_FIELDS,
654
+ CHARACTER_FIELDS_COMPACT: () => CHARACTER_FIELDS_COMPACT,
655
+ CHARACTER_FIELDS_WITH_VA: () => CHARACTER_FIELDS_WITH_VA,
656
+ MEDIA_FIELDS: () => MEDIA_FIELDS,
657
+ MEDIA_FIELDS_BASE: () => MEDIA_FIELDS_BASE,
658
+ MEDIA_FIELDS_LIGHT: () => MEDIA_FIELDS_LIGHT,
659
+ MEDIA_LIST_FIELDS: () => MEDIA_LIST_FIELDS,
660
+ MEDIA_RECOMMENDATION_FIELDS: () => MEDIA_RECOMMENDATION_FIELDS,
661
+ RELATIONS_FIELDS: () => RELATIONS_FIELDS,
662
+ STAFF_FIELDS: () => STAFF_FIELDS,
663
+ STAFF_MEDIA_FIELDS: () => STAFF_MEDIA_FIELDS,
664
+ STUDIO_FIELDS: () => STUDIO_FIELDS,
665
+ THREAD_FIELDS: () => THREAD_FIELDS,
666
+ USER_FAVORITES_FIELDS: () => USER_FAVORITES_FIELDS,
667
+ USER_FIELDS: () => USER_FIELDS,
668
+ VOICE_ACTOR_FIELDS_COMPACT: () => VOICE_ACTOR_FIELDS_COMPACT
669
+ });
576
670
  var MEDIA_FIELDS_LIGHT = `
577
671
  __typename
578
672
  id
@@ -1263,6 +1357,9 @@ function buildMediaIncludeQuery(include) {
1263
1357
  statusDistribution { status amount }
1264
1358
  }`);
1265
1359
  }
1360
+ if (include.customFields) {
1361
+ extra.push(include.customFields);
1362
+ }
1266
1363
  return extra;
1267
1364
  }
1268
1365
  function buildMediaByIdQuery(include) {
@@ -1490,6 +1587,14 @@ query ($id: Int!) {
1490
1587
  ${CHARACTER_FIELDS_WITH_VA}
1491
1588
  }
1492
1589
  }`;
1590
+ function buildCharacterByIdQuery(include) {
1591
+ if (!include?.customFields) {
1592
+ return include?.voiceActors ? QUERY_CHARACTER_BY_ID_WITH_VA : QUERY_CHARACTER_BY_ID;
1593
+ }
1594
+ const fields = include.voiceActors ? CHARACTER_FIELDS_WITH_VA : CHARACTER_FIELDS;
1595
+ return `query ($id: Int!) { Character(id: $id) { ${fields}
1596
+ ${include.customFields} } }`;
1597
+ }
1493
1598
  var QUERY_CHARACTER_SEARCH = `
1494
1599
  query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
1495
1600
  Page(page: $page, perPage: $perPage) {
@@ -1608,6 +1713,15 @@ query ($id: Int!, $perPage: Int) {
1608
1713
  ${STAFF_MEDIA_FIELDS}
1609
1714
  }
1610
1715
  }`;
1716
+ function buildStaffByIdQuery(include) {
1717
+ if (!include?.customFields) {
1718
+ return include?.characterRoles || include?.media ? QUERY_STAFF_BY_ID_WITH_MEDIA : QUERY_STAFF_BY_ID;
1719
+ }
1720
+ const extra = include?.characterRoles || include?.media ? `
1721
+ ${STAFF_MEDIA_FIELDS}` : "";
1722
+ return `query ($id: Int!, $perPage: Int) { Staff(id: $id) { ${STAFF_FIELDS}${extra}
1723
+ ${include.customFields} } }`;
1724
+ }
1611
1725
  var QUERY_STAFF_SEARCH = `
1612
1726
  query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
1613
1727
  Page(page: $page, perPage: $perPage) {
@@ -1625,9 +1739,15 @@ query ($id: Int!) {
1625
1739
  ${STUDIO_FIELDS}
1626
1740
  }
1627
1741
  }`;
1628
- function buildStudioByIdQuery(mediaPerPage) {
1629
- if (mediaPerPage === void 0) return QUERY_STUDIO_BY_ID;
1630
- const pp = clampPerPage(mediaPerPage);
1742
+ function buildStudioByIdQuery(include) {
1743
+ const custom = include?.customFields ? `
1744
+ ${include.customFields}` : "";
1745
+ if (!include?.media) {
1746
+ if (!custom) return QUERY_STUDIO_BY_ID;
1747
+ return `query ($id: Int!) { Studio(id: $id) { ${STUDIO_FIELDS}${custom} } }`;
1748
+ }
1749
+ const mediaOpts = typeof include.media === "object" ? include.media : {};
1750
+ const pp = clampPerPage(mediaOpts.perPage ?? 25);
1631
1751
  return `
1632
1752
  query ($id: Int!) {
1633
1753
  Studio(id: $id) {
@@ -1641,7 +1761,7 @@ query ($id: Int!) {
1641
1761
  nodes {
1642
1762
  ${MEDIA_FIELDS_LIGHT}
1643
1763
  }
1644
- }
1764
+ }${custom}
1645
1765
  }
1646
1766
  }`;
1647
1767
  }
@@ -1924,7 +2044,7 @@ function isNetworkError(err) {
1924
2044
  // src/client/character.ts
1925
2045
  async function getCharacter(client, id, include) {
1926
2046
  validateId(id, "characterId");
1927
- const query = include?.voiceActors ? QUERY_CHARACTER_BY_ID_WITH_VA : QUERY_CHARACTER_BY_ID;
2047
+ const query = buildCharacterByIdQuery(include);
1928
2048
  const data = await client.request(query, { id });
1929
2049
  return data.Character;
1930
2050
  }
@@ -2401,12 +2521,13 @@ async function searchReviews(client, options = {}) {
2401
2521
  // src/client/staff.ts
2402
2522
  async function getStaff(client, id, include) {
2403
2523
  validateId(id, "staffId");
2404
- if (include?.media) {
2524
+ const query = buildStaffByIdQuery(include);
2525
+ if (include?.characterRoles || include?.media) {
2405
2526
  const perPage = typeof include.media === "object" ? include.media.perPage ?? 25 : 25;
2406
- const data2 = await client.request(QUERY_STAFF_BY_ID_WITH_MEDIA, { id, perPage });
2527
+ const data2 = await client.request(query, { id, perPage });
2407
2528
  return data2.Staff;
2408
2529
  }
2409
- const data = await client.request(QUERY_STAFF_BY_ID, { id });
2530
+ const data = await client.request(query, { id });
2410
2531
  return data.Staff;
2411
2532
  }
2412
2533
  async function searchStaff(client, options = {}) {
@@ -2421,13 +2542,8 @@ async function searchStaff(client, options = {}) {
2421
2542
  // src/client/studio.ts
2422
2543
  async function getStudio(client, id, include) {
2423
2544
  validateId(id, "studioId");
2424
- if (include?.media) {
2425
- const perPage = typeof include.media === "object" ? include.media.perPage : void 0;
2426
- const query = buildStudioByIdQuery(perPage);
2427
- const data2 = await client.request(query, { id });
2428
- return data2.Studio;
2429
- }
2430
- const data = await client.request(QUERY_STUDIO_BY_ID, { id });
2545
+ const query = buildStudioByIdQuery(include);
2546
+ const data = await client.request(query, { id });
2431
2547
  return data.Studio;
2432
2548
  }
2433
2549
  async function searchStudios(client, options = {}) {
@@ -2529,7 +2645,7 @@ function mapFavorites(fav) {
2529
2645
 
2530
2646
  // src/client/index.ts
2531
2647
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2532
- var LIB_VERSION = "2.3.0" ;
2648
+ var LIB_VERSION = "2.5.0" ;
2533
2649
  var AniListClient = class {
2534
2650
  apiUrl;
2535
2651
  headers;
@@ -2541,6 +2657,10 @@ var AniListClient = class {
2541
2657
  inFlight = /* @__PURE__ */ new Map();
2542
2658
  _rateLimitInfo;
2543
2659
  _lastRequestMeta;
2660
+ mediaLoader;
2661
+ characterLoader;
2662
+ staffLoader;
2663
+ batchingEnabled;
2544
2664
  constructor(options = {}) {
2545
2665
  this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
2546
2666
  this.headers = {
@@ -2556,6 +2676,12 @@ var AniListClient = class {
2556
2676
  this.hooks = options.hooks ?? {};
2557
2677
  this.logger = options.logger;
2558
2678
  this.signal = options.signal;
2679
+ this.batchingEnabled = options.batching?.enabled ?? true;
2680
+ const windowMs = options.batching?.windowMs ?? 50;
2681
+ const maxBatchSize = options.batching?.maxBatchSize ?? 50;
2682
+ this.mediaLoader = new BatchLoader((ids) => this.getMediaBatch(ids), windowMs, maxBatchSize);
2683
+ this.characterLoader = new BatchLoader((ids) => this.getCharacterBatch(ids), windowMs, maxBatchSize);
2684
+ this.staffLoader = new BatchLoader((ids) => this.getStaffBatch(ids), windowMs, maxBatchSize);
2559
2685
  }
2560
2686
  /**
2561
2687
  * The current rate limit information from the last API response.
@@ -2658,6 +2784,17 @@ var AniListClient = class {
2658
2784
  remaining: Number.parseInt(rlRemaining, 10),
2659
2785
  reset: Number.parseInt(rlReset, 10)
2660
2786
  };
2787
+ const remainingPercent = this._rateLimitInfo.remaining / this._rateLimitInfo.limit;
2788
+ if (remainingPercent <= 0.15) {
2789
+ const resetIn = this._rateLimitInfo.reset - Math.floor(Date.now() / 1e3);
2790
+ const resetInSeconds = resetIn > 0 ? resetIn : 0;
2791
+ this.logger?.warn("Smart Alert: AniList API rate limit is critically low", {
2792
+ remaining: this._rateLimitInfo.remaining,
2793
+ limit: this._rateLimitInfo.limit,
2794
+ resetInSeconds
2795
+ });
2796
+ this.hooks.onRateLimitAlert?.(this._rateLimitInfo.remaining, resetInSeconds);
2797
+ }
2661
2798
  }
2662
2799
  const durationMs = Date.now() - start;
2663
2800
  const data = json.data;
@@ -2686,7 +2823,10 @@ var AniListClient = class {
2686
2823
  * @param include - Optional related data to include
2687
2824
  */
2688
2825
  async getMedia(id, include) {
2689
- return getMedia(this, id, include);
2826
+ if (!this.batchingEnabled || include) {
2827
+ return getMedia(this, id, include);
2828
+ }
2829
+ return this.mediaLoader.load(id);
2690
2830
  }
2691
2831
  async getMediaCharacters(mediaId, options = {}) {
2692
2832
  return getMediaCharacters(this, mediaId, options);
@@ -2754,7 +2894,10 @@ var AniListClient = class {
2754
2894
  }
2755
2895
  /** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
2756
2896
  async getCharacter(id, include) {
2757
- return getCharacter(this, id, include);
2897
+ if (!this.batchingEnabled || include) {
2898
+ return getCharacter(this, id, include);
2899
+ }
2900
+ return this.characterLoader.load(id);
2758
2901
  }
2759
2902
  /** Search for characters by name. */
2760
2903
  async searchCharacters(options = {}) {
@@ -2762,7 +2905,10 @@ var AniListClient = class {
2762
2905
  }
2763
2906
  /** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
2764
2907
  async getStaff(id, include) {
2765
- return getStaff(this, id, include);
2908
+ if (!this.batchingEnabled || include) {
2909
+ return getStaff(this, id, include);
2910
+ }
2911
+ return this.staffLoader.load(id);
2766
2912
  }
2767
2913
  /** Search for staff (voice actors, directors, etc.). */
2768
2914
  async searchStaff(options = {}) {
@@ -2876,12 +3022,25 @@ var AniListClient = class {
2876
3022
  page++;
2877
3023
  }
2878
3024
  }
3025
+ /**
3026
+ * Utility to fetch all pages of a paginated request and return them as a single array.
3027
+ *
3028
+ * @param fetchPage - A function that takes a page number and returns a `PagedResult<T>`
3029
+ * @param maxPages - Maximum number of pages to fetch (default: Infinity)
3030
+ */
3031
+ async fetchAll(fetchPage, maxPages = Number.POSITIVE_INFINITY) {
3032
+ const results = [];
3033
+ for await (const item of this.paginate(fetchPage, maxPages)) {
3034
+ results.push(item);
3035
+ }
3036
+ return results;
3037
+ }
2879
3038
  /** Fetch multiple media entries in a single API request. */
2880
3039
  async getMediaBatch(ids) {
2881
3040
  if (ids.length === 0) return [];
2882
3041
  validateIds(ids, "mediaId");
2883
3042
  const [singleMediaId] = ids;
2884
- if (ids.length === 1 && singleMediaId !== void 0) return [await this.getMedia(singleMediaId)];
3043
+ if (ids.length === 1 && singleMediaId !== void 0) return [await getMedia(this, singleMediaId)];
2885
3044
  return this.executeBatch(ids, buildBatchMediaQuery, "m");
2886
3045
  }
2887
3046
  /** Fetch multiple characters in a single API request. */
@@ -2889,7 +3048,8 @@ var AniListClient = class {
2889
3048
  if (ids.length === 0) return [];
2890
3049
  validateIds(ids, "characterId");
2891
3050
  const [singleCharId] = ids;
2892
- if (ids.length === 1 && singleCharId !== void 0) return [await this.getCharacter(singleCharId)];
3051
+ if (ids.length === 1 && singleCharId !== void 0)
3052
+ return [await getCharacter(this, singleCharId)];
2893
3053
  return this.executeBatch(ids, buildBatchCharacterQuery, "c");
2894
3054
  }
2895
3055
  /** Fetch multiple staff members in a single API request. */
@@ -2897,7 +3057,7 @@ var AniListClient = class {
2897
3057
  if (ids.length === 0) return [];
2898
3058
  validateIds(ids, "staffId");
2899
3059
  const [singleStaffId] = ids;
2900
- if (ids.length === 1 && singleStaffId !== void 0) return [await this.getStaff(singleStaffId)];
3060
+ if (ids.length === 1 && singleStaffId !== void 0) return [await getStaff(this, singleStaffId)];
2901
3061
  return this.executeBatch(ids, buildBatchStaffQuery, "s");
2902
3062
  }
2903
3063
  /** @internal */
@@ -2962,6 +3122,6 @@ var AniListClient = class {
2962
3122
  }
2963
3123
  };
2964
3124
 
2965
- export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaSource, MediaStatus, MediaType, MemoryCache, NormalizedCache, RateLimiter, RecommendationSort, RedisCache, ReviewSort, StaffSort, StudioSort, ThreadSort, UserSort, parseAniListMarkdown };
3125
+ export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, fragments_exports as Fragments, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaSource, MediaStatus, MediaType, MemoryCache, NormalizedCache, RateLimiter, RecommendationSort, RedisCache, ReviewSort, StaffSort, StudioSort, ThreadSort, UserSort, parseAniListMarkdown };
2966
3126
  //# sourceMappingURL=index.mjs.map
2967
3127
  //# sourceMappingURL=index.mjs.map