ani-client 2.1.0 → 2.1.2

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
@@ -124,6 +124,174 @@ function sortObjectKeys(obj) {
124
124
  return sorted;
125
125
  }
126
126
 
127
+ // src/cache/normalized.ts
128
+ var NormalizedCache = class {
129
+ ttl;
130
+ maxSize;
131
+ enabled;
132
+ swrMs;
133
+ queryStore = /* @__PURE__ */ new Map();
134
+ entityStore = /* @__PURE__ */ new Map();
135
+ _hits = 0;
136
+ _misses = 0;
137
+ _stales = 0;
138
+ constructor(options = {}) {
139
+ this.ttl = options.ttl ?? 24 * 60 * 60 * 1e3;
140
+ this.maxSize = options.maxSize ?? 500;
141
+ this.enabled = options.enabled ?? true;
142
+ this.swrMs = options.staleWhileRevalidateMs ?? 0;
143
+ }
144
+ static key(query, variables) {
145
+ const normalized = normalizeQuery(query);
146
+ return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
147
+ }
148
+ /** Normalizes a GraphQL response, extracting entities and returning a tree of references. */
149
+ normalize(data, seen = /* @__PURE__ */ new WeakSet()) {
150
+ if (Array.isArray(data)) {
151
+ if (seen.has(data)) return null;
152
+ seen.add(data);
153
+ return data.map((item) => this.normalize(item, seen));
154
+ }
155
+ if (data !== null && typeof data === "object") {
156
+ if (seen.has(data)) return null;
157
+ seen.add(data);
158
+ const obj = data;
159
+ if (typeof obj.__typename === "string" && (typeof obj.id === "number" || typeof obj.id === "string")) {
160
+ const ref = `${obj.__typename}:${obj.id}`;
161
+ const normalizedObj = {};
162
+ for (const [k, v] of Object.entries(obj)) {
163
+ normalizedObj[k] = this.normalize(v, seen);
164
+ }
165
+ const existing = this.entityStore.get(ref) || {};
166
+ this.entityStore.set(ref, { ...existing, ...normalizedObj });
167
+ return { __ref: ref };
168
+ }
169
+ const result = {};
170
+ for (const [k, v] of Object.entries(obj)) {
171
+ result[k] = this.normalize(v, seen);
172
+ }
173
+ return result;
174
+ }
175
+ return data;
176
+ }
177
+ /** Reconstructs a GraphQL response from references. */
178
+ denormalize(data, seen = /* @__PURE__ */ new Set()) {
179
+ if (Array.isArray(data)) {
180
+ return data.map((item) => this.denormalize(item, seen));
181
+ }
182
+ if (data !== null && typeof data === "object") {
183
+ const obj = data;
184
+ if (typeof obj.__ref === "string") {
185
+ const ref = obj.__ref;
186
+ if (seen.has(ref)) {
187
+ return { __ref: ref };
188
+ }
189
+ seen.add(ref);
190
+ const entity = this.entityStore.get(ref);
191
+ if (!entity) return void 0;
192
+ const result2 = this.denormalize(entity, seen);
193
+ seen.delete(ref);
194
+ return result2;
195
+ }
196
+ const result = {};
197
+ for (const [k, v] of Object.entries(obj)) {
198
+ const denormalized = this.denormalize(v, seen);
199
+ if (denormalized === void 0) return void 0;
200
+ result[k] = denormalized;
201
+ }
202
+ return result;
203
+ }
204
+ return data;
205
+ }
206
+ getWithMeta(key) {
207
+ if (!this.enabled) return void 0;
208
+ const entry = this.queryStore.get(key);
209
+ if (!entry) {
210
+ this._misses++;
211
+ return void 0;
212
+ }
213
+ const now = Date.now();
214
+ let isStale = false;
215
+ if (now > entry.expiresAt) {
216
+ if (this.swrMs > 0 && now <= entry.expiresAt + this.swrMs) {
217
+ isStale = true;
218
+ } else {
219
+ this.queryStore.delete(key);
220
+ this._misses++;
221
+ return void 0;
222
+ }
223
+ }
224
+ const denormalized = this.denormalize(entry.data);
225
+ if (denormalized === void 0) {
226
+ this.queryStore.delete(key);
227
+ this._misses++;
228
+ return void 0;
229
+ }
230
+ this.queryStore.delete(key);
231
+ this.queryStore.set(key, entry);
232
+ if (isStale) {
233
+ this._stales++;
234
+ } else {
235
+ this._hits++;
236
+ }
237
+ return { data: denormalized, stale: isStale };
238
+ }
239
+ get(key) {
240
+ const res = this.getWithMeta(key);
241
+ return res ? res.data : void 0;
242
+ }
243
+ set(key, data) {
244
+ if (!this.enabled) return;
245
+ const normalizedData = this.normalize(data);
246
+ this.queryStore.delete(key);
247
+ if (this.maxSize > 0 && this.queryStore.size >= this.maxSize) {
248
+ const firstKey = this.queryStore.keys().next().value;
249
+ if (firstKey !== void 0) this.queryStore.delete(firstKey);
250
+ }
251
+ this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
252
+ }
253
+ delete(key) {
254
+ return this.queryStore.delete(key);
255
+ }
256
+ clear() {
257
+ this.queryStore.clear();
258
+ this.entityStore.clear();
259
+ this._hits = 0;
260
+ this._misses = 0;
261
+ this._stales = 0;
262
+ }
263
+ get size() {
264
+ return this.queryStore.size;
265
+ }
266
+ keys() {
267
+ return [...this.queryStore.keys()];
268
+ }
269
+ invalidate(pattern) {
270
+ const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
271
+ const toDelete = [];
272
+ for (const key of this.queryStore.keys()) {
273
+ if (test(key)) toDelete.push(key);
274
+ }
275
+ for (const key of toDelete) this.queryStore.delete(key);
276
+ return toDelete.length;
277
+ }
278
+ get stats() {
279
+ const total = this._hits + this._misses + this._stales;
280
+ return {
281
+ hits: this._hits,
282
+ misses: this._misses,
283
+ stales: this._stales,
284
+ hitRate: total === 0 ? Number.NaN : this._hits / total,
285
+ entitiesCount: this.entityStore.size
286
+ };
287
+ }
288
+ resetStats() {
289
+ this._hits = 0;
290
+ this._misses = 0;
291
+ this._stales = 0;
292
+ }
293
+ };
294
+
127
295
  // src/cache/index.ts
128
296
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
129
297
  var MemoryCache = class {
@@ -151,7 +319,10 @@ var MemoryCache = class {
151
319
  * With stale-while-revalidate enabled, returns stale data within the grace window
152
320
  * and flags it so the caller can refresh in the background.
153
321
  */
154
- get(key) {
322
+ /**
323
+ * Retrieve a cached value and its stale status.
324
+ */
325
+ getWithMeta(key) {
155
326
  if (!this.enabled) return void 0;
156
327
  const entry = this.store.get(key);
157
328
  if (!entry) {
@@ -164,7 +335,7 @@ var MemoryCache = class {
164
335
  this.store.delete(key);
165
336
  this.store.set(key, entry);
166
337
  this._stales++;
167
- return entry.data;
338
+ return { data: entry.data, stale: true };
168
339
  }
169
340
  this.store.delete(key);
170
341
  this._misses++;
@@ -173,7 +344,11 @@ var MemoryCache = class {
173
344
  this.store.delete(key);
174
345
  this.store.set(key, entry);
175
346
  this._hits++;
176
- return entry.data;
347
+ return { data: entry.data, stale: false };
348
+ }
349
+ get(key) {
350
+ const res = this.getWithMeta(key);
351
+ return res ? res.data : void 0;
177
352
  }
178
353
  /** Store a value in the cache. */
179
354
  set(key, data) {
@@ -351,7 +526,7 @@ var AniListError = class _AniListError extends Error {
351
526
  this.status = status;
352
527
  this.errors = errors;
353
528
  Object.setPrototypeOf(this, _AniListError.prototype);
354
- if (Error.captureStackTrace) {
529
+ if ("captureStackTrace" in Error) {
355
530
  Error.captureStackTrace(this, _AniListError);
356
531
  }
357
532
  }
@@ -359,6 +534,7 @@ var AniListError = class _AniListError extends Error {
359
534
 
360
535
  // src/queries/fragments.ts
361
536
  var MEDIA_FIELDS_LIGHT = `
537
+ __typename
362
538
  id
363
539
  idMal
364
540
  title { romaji english native userPreferred }
@@ -385,6 +561,7 @@ var MEDIA_FIELDS_LIGHT = `
385
561
  }
386
562
  `;
387
563
  var MEDIA_FIELDS_BASE = `
564
+ __typename
388
565
  id
389
566
  idMal
390
567
  title { romaji english native userPreferred }
@@ -431,6 +608,7 @@ var RELATIONS_FIELDS = `
431
608
  edges {
432
609
  relationType(version: 2)
433
610
  node {
611
+ __typename
434
612
  id
435
613
  title { romaji english native userPreferred }
436
614
  type
@@ -461,25 +639,33 @@ var RELATIONS_FIELDS = `
461
639
  }
462
640
  `;
463
641
  var MEDIA_RECOMMENDATION_FIELDS = `
642
+ __typename
464
643
  id
465
644
  rating
466
645
  mediaRecommendation {
646
+ __typename
467
647
  id
468
648
  title { romaji english native userPreferred }
469
649
  type
470
650
  format
471
- coverImage { large medium }
651
+ coverImage { extraLarge large medium color }
472
652
  averageScore
473
653
  meanScore
474
654
  episodes
475
655
  chapters
476
656
  volumes
477
- nextAiringEpisode
657
+ nextAiringEpisode {
658
+ id
659
+ airingAt
660
+ episode
661
+ mediaId
662
+ timeUntilAiring
663
+ }
478
664
  season
479
665
  seasonYear
480
- startDate
481
- endDate
482
- studios
666
+ startDate { year month day }
667
+ endDate { year month day }
668
+ studios { nodes { id name isAnimationStudio siteUrl } }
483
669
  genres
484
670
  siteUrl
485
671
  }
@@ -489,6 +675,7 @@ var MEDIA_FIELDS = `
489
675
  ${RELATIONS_FIELDS}
490
676
  `;
491
677
  var CHARACTER_FIELDS_COMPACT = `
678
+ __typename
492
679
  id
493
680
  name { first middle last full native alternative }
494
681
  image { large medium }
@@ -503,6 +690,7 @@ var CHARACTER_FIELDS_COMPACT = `
503
690
  var CHARACTER_MEDIA_NODES = `
504
691
  media(perPage: 10) {
505
692
  nodes {
693
+ __typename
506
694
  id
507
695
  title { romaji english native userPreferred }
508
696
  type
@@ -512,6 +700,7 @@ var CHARACTER_MEDIA_NODES = `
512
700
  }
513
701
  `;
514
702
  var VOICE_ACTOR_FIELDS_COMPACT = `
703
+ __typename
515
704
  id
516
705
  name { first middle last full native userPreferred }
517
706
  languageV2
@@ -527,6 +716,7 @@ var CHARACTER_MEDIA_EDGES_WITH_VA = `
527
716
  ${VOICE_ACTOR_FIELDS_COMPACT}
528
717
  }
529
718
  node {
719
+ __typename
530
720
  id
531
721
  title { romaji english native userPreferred }
532
722
  type
@@ -545,6 +735,7 @@ var CHARACTER_FIELDS_WITH_VA = `
545
735
  ${CHARACTER_MEDIA_EDGES_WITH_VA}
546
736
  `;
547
737
  var STAFF_FIELDS = `
738
+ __typename
548
739
  id
549
740
  name { first middle last full native }
550
741
  language
@@ -564,6 +755,7 @@ var STAFF_FIELDS = `
564
755
  var STAFF_MEDIA_FIELDS = `
565
756
  staffMedia(perPage: $perPage, sort: [POPULARITY_DESC]) {
566
757
  nodes {
758
+ __typename
567
759
  id
568
760
  title { romaji english native userPreferred }
569
761
  type
@@ -602,6 +794,7 @@ var STAFF_MEDIA_FIELDS = `
602
794
  }
603
795
  `;
604
796
  var USER_FIELDS = `
797
+ __typename
605
798
  id
606
799
  name
607
800
  about(asHtml: false)
@@ -622,6 +815,7 @@ var USER_FAVORITES_FIELDS = `
622
815
  favourites {
623
816
  anime(perPage: 25) {
624
817
  nodes {
818
+ __typename
625
819
  id
626
820
  title { romaji english native userPreferred }
627
821
  coverImage { large medium }
@@ -632,6 +826,7 @@ var USER_FAVORITES_FIELDS = `
632
826
  }
633
827
  manga(perPage: 25) {
634
828
  nodes {
829
+ __typename
635
830
  id
636
831
  title { romaji english native userPreferred }
637
832
  coverImage { large medium }
@@ -642,6 +837,7 @@ var USER_FAVORITES_FIELDS = `
642
837
  }
643
838
  characters(perPage: 25) {
644
839
  nodes {
840
+ __typename
645
841
  id
646
842
  name { full native }
647
843
  image { large medium }
@@ -650,6 +846,7 @@ var USER_FAVORITES_FIELDS = `
650
846
  }
651
847
  staff(perPage: 25) {
652
848
  nodes {
849
+ __typename
653
850
  id
654
851
  name { full native }
655
852
  image { large medium }
@@ -658,6 +855,7 @@ var USER_FAVORITES_FIELDS = `
658
855
  }
659
856
  studios(perPage: 25) {
660
857
  nodes {
858
+ __typename
661
859
  id
662
860
  name
663
861
  siteUrl
@@ -666,6 +864,7 @@ var USER_FAVORITES_FIELDS = `
666
864
  }
667
865
  `;
668
866
  var MEDIA_LIST_FIELDS = `
867
+ __typename
669
868
  id
670
869
  mediaId
671
870
  status
@@ -685,6 +884,7 @@ var MEDIA_LIST_FIELDS = `
685
884
  }
686
885
  `;
687
886
  var STUDIO_FIELDS = `
887
+ __typename
688
888
  id
689
889
  name
690
890
  isAnimationStudio
@@ -693,6 +893,7 @@ var STUDIO_FIELDS = `
693
893
  media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
694
894
  pageInfo { total perPage currentPage lastPage hasNextPage }
695
895
  nodes {
896
+ __typename
696
897
  id
697
898
  title { romaji english native userPreferred }
698
899
  type
@@ -703,6 +904,7 @@ var STUDIO_FIELDS = `
703
904
  }
704
905
  `;
705
906
  var THREAD_FIELDS = `
907
+ __typename
706
908
  id
707
909
  title
708
910
  body(asHtml: false)
@@ -719,20 +921,24 @@ var THREAD_FIELDS = `
719
921
  updatedAt
720
922
  siteUrl
721
923
  user {
924
+ __typename
722
925
  id
723
926
  name
724
927
  avatar { large medium }
725
928
  }
726
929
  replyUser {
930
+ __typename
727
931
  id
728
932
  name
729
933
  avatar { large medium }
730
934
  }
731
935
  categories {
936
+ __typename
732
937
  id
733
938
  name
734
939
  }
735
940
  mediaCategories {
941
+ __typename
736
942
  id
737
943
  title { romaji english native userPreferred }
738
944
  type
@@ -740,6 +946,7 @@ var THREAD_FIELDS = `
740
946
  siteUrl
741
947
  }
742
948
  likes {
949
+ __typename
743
950
  id
744
951
  name
745
952
  }
@@ -841,7 +1048,7 @@ query (
841
1048
  timeUntilAiring
842
1049
  episode
843
1050
  mediaId
844
- media(isAdult: $isAdult) {
1051
+ media {
845
1052
  ${MEDIA_FIELDS_BASE}
846
1053
  }
847
1054
  }
@@ -2117,7 +2324,7 @@ function mapFavorites(fav) {
2117
2324
 
2118
2325
  // src/client/index.ts
2119
2326
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2120
- var LIB_VERSION = "2.1.0" ;
2327
+ var LIB_VERSION = "2.1.2" ;
2121
2328
  var AniListClient = class {
2122
2329
  apiUrl;
2123
2330
  headers;
@@ -2162,10 +2369,25 @@ var AniListClient = class {
2162
2369
  /** @internal */
2163
2370
  async request(query, variables = {}) {
2164
2371
  const cacheKey = MemoryCache.key(query, variables);
2165
- const cached = await this.cacheAdapter.get(cacheKey);
2372
+ let cached;
2373
+ let isStale = false;
2374
+ if (this.cacheAdapter.getWithMeta) {
2375
+ const res = await this.cacheAdapter.getWithMeta(cacheKey);
2376
+ if (res) {
2377
+ cached = res.data;
2378
+ isStale = res.stale;
2379
+ }
2380
+ } else {
2381
+ cached = await this.cacheAdapter.get(cacheKey);
2382
+ }
2166
2383
  if (cached !== void 0) {
2384
+ if (isStale) {
2385
+ this.executeRequest(query, variables, cacheKey).catch((err) => {
2386
+ this.logger?.error("Background revalidation failed", { error: err.message, cacheKey });
2387
+ });
2388
+ }
2167
2389
  this.hooks.onCacheHit?.(cacheKey);
2168
- this.logger?.debug("Cache hit", { cacheKey });
2390
+ this.logger?.debug("Cache hit", { cacheKey, isStale });
2169
2391
  const meta = { durationMs: 0, fromCache: true };
2170
2392
  this._lastRequestMeta = meta;
2171
2393
  this.hooks.onResponse?.(query, 0, true);
@@ -2555,6 +2777,7 @@ exports.MediaSource = MediaSource;
2555
2777
  exports.MediaStatus = MediaStatus;
2556
2778
  exports.MediaType = MediaType;
2557
2779
  exports.MemoryCache = MemoryCache;
2780
+ exports.NormalizedCache = NormalizedCache;
2558
2781
  exports.RateLimiter = RateLimiter;
2559
2782
  exports.RecommendationSort = RecommendationSort;
2560
2783
  exports.RedisCache = RedisCache;