ani-client 2.1.1 → 2.1.3

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
@@ -122,6 +122,212 @@ function sortObjectKeys(obj) {
122
122
  return sorted;
123
123
  }
124
124
 
125
+ // src/cache/normalized.ts
126
+ var NormalizedCache = class {
127
+ ttl;
128
+ maxSize;
129
+ enabled;
130
+ swrMs;
131
+ queryStore = /* @__PURE__ */ new Map();
132
+ entityStore = /* @__PURE__ */ new Map();
133
+ _hits = 0;
134
+ _misses = 0;
135
+ _stales = 0;
136
+ constructor(options = {}) {
137
+ this.ttl = options.ttl ?? 24 * 60 * 60 * 1e3;
138
+ this.maxSize = options.maxSize ?? 500;
139
+ this.enabled = options.enabled ?? true;
140
+ this.swrMs = options.staleWhileRevalidateMs ?? 0;
141
+ }
142
+ static key(query, variables) {
143
+ const normalized = normalizeQuery(query);
144
+ return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
145
+ }
146
+ /** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
147
+ normalize(data, seen = /* @__PURE__ */ new WeakSet()) {
148
+ if (Array.isArray(data)) {
149
+ if (seen.has(data)) return null;
150
+ seen.add(data);
151
+ return data.map((item) => this.normalize(item, seen));
152
+ }
153
+ if (data !== null && typeof data === "object") {
154
+ if (seen.has(data)) return null;
155
+ seen.add(data);
156
+ const obj = data;
157
+ if (typeof obj.__typename === "string" && (typeof obj.id === "number" || typeof obj.id === "string")) {
158
+ const ref = `${obj.__typename}:${obj.id}`;
159
+ const normalizedObj = {};
160
+ for (const [k, v] of Object.entries(obj)) {
161
+ normalizedObj[k] = this.normalize(v, seen);
162
+ }
163
+ const existing = this.entityStore.get(ref) || {};
164
+ this.entityStore.set(ref, { ...existing, ...normalizedObj });
165
+ return { __ref: ref };
166
+ }
167
+ const result = {};
168
+ for (const [k, v] of Object.entries(obj)) {
169
+ result[k] = this.normalize(v, seen);
170
+ }
171
+ return result;
172
+ }
173
+ return data;
174
+ }
175
+ /** Reconstructs a GraphQL response from references. */
176
+ denormalize(data, seen = /* @__PURE__ */ new Set()) {
177
+ if (Array.isArray(data)) {
178
+ return data.map((item) => this.denormalize(item, seen));
179
+ }
180
+ if (data !== null && typeof data === "object") {
181
+ const obj = data;
182
+ if (typeof obj.__ref === "string") {
183
+ const ref = obj.__ref;
184
+ if (seen.has(ref)) {
185
+ return { __ref: ref };
186
+ }
187
+ seen.add(ref);
188
+ const entity = this.entityStore.get(ref);
189
+ if (!entity) return void 0;
190
+ const result2 = this.denormalize(entity, seen);
191
+ seen.delete(ref);
192
+ return result2;
193
+ }
194
+ const result = {};
195
+ for (const [k, v] of Object.entries(obj)) {
196
+ const denormalized = this.denormalize(v, seen);
197
+ if (denormalized === void 0) return void 0;
198
+ result[k] = denormalized;
199
+ }
200
+ return result;
201
+ }
202
+ return data;
203
+ }
204
+ getWithMeta(key) {
205
+ if (!this.enabled) return void 0;
206
+ const entry = this.queryStore.get(key);
207
+ if (!entry) {
208
+ this._misses++;
209
+ return void 0;
210
+ }
211
+ const now = Date.now();
212
+ let isStale = false;
213
+ if (now > entry.expiresAt) {
214
+ if (this.swrMs > 0 && now <= entry.expiresAt + this.swrMs) {
215
+ isStale = true;
216
+ } else {
217
+ this.queryStore.delete(key);
218
+ this._misses++;
219
+ return void 0;
220
+ }
221
+ }
222
+ const denormalized = this.denormalize(entry.data);
223
+ if (denormalized === void 0) {
224
+ this.queryStore.delete(key);
225
+ this._misses++;
226
+ return void 0;
227
+ }
228
+ this.queryStore.delete(key);
229
+ this.queryStore.set(key, entry);
230
+ if (isStale) {
231
+ this._stales++;
232
+ } else {
233
+ this._hits++;
234
+ }
235
+ return { data: denormalized, stale: isStale };
236
+ }
237
+ get(key) {
238
+ const res = this.getWithMeta(key);
239
+ return res ? res.data : void 0;
240
+ }
241
+ set(key, data) {
242
+ if (!this.enabled) return;
243
+ const normalizedData = this.normalize(data);
244
+ this.queryStore.delete(key);
245
+ if (this.maxSize > 0 && this.queryStore.size >= this.maxSize) {
246
+ const firstKey = this.queryStore.keys().next().value;
247
+ if (firstKey !== void 0) this.queryStore.delete(firstKey);
248
+ this.gc();
249
+ }
250
+ this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
251
+ }
252
+ delete(key) {
253
+ return this.queryStore.delete(key);
254
+ }
255
+ clear() {
256
+ this.queryStore.clear();
257
+ this.entityStore.clear();
258
+ this._hits = 0;
259
+ this._misses = 0;
260
+ this._stales = 0;
261
+ }
262
+ get size() {
263
+ return this.queryStore.size;
264
+ }
265
+ keys() {
266
+ return [...this.queryStore.keys()];
267
+ }
268
+ invalidate(pattern) {
269
+ const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
270
+ const toDelete = [];
271
+ for (const key of this.queryStore.keys()) {
272
+ if (test(key)) toDelete.push(key);
273
+ }
274
+ for (const key of toDelete) this.queryStore.delete(key);
275
+ return toDelete.length;
276
+ }
277
+ get stats() {
278
+ const total = this._hits + this._misses + this._stales;
279
+ return {
280
+ hits: this._hits,
281
+ misses: this._misses,
282
+ stales: this._stales,
283
+ hitRate: total === 0 ? Number.NaN : this._hits / total,
284
+ entitiesCount: this.entityStore.size
285
+ };
286
+ }
287
+ resetStats() {
288
+ this._hits = 0;
289
+ this._misses = 0;
290
+ this._stales = 0;
291
+ }
292
+ /**
293
+ * Garbage-collect orphaned entities that are no longer referenced by any query.
294
+ * Called automatically on LRU eviction to prevent unbounded entity store growth.
295
+ */
296
+ gc() {
297
+ const referencedRefs = /* @__PURE__ */ new Set();
298
+ const collectRefs = (data) => {
299
+ if (Array.isArray(data)) {
300
+ for (const item of data) collectRefs(item);
301
+ return;
302
+ }
303
+ if (data !== null && typeof data === "object") {
304
+ const obj = data;
305
+ if (typeof obj.__ref === "string") {
306
+ referencedRefs.add(obj.__ref);
307
+ const entity = this.entityStore.get(obj.__ref);
308
+ if (entity && !referencedRefs.has(`_visited:${obj.__ref}`)) {
309
+ referencedRefs.add(`_visited:${obj.__ref}`);
310
+ collectRefs(entity);
311
+ }
312
+ return;
313
+ }
314
+ for (const v of Object.values(obj)) collectRefs(v);
315
+ }
316
+ };
317
+ for (const entry of this.queryStore.values()) {
318
+ collectRefs(entry.data);
319
+ }
320
+ let removed = 0;
321
+ for (const ref of this.entityStore.keys()) {
322
+ if (!referencedRefs.has(ref)) {
323
+ this.entityStore.delete(ref);
324
+ removed++;
325
+ }
326
+ }
327
+ return removed;
328
+ }
329
+ };
330
+
125
331
  // src/cache/index.ts
126
332
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
127
333
  var MemoryCache = class {
@@ -145,11 +351,11 @@ var MemoryCache = class {
145
351
  return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
146
352
  }
147
353
  /**
148
- * Retrieve a cached value, or `undefined` if missing / expired.
354
+ * Retrieve a cached value and its stale status.
149
355
  * With stale-while-revalidate enabled, returns stale data within the grace window
150
356
  * and flags it so the caller can refresh in the background.
151
357
  */
152
- get(key) {
358
+ getWithMeta(key) {
153
359
  if (!this.enabled) return void 0;
154
360
  const entry = this.store.get(key);
155
361
  if (!entry) {
@@ -162,7 +368,7 @@ var MemoryCache = class {
162
368
  this.store.delete(key);
163
369
  this.store.set(key, entry);
164
370
  this._stales++;
165
- return entry.data;
371
+ return { data: entry.data, stale: true };
166
372
  }
167
373
  this.store.delete(key);
168
374
  this._misses++;
@@ -171,7 +377,11 @@ var MemoryCache = class {
171
377
  this.store.delete(key);
172
378
  this.store.set(key, entry);
173
379
  this._hits++;
174
- return entry.data;
380
+ return { data: entry.data, stale: false };
381
+ }
382
+ get(key) {
383
+ const res = this.getWithMeta(key);
384
+ return res ? res.data : void 0;
175
385
  }
176
386
  /** Store a value in the cache. */
177
387
  set(key, data) {
@@ -357,6 +567,7 @@ var AniListError = class _AniListError extends Error {
357
567
 
358
568
  // src/queries/fragments.ts
359
569
  var MEDIA_FIELDS_LIGHT = `
570
+ __typename
360
571
  id
361
572
  idMal
362
573
  title { romaji english native userPreferred }
@@ -383,6 +594,7 @@ var MEDIA_FIELDS_LIGHT = `
383
594
  }
384
595
  `;
385
596
  var MEDIA_FIELDS_BASE = `
597
+ __typename
386
598
  id
387
599
  idMal
388
600
  title { romaji english native userPreferred }
@@ -429,6 +641,7 @@ var RELATIONS_FIELDS = `
429
641
  edges {
430
642
  relationType(version: 2)
431
643
  node {
644
+ __typename
432
645
  id
433
646
  title { romaji english native userPreferred }
434
647
  type
@@ -459,9 +672,11 @@ var RELATIONS_FIELDS = `
459
672
  }
460
673
  `;
461
674
  var MEDIA_RECOMMENDATION_FIELDS = `
675
+ __typename
462
676
  id
463
677
  rating
464
678
  mediaRecommendation {
679
+ __typename
465
680
  id
466
681
  title { romaji english native userPreferred }
467
682
  type
@@ -493,6 +708,7 @@ var MEDIA_FIELDS = `
493
708
  ${RELATIONS_FIELDS}
494
709
  `;
495
710
  var CHARACTER_FIELDS_COMPACT = `
711
+ __typename
496
712
  id
497
713
  name { first middle last full native alternative }
498
714
  image { large medium }
@@ -507,6 +723,7 @@ var CHARACTER_FIELDS_COMPACT = `
507
723
  var CHARACTER_MEDIA_NODES = `
508
724
  media(perPage: 10) {
509
725
  nodes {
726
+ __typename
510
727
  id
511
728
  title { romaji english native userPreferred }
512
729
  type
@@ -516,6 +733,7 @@ var CHARACTER_MEDIA_NODES = `
516
733
  }
517
734
  `;
518
735
  var VOICE_ACTOR_FIELDS_COMPACT = `
736
+ __typename
519
737
  id
520
738
  name { first middle last full native userPreferred }
521
739
  languageV2
@@ -531,6 +749,7 @@ var CHARACTER_MEDIA_EDGES_WITH_VA = `
531
749
  ${VOICE_ACTOR_FIELDS_COMPACT}
532
750
  }
533
751
  node {
752
+ __typename
534
753
  id
535
754
  title { romaji english native userPreferred }
536
755
  type
@@ -549,6 +768,7 @@ var CHARACTER_FIELDS_WITH_VA = `
549
768
  ${CHARACTER_MEDIA_EDGES_WITH_VA}
550
769
  `;
551
770
  var STAFF_FIELDS = `
771
+ __typename
552
772
  id
553
773
  name { first middle last full native }
554
774
  language
@@ -568,6 +788,7 @@ var STAFF_FIELDS = `
568
788
  var STAFF_MEDIA_FIELDS = `
569
789
  staffMedia(perPage: $perPage, sort: [POPULARITY_DESC]) {
570
790
  nodes {
791
+ __typename
571
792
  id
572
793
  title { romaji english native userPreferred }
573
794
  type
@@ -606,6 +827,7 @@ var STAFF_MEDIA_FIELDS = `
606
827
  }
607
828
  `;
608
829
  var USER_FIELDS = `
830
+ __typename
609
831
  id
610
832
  name
611
833
  about(asHtml: false)
@@ -626,6 +848,7 @@ var USER_FAVORITES_FIELDS = `
626
848
  favourites {
627
849
  anime(perPage: 25) {
628
850
  nodes {
851
+ __typename
629
852
  id
630
853
  title { romaji english native userPreferred }
631
854
  coverImage { large medium }
@@ -636,6 +859,7 @@ var USER_FAVORITES_FIELDS = `
636
859
  }
637
860
  manga(perPage: 25) {
638
861
  nodes {
862
+ __typename
639
863
  id
640
864
  title { romaji english native userPreferred }
641
865
  coverImage { large medium }
@@ -646,6 +870,7 @@ var USER_FAVORITES_FIELDS = `
646
870
  }
647
871
  characters(perPage: 25) {
648
872
  nodes {
873
+ __typename
649
874
  id
650
875
  name { full native }
651
876
  image { large medium }
@@ -654,6 +879,7 @@ var USER_FAVORITES_FIELDS = `
654
879
  }
655
880
  staff(perPage: 25) {
656
881
  nodes {
882
+ __typename
657
883
  id
658
884
  name { full native }
659
885
  image { large medium }
@@ -662,6 +888,7 @@ var USER_FAVORITES_FIELDS = `
662
888
  }
663
889
  studios(perPage: 25) {
664
890
  nodes {
891
+ __typename
665
892
  id
666
893
  name
667
894
  siteUrl
@@ -670,6 +897,7 @@ var USER_FAVORITES_FIELDS = `
670
897
  }
671
898
  `;
672
899
  var MEDIA_LIST_FIELDS = `
900
+ __typename
673
901
  id
674
902
  mediaId
675
903
  status
@@ -689,6 +917,7 @@ var MEDIA_LIST_FIELDS = `
689
917
  }
690
918
  `;
691
919
  var STUDIO_FIELDS = `
920
+ __typename
692
921
  id
693
922
  name
694
923
  isAnimationStudio
@@ -697,6 +926,7 @@ var STUDIO_FIELDS = `
697
926
  media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
698
927
  pageInfo { total perPage currentPage lastPage hasNextPage }
699
928
  nodes {
929
+ __typename
700
930
  id
701
931
  title { romaji english native userPreferred }
702
932
  type
@@ -707,6 +937,7 @@ var STUDIO_FIELDS = `
707
937
  }
708
938
  `;
709
939
  var THREAD_FIELDS = `
940
+ __typename
710
941
  id
711
942
  title
712
943
  body(asHtml: false)
@@ -723,20 +954,24 @@ var THREAD_FIELDS = `
723
954
  updatedAt
724
955
  siteUrl
725
956
  user {
957
+ __typename
726
958
  id
727
959
  name
728
960
  avatar { large medium }
729
961
  }
730
962
  replyUser {
963
+ __typename
731
964
  id
732
965
  name
733
966
  avatar { large medium }
734
967
  }
735
968
  categories {
969
+ __typename
736
970
  id
737
971
  name
738
972
  }
739
973
  mediaCategories {
974
+ __typename
740
975
  id
741
976
  title { romaji english native userPreferred }
742
977
  type
@@ -744,6 +979,7 @@ var THREAD_FIELDS = `
744
979
  siteUrl
745
980
  }
746
981
  likes {
982
+ __typename
747
983
  id
748
984
  name
749
985
  }
@@ -1987,7 +2223,11 @@ async function getReview(client, id) {
1987
2223
  }
1988
2224
  async function searchReviews(client, options = {}) {
1989
2225
  const { mediaId, userId, sort, page = 1, perPage = 20 } = options;
1990
- return client.pagedRequest(QUERY_REVIEWS, { mediaId, userId, sort, page, perPage }, "reviews");
2226
+ return client.pagedRequest(
2227
+ QUERY_REVIEWS,
2228
+ { mediaId, userId, sort, page, perPage: clampPerPage(perPage) },
2229
+ "reviews"
2230
+ );
1991
2231
  }
1992
2232
 
1993
2233
  // src/client/staff.ts
@@ -2121,7 +2361,7 @@ function mapFavorites(fav) {
2121
2361
 
2122
2362
  // src/client/index.ts
2123
2363
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2124
- var LIB_VERSION = "2.1.1" ;
2364
+ var LIB_VERSION = "2.1.3" ;
2125
2365
  var AniListClient = class {
2126
2366
  apiUrl;
2127
2367
  headers;
@@ -2166,10 +2406,25 @@ var AniListClient = class {
2166
2406
  /** @internal */
2167
2407
  async request(query, variables = {}) {
2168
2408
  const cacheKey = MemoryCache.key(query, variables);
2169
- const cached = await this.cacheAdapter.get(cacheKey);
2409
+ let cached;
2410
+ let isStale = false;
2411
+ if (this.cacheAdapter.getWithMeta) {
2412
+ const res = await this.cacheAdapter.getWithMeta(cacheKey);
2413
+ if (res) {
2414
+ cached = res.data;
2415
+ isStale = res.stale;
2416
+ }
2417
+ } else {
2418
+ cached = await this.cacheAdapter.get(cacheKey);
2419
+ }
2170
2420
  if (cached !== void 0) {
2421
+ if (isStale) {
2422
+ this.executeRequest(query, variables, cacheKey).catch((err) => {
2423
+ this.logger?.error("Background revalidation failed", { error: err.message, cacheKey });
2424
+ });
2425
+ }
2171
2426
  this.hooks.onCacheHit?.(cacheKey);
2172
- this.logger?.debug("Cache hit", { cacheKey });
2427
+ this.logger?.debug("Cache hit", { cacheKey, isStale });
2173
2428
  const meta = { durationMs: 0, fromCache: true };
2174
2429
  this._lastRequestMeta = meta;
2175
2430
  this.hooks.onResponse?.(query, 0, true);
@@ -2319,8 +2574,8 @@ var AniListClient = class {
2319
2574
  return getMediaByMalId(this, malId, type);
2320
2575
  }
2321
2576
  /** Get the detailed schedule for the current week, sorted by day. */
2322
- async getWeeklySchedule(date) {
2323
- return getWeeklySchedule(this, date);
2577
+ async getWeeklySchedule(date, idNotIn) {
2578
+ return getWeeklySchedule(this, date, idNotIn);
2324
2579
  }
2325
2580
  /** Get upcoming (not yet released) media. */
2326
2581
  async getPlanning(options = {}) {
@@ -2544,6 +2799,6 @@ var AniListClient = class {
2544
2799
  }
2545
2800
  };
2546
2801
 
2547
- export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaSource, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort, RedisCache, ReviewSort, StaffSort, StudioSort, ThreadSort, UserSort, parseAniListMarkdown };
2802
+ 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 };
2548
2803
  //# sourceMappingURL=index.mjs.map
2549
2804
  //# sourceMappingURL=index.mjs.map