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.mjs CHANGED
@@ -122,6 +122,174 @@ 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
+ }
249
+ this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
250
+ }
251
+ delete(key) {
252
+ return this.queryStore.delete(key);
253
+ }
254
+ clear() {
255
+ this.queryStore.clear();
256
+ this.entityStore.clear();
257
+ this._hits = 0;
258
+ this._misses = 0;
259
+ this._stales = 0;
260
+ }
261
+ get size() {
262
+ return this.queryStore.size;
263
+ }
264
+ keys() {
265
+ return [...this.queryStore.keys()];
266
+ }
267
+ invalidate(pattern) {
268
+ const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
269
+ const toDelete = [];
270
+ for (const key of this.queryStore.keys()) {
271
+ if (test(key)) toDelete.push(key);
272
+ }
273
+ for (const key of toDelete) this.queryStore.delete(key);
274
+ return toDelete.length;
275
+ }
276
+ get stats() {
277
+ const total = this._hits + this._misses + this._stales;
278
+ return {
279
+ hits: this._hits,
280
+ misses: this._misses,
281
+ stales: this._stales,
282
+ hitRate: total === 0 ? Number.NaN : this._hits / total,
283
+ entitiesCount: this.entityStore.size
284
+ };
285
+ }
286
+ resetStats() {
287
+ this._hits = 0;
288
+ this._misses = 0;
289
+ this._stales = 0;
290
+ }
291
+ };
292
+
125
293
  // src/cache/index.ts
126
294
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
127
295
  var MemoryCache = class {
@@ -149,7 +317,10 @@ var MemoryCache = class {
149
317
  * With stale-while-revalidate enabled, returns stale data within the grace window
150
318
  * and flags it so the caller can refresh in the background.
151
319
  */
152
- get(key) {
320
+ /**
321
+ * Retrieve a cached value and its stale status.
322
+ */
323
+ getWithMeta(key) {
153
324
  if (!this.enabled) return void 0;
154
325
  const entry = this.store.get(key);
155
326
  if (!entry) {
@@ -162,7 +333,7 @@ var MemoryCache = class {
162
333
  this.store.delete(key);
163
334
  this.store.set(key, entry);
164
335
  this._stales++;
165
- return entry.data;
336
+ return { data: entry.data, stale: true };
166
337
  }
167
338
  this.store.delete(key);
168
339
  this._misses++;
@@ -171,7 +342,11 @@ var MemoryCache = class {
171
342
  this.store.delete(key);
172
343
  this.store.set(key, entry);
173
344
  this._hits++;
174
- return entry.data;
345
+ return { data: entry.data, stale: false };
346
+ }
347
+ get(key) {
348
+ const res = this.getWithMeta(key);
349
+ return res ? res.data : void 0;
175
350
  }
176
351
  /** Store a value in the cache. */
177
352
  set(key, data) {
@@ -349,7 +524,7 @@ var AniListError = class _AniListError extends Error {
349
524
  this.status = status;
350
525
  this.errors = errors;
351
526
  Object.setPrototypeOf(this, _AniListError.prototype);
352
- if (Error.captureStackTrace) {
527
+ if ("captureStackTrace" in Error) {
353
528
  Error.captureStackTrace(this, _AniListError);
354
529
  }
355
530
  }
@@ -357,6 +532,7 @@ var AniListError = class _AniListError extends Error {
357
532
 
358
533
  // src/queries/fragments.ts
359
534
  var MEDIA_FIELDS_LIGHT = `
535
+ __typename
360
536
  id
361
537
  idMal
362
538
  title { romaji english native userPreferred }
@@ -383,6 +559,7 @@ var MEDIA_FIELDS_LIGHT = `
383
559
  }
384
560
  `;
385
561
  var MEDIA_FIELDS_BASE = `
562
+ __typename
386
563
  id
387
564
  idMal
388
565
  title { romaji english native userPreferred }
@@ -429,6 +606,7 @@ var RELATIONS_FIELDS = `
429
606
  edges {
430
607
  relationType(version: 2)
431
608
  node {
609
+ __typename
432
610
  id
433
611
  title { romaji english native userPreferred }
434
612
  type
@@ -459,25 +637,33 @@ var RELATIONS_FIELDS = `
459
637
  }
460
638
  `;
461
639
  var MEDIA_RECOMMENDATION_FIELDS = `
640
+ __typename
462
641
  id
463
642
  rating
464
643
  mediaRecommendation {
644
+ __typename
465
645
  id
466
646
  title { romaji english native userPreferred }
467
647
  type
468
648
  format
469
- coverImage { large medium }
649
+ coverImage { extraLarge large medium color }
470
650
  averageScore
471
651
  meanScore
472
652
  episodes
473
653
  chapters
474
654
  volumes
475
- nextAiringEpisode
655
+ nextAiringEpisode {
656
+ id
657
+ airingAt
658
+ episode
659
+ mediaId
660
+ timeUntilAiring
661
+ }
476
662
  season
477
663
  seasonYear
478
- startDate
479
- endDate
480
- studios
664
+ startDate { year month day }
665
+ endDate { year month day }
666
+ studios { nodes { id name isAnimationStudio siteUrl } }
481
667
  genres
482
668
  siteUrl
483
669
  }
@@ -487,6 +673,7 @@ var MEDIA_FIELDS = `
487
673
  ${RELATIONS_FIELDS}
488
674
  `;
489
675
  var CHARACTER_FIELDS_COMPACT = `
676
+ __typename
490
677
  id
491
678
  name { first middle last full native alternative }
492
679
  image { large medium }
@@ -501,6 +688,7 @@ var CHARACTER_FIELDS_COMPACT = `
501
688
  var CHARACTER_MEDIA_NODES = `
502
689
  media(perPage: 10) {
503
690
  nodes {
691
+ __typename
504
692
  id
505
693
  title { romaji english native userPreferred }
506
694
  type
@@ -510,6 +698,7 @@ var CHARACTER_MEDIA_NODES = `
510
698
  }
511
699
  `;
512
700
  var VOICE_ACTOR_FIELDS_COMPACT = `
701
+ __typename
513
702
  id
514
703
  name { first middle last full native userPreferred }
515
704
  languageV2
@@ -525,6 +714,7 @@ var CHARACTER_MEDIA_EDGES_WITH_VA = `
525
714
  ${VOICE_ACTOR_FIELDS_COMPACT}
526
715
  }
527
716
  node {
717
+ __typename
528
718
  id
529
719
  title { romaji english native userPreferred }
530
720
  type
@@ -543,6 +733,7 @@ var CHARACTER_FIELDS_WITH_VA = `
543
733
  ${CHARACTER_MEDIA_EDGES_WITH_VA}
544
734
  `;
545
735
  var STAFF_FIELDS = `
736
+ __typename
546
737
  id
547
738
  name { first middle last full native }
548
739
  language
@@ -562,6 +753,7 @@ var STAFF_FIELDS = `
562
753
  var STAFF_MEDIA_FIELDS = `
563
754
  staffMedia(perPage: $perPage, sort: [POPULARITY_DESC]) {
564
755
  nodes {
756
+ __typename
565
757
  id
566
758
  title { romaji english native userPreferred }
567
759
  type
@@ -600,6 +792,7 @@ var STAFF_MEDIA_FIELDS = `
600
792
  }
601
793
  `;
602
794
  var USER_FIELDS = `
795
+ __typename
603
796
  id
604
797
  name
605
798
  about(asHtml: false)
@@ -620,6 +813,7 @@ var USER_FAVORITES_FIELDS = `
620
813
  favourites {
621
814
  anime(perPage: 25) {
622
815
  nodes {
816
+ __typename
623
817
  id
624
818
  title { romaji english native userPreferred }
625
819
  coverImage { large medium }
@@ -630,6 +824,7 @@ var USER_FAVORITES_FIELDS = `
630
824
  }
631
825
  manga(perPage: 25) {
632
826
  nodes {
827
+ __typename
633
828
  id
634
829
  title { romaji english native userPreferred }
635
830
  coverImage { large medium }
@@ -640,6 +835,7 @@ var USER_FAVORITES_FIELDS = `
640
835
  }
641
836
  characters(perPage: 25) {
642
837
  nodes {
838
+ __typename
643
839
  id
644
840
  name { full native }
645
841
  image { large medium }
@@ -648,6 +844,7 @@ var USER_FAVORITES_FIELDS = `
648
844
  }
649
845
  staff(perPage: 25) {
650
846
  nodes {
847
+ __typename
651
848
  id
652
849
  name { full native }
653
850
  image { large medium }
@@ -656,6 +853,7 @@ var USER_FAVORITES_FIELDS = `
656
853
  }
657
854
  studios(perPage: 25) {
658
855
  nodes {
856
+ __typename
659
857
  id
660
858
  name
661
859
  siteUrl
@@ -664,6 +862,7 @@ var USER_FAVORITES_FIELDS = `
664
862
  }
665
863
  `;
666
864
  var MEDIA_LIST_FIELDS = `
865
+ __typename
667
866
  id
668
867
  mediaId
669
868
  status
@@ -683,6 +882,7 @@ var MEDIA_LIST_FIELDS = `
683
882
  }
684
883
  `;
685
884
  var STUDIO_FIELDS = `
885
+ __typename
686
886
  id
687
887
  name
688
888
  isAnimationStudio
@@ -691,6 +891,7 @@ var STUDIO_FIELDS = `
691
891
  media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
692
892
  pageInfo { total perPage currentPage lastPage hasNextPage }
693
893
  nodes {
894
+ __typename
694
895
  id
695
896
  title { romaji english native userPreferred }
696
897
  type
@@ -701,6 +902,7 @@ var STUDIO_FIELDS = `
701
902
  }
702
903
  `;
703
904
  var THREAD_FIELDS = `
905
+ __typename
704
906
  id
705
907
  title
706
908
  body(asHtml: false)
@@ -717,20 +919,24 @@ var THREAD_FIELDS = `
717
919
  updatedAt
718
920
  siteUrl
719
921
  user {
922
+ __typename
720
923
  id
721
924
  name
722
925
  avatar { large medium }
723
926
  }
724
927
  replyUser {
928
+ __typename
725
929
  id
726
930
  name
727
931
  avatar { large medium }
728
932
  }
729
933
  categories {
934
+ __typename
730
935
  id
731
936
  name
732
937
  }
733
938
  mediaCategories {
939
+ __typename
734
940
  id
735
941
  title { romaji english native userPreferred }
736
942
  type
@@ -738,6 +944,7 @@ var THREAD_FIELDS = `
738
944
  siteUrl
739
945
  }
740
946
  likes {
947
+ __typename
741
948
  id
742
949
  name
743
950
  }
@@ -839,7 +1046,7 @@ query (
839
1046
  timeUntilAiring
840
1047
  episode
841
1048
  mediaId
842
- media(isAdult: $isAdult) {
1049
+ media {
843
1050
  ${MEDIA_FIELDS_BASE}
844
1051
  }
845
1052
  }
@@ -2115,7 +2322,7 @@ function mapFavorites(fav) {
2115
2322
 
2116
2323
  // src/client/index.ts
2117
2324
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2118
- var LIB_VERSION = "2.1.0" ;
2325
+ var LIB_VERSION = "2.1.2" ;
2119
2326
  var AniListClient = class {
2120
2327
  apiUrl;
2121
2328
  headers;
@@ -2160,10 +2367,25 @@ var AniListClient = class {
2160
2367
  /** @internal */
2161
2368
  async request(query, variables = {}) {
2162
2369
  const cacheKey = MemoryCache.key(query, variables);
2163
- const cached = await this.cacheAdapter.get(cacheKey);
2370
+ let cached;
2371
+ let isStale = false;
2372
+ if (this.cacheAdapter.getWithMeta) {
2373
+ const res = await this.cacheAdapter.getWithMeta(cacheKey);
2374
+ if (res) {
2375
+ cached = res.data;
2376
+ isStale = res.stale;
2377
+ }
2378
+ } else {
2379
+ cached = await this.cacheAdapter.get(cacheKey);
2380
+ }
2164
2381
  if (cached !== void 0) {
2382
+ if (isStale) {
2383
+ this.executeRequest(query, variables, cacheKey).catch((err) => {
2384
+ this.logger?.error("Background revalidation failed", { error: err.message, cacheKey });
2385
+ });
2386
+ }
2165
2387
  this.hooks.onCacheHit?.(cacheKey);
2166
- this.logger?.debug("Cache hit", { cacheKey });
2388
+ this.logger?.debug("Cache hit", { cacheKey, isStale });
2167
2389
  const meta = { durationMs: 0, fromCache: true };
2168
2390
  this._lastRequestMeta = meta;
2169
2391
  this.hooks.onResponse?.(query, 0, true);
@@ -2538,6 +2760,6 @@ var AniListClient = class {
2538
2760
  }
2539
2761
  };
2540
2762
 
2541
- 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 };
2763
+ 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 };
2542
2764
  //# sourceMappingURL=index.mjs.map
2543
2765
  //# sourceMappingURL=index.mjs.map