ani-client 2.2.1 → 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/dist/index.mjs CHANGED
@@ -1,3 +1,60 @@
1
+ // src/utils/dataloader.ts
2
+ var BatchLoader = class {
3
+ constructor(batchFetch, maxWaitMs = 50) {
4
+ this.batchFetch = batchFetch;
5
+ this.maxWaitMs = maxWaitMs;
6
+ }
7
+ queue = /* @__PURE__ */ new Map();
8
+ timeout = null;
9
+ /**
10
+ * Queue an ID to be fetched in the next batch.
11
+ * Returns a Promise that resolves when the batch request completes.
12
+ */
13
+ async load(id) {
14
+ return new Promise((resolve, reject) => {
15
+ let callbacks = this.queue.get(id);
16
+ if (!callbacks) {
17
+ callbacks = [];
18
+ this.queue.set(id, callbacks);
19
+ }
20
+ callbacks.push({ resolve, reject });
21
+ if (this.timeout === null) {
22
+ this.timeout = setTimeout(() => this.dispatch(), this.maxWaitMs);
23
+ if (typeof this.timeout.unref === "function") {
24
+ this.timeout.unref();
25
+ }
26
+ }
27
+ });
28
+ }
29
+ async dispatch() {
30
+ this.timeout = null;
31
+ if (this.queue.size === 0) return;
32
+ const currentQueue = this.queue;
33
+ this.queue = /* @__PURE__ */ new Map();
34
+ const ids = Array.from(currentQueue.keys());
35
+ try {
36
+ const results = await this.batchFetch(ids);
37
+ const resultMap = /* @__PURE__ */ new Map();
38
+ for (const item of results) {
39
+ resultMap.set(item.id, item);
40
+ }
41
+ for (const [id, callbacks] of currentQueue.entries()) {
42
+ const result = resultMap.get(id);
43
+ if (result) {
44
+ for (const cb of callbacks) cb.resolve(result);
45
+ } else {
46
+ const err = new Error(`Item with ID ${id} not found in batch response`);
47
+ for (const cb of callbacks) cb.reject(err);
48
+ }
49
+ }
50
+ } catch (err) {
51
+ for (const callbacks of currentQueue.values()) {
52
+ for (const cb of callbacks) cb.reject(err);
53
+ }
54
+ }
55
+ }
56
+ };
57
+
1
58
  // src/utils/markdown.ts
2
59
  function isSafeUrl(url) {
3
60
  return /^https?:\/\//i.test(url);
@@ -115,8 +172,17 @@ function validateIds(ids, label = "id") {
115
172
  function sortObjectKeys(obj) {
116
173
  if (obj === null || typeof obj !== "object") return obj;
117
174
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
175
+ const keys = Object.keys(obj);
176
+ if (keys.length === 0) return obj;
177
+ if (keys.length === 1) {
178
+ const key = keys[0];
179
+ const val = obj[key];
180
+ const sortedVal = sortObjectKeys(val);
181
+ if (val === sortedVal) return obj;
182
+ return { [key]: sortedVal };
183
+ }
118
184
  const sorted = {};
119
- for (const key of Object.keys(obj).sort()) {
185
+ for (const key of keys.sort()) {
120
186
  sorted[key] = sortObjectKeys(obj[key]);
121
187
  }
122
188
  return sorted;
@@ -130,10 +196,10 @@ var NormalizedCache = class {
130
196
  swrMs;
131
197
  queryStore = /* @__PURE__ */ new Map();
132
198
  entityStore = /* @__PURE__ */ new Map();
199
+ refCount = /* @__PURE__ */ new Map();
133
200
  _hits = 0;
134
201
  _misses = 0;
135
202
  _stales = 0;
136
- gcTimeout;
137
203
  constructor(options = {}) {
138
204
  this.ttl = options.ttl ?? 24 * 60 * 60 * 1e3;
139
205
  this.maxSize = options.maxSize ?? 500;
@@ -145,11 +211,11 @@ var NormalizedCache = class {
145
211
  return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
146
212
  }
147
213
  /** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
148
- normalize(data, seen = /* @__PURE__ */ new WeakSet()) {
214
+ normalize(data, refsOut, seen = /* @__PURE__ */ new WeakSet()) {
149
215
  if (Array.isArray(data)) {
150
216
  if (seen.has(data)) return null;
151
217
  seen.add(data);
152
- return data.map((item) => this.normalize(item, seen));
218
+ return data.map((item) => this.normalize(item, refsOut, seen));
153
219
  }
154
220
  if (data !== null && typeof data === "object") {
155
221
  if (seen.has(data)) return null;
@@ -157,17 +223,22 @@ var NormalizedCache = class {
157
223
  const obj = data;
158
224
  if (typeof obj.__typename === "string" && (typeof obj.id === "number" || typeof obj.id === "string")) {
159
225
  const ref = `${obj.__typename}:${obj.id}`;
226
+ refsOut.add(ref);
160
227
  const normalizedObj = {};
161
- for (const [k, v] of Object.entries(obj)) {
162
- normalizedObj[k] = this.normalize(v, seen);
228
+ for (const k in obj) {
229
+ if (Object.hasOwn(obj, k)) {
230
+ normalizedObj[k] = this.normalize(obj[k], refsOut, seen);
231
+ }
163
232
  }
164
233
  const existing = this.entityStore.get(ref) || {};
165
234
  this.entityStore.set(ref, { ...existing, ...normalizedObj });
166
235
  return { __ref: ref };
167
236
  }
168
237
  const result = {};
169
- for (const [k, v] of Object.entries(obj)) {
170
- result[k] = this.normalize(v, seen);
238
+ for (const k in obj) {
239
+ if (Object.hasOwn(obj, k)) {
240
+ result[k] = this.normalize(obj[k], refsOut, seen);
241
+ }
171
242
  }
172
243
  return result;
173
244
  }
@@ -193,10 +264,12 @@ var NormalizedCache = class {
193
264
  return result2;
194
265
  }
195
266
  const result = {};
196
- for (const [k, v] of Object.entries(obj)) {
197
- const denormalized = this.denormalize(v, seen);
198
- if (denormalized === void 0) return void 0;
199
- result[k] = denormalized;
267
+ for (const k in obj) {
268
+ if (Object.hasOwn(obj, k)) {
269
+ const denormalized = this.denormalize(obj[k], seen);
270
+ if (denormalized === void 0) return void 0;
271
+ result[k] = denormalized;
272
+ }
200
273
  }
201
274
  return result;
202
275
  }
@@ -215,14 +288,14 @@ var NormalizedCache = class {
215
288
  if (this.swrMs > 0 && now <= entry.expiresAt + this.swrMs) {
216
289
  isStale = true;
217
290
  } else {
218
- this.queryStore.delete(key);
291
+ this.deleteQueryEntry(key, entry);
219
292
  this._misses++;
220
293
  return void 0;
221
294
  }
222
295
  }
223
296
  const denormalized = this.denormalize(entry.data);
224
297
  if (denormalized === void 0) {
225
- this.queryStore.delete(key);
298
+ this.deleteQueryEntry(key, entry);
226
299
  this._misses++;
227
300
  return void 0;
228
301
  }
@@ -241,27 +314,46 @@ var NormalizedCache = class {
241
314
  }
242
315
  set(key, data) {
243
316
  if (!this.enabled) return;
244
- const normalizedData = this.normalize(data);
245
- this.queryStore.delete(key);
317
+ const refs = /* @__PURE__ */ new Set();
318
+ const normalizedData = this.normalize(data, refs);
319
+ const existing = this.queryStore.get(key);
320
+ if (existing) {
321
+ this.deleteQueryEntry(key, existing);
322
+ }
246
323
  if (this.maxSize > 0 && this.queryStore.size >= this.maxSize) {
247
324
  const firstKey = this.queryStore.keys().next().value;
248
325
  if (firstKey !== void 0) {
249
- this.queryStore.delete(firstKey);
326
+ const firstEntry = this.queryStore.get(firstKey);
327
+ if (firstEntry) this.deleteQueryEntry(firstKey, firstEntry);
328
+ }
329
+ }
330
+ for (const ref of refs) {
331
+ this.refCount.set(ref, (this.refCount.get(ref) ?? 0) + 1);
332
+ }
333
+ this.queryStore.set(key, { data: normalizedData, refs, expiresAt: Date.now() + this.ttl });
334
+ }
335
+ deleteQueryEntry(key, entry) {
336
+ this.queryStore.delete(key);
337
+ for (const ref of entry.refs) {
338
+ const count = (this.refCount.get(ref) ?? 0) - 1;
339
+ if (count <= 0) {
340
+ this.refCount.delete(ref);
341
+ this.entityStore.delete(ref);
342
+ } else {
343
+ this.refCount.set(ref, count);
250
344
  }
251
- this.scheduleGc();
252
345
  }
253
- this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
254
346
  }
255
347
  delete(key) {
256
- return this.queryStore.delete(key);
348
+ const entry = this.queryStore.get(key);
349
+ if (!entry) return false;
350
+ this.deleteQueryEntry(key, entry);
351
+ return true;
257
352
  }
258
353
  clear() {
259
- if (this.gcTimeout) {
260
- clearTimeout(this.gcTimeout);
261
- this.gcTimeout = void 0;
262
- }
263
354
  this.queryStore.clear();
264
355
  this.entityStore.clear();
356
+ this.refCount.clear();
265
357
  this._hits = 0;
266
358
  this._misses = 0;
267
359
  this._stales = 0;
@@ -278,7 +370,9 @@ var NormalizedCache = class {
278
370
  for (const key of this.queryStore.keys()) {
279
371
  if (test(key)) toDelete.push(key);
280
372
  }
281
- for (const key of toDelete) this.queryStore.delete(key);
373
+ for (const key of toDelete) {
374
+ this.delete(key);
375
+ }
282
376
  return toDelete.length;
283
377
  }
284
378
  get stats() {
@@ -296,53 +390,6 @@ var NormalizedCache = class {
296
390
  this._misses = 0;
297
391
  this._stales = 0;
298
392
  }
299
- scheduleGc() {
300
- if (this.gcTimeout) return;
301
- this.gcTimeout = setTimeout(() => {
302
- this.gc();
303
- this.gcTimeout = void 0;
304
- }, 500);
305
- if (typeof this.gcTimeout.unref === "function") {
306
- this.gcTimeout.unref();
307
- }
308
- }
309
- /**
310
- * Garbage-collect orphaned entities that are no longer referenced by any query.
311
- * Called automatically on LRU eviction to prevent unbounded entity store growth.
312
- */
313
- gc() {
314
- const referencedRefs = /* @__PURE__ */ new Set();
315
- const collectRefs = (data) => {
316
- if (Array.isArray(data)) {
317
- for (const item of data) collectRefs(item);
318
- return;
319
- }
320
- if (data !== null && typeof data === "object") {
321
- const obj = data;
322
- if (typeof obj.__ref === "string") {
323
- referencedRefs.add(obj.__ref);
324
- const entity = this.entityStore.get(obj.__ref);
325
- if (entity && !referencedRefs.has(`_visited:${obj.__ref}`)) {
326
- referencedRefs.add(`_visited:${obj.__ref}`);
327
- collectRefs(entity);
328
- }
329
- return;
330
- }
331
- for (const v of Object.values(obj)) collectRefs(v);
332
- }
333
- };
334
- for (const entry of this.queryStore.values()) {
335
- collectRefs(entry.data);
336
- }
337
- let removed = 0;
338
- for (const ref of this.entityStore.keys()) {
339
- if (!referencedRefs.has(ref)) {
340
- this.entityStore.delete(ref);
341
- removed++;
342
- }
343
- }
344
- return removed;
345
- }
346
393
  };
347
394
 
348
395
  // src/cache/index.ts
@@ -2539,7 +2586,7 @@ function mapFavorites(fav) {
2539
2586
 
2540
2587
  // src/client/index.ts
2541
2588
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2542
- var LIB_VERSION = "2.2.1" ;
2589
+ var LIB_VERSION = "2.4.0" ;
2543
2590
  var AniListClient = class {
2544
2591
  apiUrl;
2545
2592
  headers;
@@ -2551,6 +2598,9 @@ var AniListClient = class {
2551
2598
  inFlight = /* @__PURE__ */ new Map();
2552
2599
  _rateLimitInfo;
2553
2600
  _lastRequestMeta;
2601
+ mediaLoader;
2602
+ characterLoader;
2603
+ staffLoader;
2554
2604
  constructor(options = {}) {
2555
2605
  this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
2556
2606
  this.headers = {
@@ -2566,6 +2616,9 @@ var AniListClient = class {
2566
2616
  this.hooks = options.hooks ?? {};
2567
2617
  this.logger = options.logger;
2568
2618
  this.signal = options.signal;
2619
+ this.mediaLoader = new BatchLoader((ids) => this.getMediaBatch(ids), 50);
2620
+ this.characterLoader = new BatchLoader((ids) => this.getCharacterBatch(ids), 50);
2621
+ this.staffLoader = new BatchLoader((ids) => this.getStaffBatch(ids), 50);
2569
2622
  }
2570
2623
  /**
2571
2624
  * The current rate limit information from the last API response.
@@ -2696,6 +2749,9 @@ var AniListClient = class {
2696
2749
  * @param include - Optional related data to include
2697
2750
  */
2698
2751
  async getMedia(id, include) {
2752
+ if (!include) {
2753
+ return this.mediaLoader.load(id);
2754
+ }
2699
2755
  return getMedia(this, id, include);
2700
2756
  }
2701
2757
  async getMediaCharacters(mediaId, options = {}) {
@@ -2764,6 +2820,9 @@ var AniListClient = class {
2764
2820
  }
2765
2821
  /** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
2766
2822
  async getCharacter(id, include) {
2823
+ if (!include) {
2824
+ return this.characterLoader.load(id);
2825
+ }
2767
2826
  return getCharacter(this, id, include);
2768
2827
  }
2769
2828
  /** Search for characters by name. */
@@ -2772,6 +2831,9 @@ var AniListClient = class {
2772
2831
  }
2773
2832
  /** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
2774
2833
  async getStaff(id, include) {
2834
+ if (!include) {
2835
+ return this.staffLoader.load(id);
2836
+ }
2775
2837
  return getStaff(this, id, include);
2776
2838
  }
2777
2839
  /** Search for staff (voice actors, directors, etc.). */
@@ -2891,7 +2953,7 @@ var AniListClient = class {
2891
2953
  if (ids.length === 0) return [];
2892
2954
  validateIds(ids, "mediaId");
2893
2955
  const [singleMediaId] = ids;
2894
- if (ids.length === 1 && singleMediaId !== void 0) return [await this.getMedia(singleMediaId)];
2956
+ if (ids.length === 1 && singleMediaId !== void 0) return [await getMedia(this, singleMediaId)];
2895
2957
  return this.executeBatch(ids, buildBatchMediaQuery, "m");
2896
2958
  }
2897
2959
  /** Fetch multiple characters in a single API request. */
@@ -2899,7 +2961,8 @@ var AniListClient = class {
2899
2961
  if (ids.length === 0) return [];
2900
2962
  validateIds(ids, "characterId");
2901
2963
  const [singleCharId] = ids;
2902
- if (ids.length === 1 && singleCharId !== void 0) return [await this.getCharacter(singleCharId)];
2964
+ if (ids.length === 1 && singleCharId !== void 0)
2965
+ return [await getCharacter(this, singleCharId)];
2903
2966
  return this.executeBatch(ids, buildBatchCharacterQuery, "c");
2904
2967
  }
2905
2968
  /** Fetch multiple staff members in a single API request. */
@@ -2907,7 +2970,7 @@ var AniListClient = class {
2907
2970
  if (ids.length === 0) return [];
2908
2971
  validateIds(ids, "staffId");
2909
2972
  const [singleStaffId] = ids;
2910
- if (ids.length === 1 && singleStaffId !== void 0) return [await this.getStaff(singleStaffId)];
2973
+ if (ids.length === 1 && singleStaffId !== void 0) return [await getStaff(this, singleStaffId)];
2911
2974
  return this.executeBatch(ids, buildBatchStaffQuery, "s");
2912
2975
  }
2913
2976
  /** @internal */