ani-client 2.3.0 → 2.4.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/README.md CHANGED
@@ -18,7 +18,8 @@
18
18
  - **LRU cache** with TTL, stale-while-revalidate, and hit/miss stats
19
19
  - **Rate-limit protection** with exponential backoff, retries, and custom strategies
20
20
  - **Request deduplication** — concurrent identical queries share a single in-flight request
21
- - **Batch queries** — fetch up to 50 media, characters, or staff in a single API call
21
+ - **Automatic batching (DataLoader)** — groups rapid, individual `getMedia(id)` calls within a 50ms window into a single batch query, saving API limits.
22
+ - **Batch queries** — manually fetch up to 50 media, characters, or staff in a single API call
22
23
  - **Auto-pagination** — async iterator that yields items across all pages
23
24
  - **AbortSignal support** — cancel globally or per-request via `withSignal()`
24
25
  - **Injectable logger** — plug in `console`, pino, winston, or any compatible logger
package/dist/index.d.mts CHANGED
@@ -1069,6 +1069,9 @@ declare class AniListClient implements ClientBase {
1069
1069
  private readonly inFlight;
1070
1070
  private _rateLimitInfo?;
1071
1071
  private _lastRequestMeta?;
1072
+ private readonly mediaLoader;
1073
+ private readonly characterLoader;
1074
+ private readonly staffLoader;
1072
1075
  constructor(options?: AniListClientOptions);
1073
1076
  /**
1074
1077
  * The current rate limit information from the last API response.
package/dist/index.d.ts CHANGED
@@ -1069,6 +1069,9 @@ declare class AniListClient implements ClientBase {
1069
1069
  private readonly inFlight;
1070
1070
  private _rateLimitInfo?;
1071
1071
  private _lastRequestMeta?;
1072
+ private readonly mediaLoader;
1073
+ private readonly characterLoader;
1074
+ private readonly staffLoader;
1072
1075
  constructor(options?: AniListClientOptions);
1073
1076
  /**
1074
1077
  * The current rate limit information from the last API response.
package/dist/index.js CHANGED
@@ -1,5 +1,62 @@
1
1
  'use strict';
2
2
 
3
+ // src/utils/dataloader.ts
4
+ var BatchLoader = class {
5
+ constructor(batchFetch, maxWaitMs = 50) {
6
+ this.batchFetch = batchFetch;
7
+ this.maxWaitMs = maxWaitMs;
8
+ }
9
+ queue = /* @__PURE__ */ new Map();
10
+ timeout = null;
11
+ /**
12
+ * Queue an ID to be fetched in the next batch.
13
+ * Returns a Promise that resolves when the batch request completes.
14
+ */
15
+ async load(id) {
16
+ return new Promise((resolve, reject) => {
17
+ let callbacks = this.queue.get(id);
18
+ if (!callbacks) {
19
+ callbacks = [];
20
+ this.queue.set(id, callbacks);
21
+ }
22
+ callbacks.push({ resolve, reject });
23
+ if (this.timeout === null) {
24
+ this.timeout = setTimeout(() => this.dispatch(), this.maxWaitMs);
25
+ if (typeof this.timeout.unref === "function") {
26
+ this.timeout.unref();
27
+ }
28
+ }
29
+ });
30
+ }
31
+ async dispatch() {
32
+ this.timeout = null;
33
+ if (this.queue.size === 0) return;
34
+ const currentQueue = this.queue;
35
+ this.queue = /* @__PURE__ */ new Map();
36
+ const ids = Array.from(currentQueue.keys());
37
+ try {
38
+ const results = await this.batchFetch(ids);
39
+ const resultMap = /* @__PURE__ */ new Map();
40
+ for (const item of results) {
41
+ resultMap.set(item.id, item);
42
+ }
43
+ for (const [id, callbacks] of currentQueue.entries()) {
44
+ const result = resultMap.get(id);
45
+ if (result) {
46
+ for (const cb of callbacks) cb.resolve(result);
47
+ } else {
48
+ const err = new Error(`Item with ID ${id} not found in batch response`);
49
+ for (const cb of callbacks) cb.reject(err);
50
+ }
51
+ }
52
+ } catch (err) {
53
+ for (const callbacks of currentQueue.values()) {
54
+ for (const cb of callbacks) cb.reject(err);
55
+ }
56
+ }
57
+ }
58
+ };
59
+
3
60
  // src/utils/markdown.ts
4
61
  function isSafeUrl(url) {
5
62
  return /^https?:\/\//i.test(url);
@@ -2531,7 +2588,7 @@ function mapFavorites(fav) {
2531
2588
 
2532
2589
  // src/client/index.ts
2533
2590
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2534
- var LIB_VERSION = "2.3.0" ;
2591
+ var LIB_VERSION = "2.4.0" ;
2535
2592
  var AniListClient = class {
2536
2593
  apiUrl;
2537
2594
  headers;
@@ -2543,6 +2600,9 @@ var AniListClient = class {
2543
2600
  inFlight = /* @__PURE__ */ new Map();
2544
2601
  _rateLimitInfo;
2545
2602
  _lastRequestMeta;
2603
+ mediaLoader;
2604
+ characterLoader;
2605
+ staffLoader;
2546
2606
  constructor(options = {}) {
2547
2607
  this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
2548
2608
  this.headers = {
@@ -2558,6 +2618,9 @@ var AniListClient = class {
2558
2618
  this.hooks = options.hooks ?? {};
2559
2619
  this.logger = options.logger;
2560
2620
  this.signal = options.signal;
2621
+ this.mediaLoader = new BatchLoader((ids) => this.getMediaBatch(ids), 50);
2622
+ this.characterLoader = new BatchLoader((ids) => this.getCharacterBatch(ids), 50);
2623
+ this.staffLoader = new BatchLoader((ids) => this.getStaffBatch(ids), 50);
2561
2624
  }
2562
2625
  /**
2563
2626
  * The current rate limit information from the last API response.
@@ -2688,6 +2751,9 @@ var AniListClient = class {
2688
2751
  * @param include - Optional related data to include
2689
2752
  */
2690
2753
  async getMedia(id, include) {
2754
+ if (!include) {
2755
+ return this.mediaLoader.load(id);
2756
+ }
2691
2757
  return getMedia(this, id, include);
2692
2758
  }
2693
2759
  async getMediaCharacters(mediaId, options = {}) {
@@ -2756,6 +2822,9 @@ var AniListClient = class {
2756
2822
  }
2757
2823
  /** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
2758
2824
  async getCharacter(id, include) {
2825
+ if (!include) {
2826
+ return this.characterLoader.load(id);
2827
+ }
2759
2828
  return getCharacter(this, id, include);
2760
2829
  }
2761
2830
  /** Search for characters by name. */
@@ -2764,6 +2833,9 @@ var AniListClient = class {
2764
2833
  }
2765
2834
  /** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
2766
2835
  async getStaff(id, include) {
2836
+ if (!include) {
2837
+ return this.staffLoader.load(id);
2838
+ }
2767
2839
  return getStaff(this, id, include);
2768
2840
  }
2769
2841
  /** Search for staff (voice actors, directors, etc.). */
@@ -2883,7 +2955,7 @@ var AniListClient = class {
2883
2955
  if (ids.length === 0) return [];
2884
2956
  validateIds(ids, "mediaId");
2885
2957
  const [singleMediaId] = ids;
2886
- if (ids.length === 1 && singleMediaId !== void 0) return [await this.getMedia(singleMediaId)];
2958
+ if (ids.length === 1 && singleMediaId !== void 0) return [await getMedia(this, singleMediaId)];
2887
2959
  return this.executeBatch(ids, buildBatchMediaQuery, "m");
2888
2960
  }
2889
2961
  /** Fetch multiple characters in a single API request. */
@@ -2891,7 +2963,8 @@ var AniListClient = class {
2891
2963
  if (ids.length === 0) return [];
2892
2964
  validateIds(ids, "characterId");
2893
2965
  const [singleCharId] = ids;
2894
- if (ids.length === 1 && singleCharId !== void 0) return [await this.getCharacter(singleCharId)];
2966
+ if (ids.length === 1 && singleCharId !== void 0)
2967
+ return [await getCharacter(this, singleCharId)];
2895
2968
  return this.executeBatch(ids, buildBatchCharacterQuery, "c");
2896
2969
  }
2897
2970
  /** Fetch multiple staff members in a single API request. */
@@ -2899,7 +2972,7 @@ var AniListClient = class {
2899
2972
  if (ids.length === 0) return [];
2900
2973
  validateIds(ids, "staffId");
2901
2974
  const [singleStaffId] = ids;
2902
- if (ids.length === 1 && singleStaffId !== void 0) return [await this.getStaff(singleStaffId)];
2975
+ if (ids.length === 1 && singleStaffId !== void 0) return [await getStaff(this, singleStaffId)];
2903
2976
  return this.executeBatch(ids, buildBatchStaffQuery, "s");
2904
2977
  }
2905
2978
  /** @internal */