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.js CHANGED
@@ -124,6 +124,212 @@ 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
+ this.gc();
251
+ }
252
+ this.queryStore.set(key, { data: normalizedData, expiresAt: Date.now() + this.ttl });
253
+ }
254
+ delete(key) {
255
+ return this.queryStore.delete(key);
256
+ }
257
+ clear() {
258
+ this.queryStore.clear();
259
+ this.entityStore.clear();
260
+ this._hits = 0;
261
+ this._misses = 0;
262
+ this._stales = 0;
263
+ }
264
+ get size() {
265
+ return this.queryStore.size;
266
+ }
267
+ keys() {
268
+ return [...this.queryStore.keys()];
269
+ }
270
+ invalidate(pattern) {
271
+ const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
272
+ const toDelete = [];
273
+ for (const key of this.queryStore.keys()) {
274
+ if (test(key)) toDelete.push(key);
275
+ }
276
+ for (const key of toDelete) this.queryStore.delete(key);
277
+ return toDelete.length;
278
+ }
279
+ get stats() {
280
+ const total = this._hits + this._misses + this._stales;
281
+ return {
282
+ hits: this._hits,
283
+ misses: this._misses,
284
+ stales: this._stales,
285
+ hitRate: total === 0 ? Number.NaN : this._hits / total,
286
+ entitiesCount: this.entityStore.size
287
+ };
288
+ }
289
+ resetStats() {
290
+ this._hits = 0;
291
+ this._misses = 0;
292
+ this._stales = 0;
293
+ }
294
+ /**
295
+ * Garbage-collect orphaned entities that are no longer referenced by any query.
296
+ * Called automatically on LRU eviction to prevent unbounded entity store growth.
297
+ */
298
+ gc() {
299
+ const referencedRefs = /* @__PURE__ */ new Set();
300
+ const collectRefs = (data) => {
301
+ if (Array.isArray(data)) {
302
+ for (const item of data) collectRefs(item);
303
+ return;
304
+ }
305
+ if (data !== null && typeof data === "object") {
306
+ const obj = data;
307
+ if (typeof obj.__ref === "string") {
308
+ referencedRefs.add(obj.__ref);
309
+ const entity = this.entityStore.get(obj.__ref);
310
+ if (entity && !referencedRefs.has(`_visited:${obj.__ref}`)) {
311
+ referencedRefs.add(`_visited:${obj.__ref}`);
312
+ collectRefs(entity);
313
+ }
314
+ return;
315
+ }
316
+ for (const v of Object.values(obj)) collectRefs(v);
317
+ }
318
+ };
319
+ for (const entry of this.queryStore.values()) {
320
+ collectRefs(entry.data);
321
+ }
322
+ let removed = 0;
323
+ for (const ref of this.entityStore.keys()) {
324
+ if (!referencedRefs.has(ref)) {
325
+ this.entityStore.delete(ref);
326
+ removed++;
327
+ }
328
+ }
329
+ return removed;
330
+ }
331
+ };
332
+
127
333
  // src/cache/index.ts
128
334
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
129
335
  var MemoryCache = class {
@@ -147,11 +353,11 @@ var MemoryCache = class {
147
353
  return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
148
354
  }
149
355
  /**
150
- * Retrieve a cached value, or `undefined` if missing / expired.
356
+ * Retrieve a cached value and its stale status.
151
357
  * With stale-while-revalidate enabled, returns stale data within the grace window
152
358
  * and flags it so the caller can refresh in the background.
153
359
  */
154
- get(key) {
360
+ getWithMeta(key) {
155
361
  if (!this.enabled) return void 0;
156
362
  const entry = this.store.get(key);
157
363
  if (!entry) {
@@ -164,7 +370,7 @@ var MemoryCache = class {
164
370
  this.store.delete(key);
165
371
  this.store.set(key, entry);
166
372
  this._stales++;
167
- return entry.data;
373
+ return { data: entry.data, stale: true };
168
374
  }
169
375
  this.store.delete(key);
170
376
  this._misses++;
@@ -173,7 +379,11 @@ var MemoryCache = class {
173
379
  this.store.delete(key);
174
380
  this.store.set(key, entry);
175
381
  this._hits++;
176
- return entry.data;
382
+ return { data: entry.data, stale: false };
383
+ }
384
+ get(key) {
385
+ const res = this.getWithMeta(key);
386
+ return res ? res.data : void 0;
177
387
  }
178
388
  /** Store a value in the cache. */
179
389
  set(key, data) {
@@ -359,6 +569,7 @@ var AniListError = class _AniListError extends Error {
359
569
 
360
570
  // src/queries/fragments.ts
361
571
  var MEDIA_FIELDS_LIGHT = `
572
+ __typename
362
573
  id
363
574
  idMal
364
575
  title { romaji english native userPreferred }
@@ -385,6 +596,7 @@ var MEDIA_FIELDS_LIGHT = `
385
596
  }
386
597
  `;
387
598
  var MEDIA_FIELDS_BASE = `
599
+ __typename
388
600
  id
389
601
  idMal
390
602
  title { romaji english native userPreferred }
@@ -431,6 +643,7 @@ var RELATIONS_FIELDS = `
431
643
  edges {
432
644
  relationType(version: 2)
433
645
  node {
646
+ __typename
434
647
  id
435
648
  title { romaji english native userPreferred }
436
649
  type
@@ -461,9 +674,11 @@ var RELATIONS_FIELDS = `
461
674
  }
462
675
  `;
463
676
  var MEDIA_RECOMMENDATION_FIELDS = `
677
+ __typename
464
678
  id
465
679
  rating
466
680
  mediaRecommendation {
681
+ __typename
467
682
  id
468
683
  title { romaji english native userPreferred }
469
684
  type
@@ -495,6 +710,7 @@ var MEDIA_FIELDS = `
495
710
  ${RELATIONS_FIELDS}
496
711
  `;
497
712
  var CHARACTER_FIELDS_COMPACT = `
713
+ __typename
498
714
  id
499
715
  name { first middle last full native alternative }
500
716
  image { large medium }
@@ -509,6 +725,7 @@ var CHARACTER_FIELDS_COMPACT = `
509
725
  var CHARACTER_MEDIA_NODES = `
510
726
  media(perPage: 10) {
511
727
  nodes {
728
+ __typename
512
729
  id
513
730
  title { romaji english native userPreferred }
514
731
  type
@@ -518,6 +735,7 @@ var CHARACTER_MEDIA_NODES = `
518
735
  }
519
736
  `;
520
737
  var VOICE_ACTOR_FIELDS_COMPACT = `
738
+ __typename
521
739
  id
522
740
  name { first middle last full native userPreferred }
523
741
  languageV2
@@ -533,6 +751,7 @@ var CHARACTER_MEDIA_EDGES_WITH_VA = `
533
751
  ${VOICE_ACTOR_FIELDS_COMPACT}
534
752
  }
535
753
  node {
754
+ __typename
536
755
  id
537
756
  title { romaji english native userPreferred }
538
757
  type
@@ -551,6 +770,7 @@ var CHARACTER_FIELDS_WITH_VA = `
551
770
  ${CHARACTER_MEDIA_EDGES_WITH_VA}
552
771
  `;
553
772
  var STAFF_FIELDS = `
773
+ __typename
554
774
  id
555
775
  name { first middle last full native }
556
776
  language
@@ -570,6 +790,7 @@ var STAFF_FIELDS = `
570
790
  var STAFF_MEDIA_FIELDS = `
571
791
  staffMedia(perPage: $perPage, sort: [POPULARITY_DESC]) {
572
792
  nodes {
793
+ __typename
573
794
  id
574
795
  title { romaji english native userPreferred }
575
796
  type
@@ -608,6 +829,7 @@ var STAFF_MEDIA_FIELDS = `
608
829
  }
609
830
  `;
610
831
  var USER_FIELDS = `
832
+ __typename
611
833
  id
612
834
  name
613
835
  about(asHtml: false)
@@ -628,6 +850,7 @@ var USER_FAVORITES_FIELDS = `
628
850
  favourites {
629
851
  anime(perPage: 25) {
630
852
  nodes {
853
+ __typename
631
854
  id
632
855
  title { romaji english native userPreferred }
633
856
  coverImage { large medium }
@@ -638,6 +861,7 @@ var USER_FAVORITES_FIELDS = `
638
861
  }
639
862
  manga(perPage: 25) {
640
863
  nodes {
864
+ __typename
641
865
  id
642
866
  title { romaji english native userPreferred }
643
867
  coverImage { large medium }
@@ -648,6 +872,7 @@ var USER_FAVORITES_FIELDS = `
648
872
  }
649
873
  characters(perPage: 25) {
650
874
  nodes {
875
+ __typename
651
876
  id
652
877
  name { full native }
653
878
  image { large medium }
@@ -656,6 +881,7 @@ var USER_FAVORITES_FIELDS = `
656
881
  }
657
882
  staff(perPage: 25) {
658
883
  nodes {
884
+ __typename
659
885
  id
660
886
  name { full native }
661
887
  image { large medium }
@@ -664,6 +890,7 @@ var USER_FAVORITES_FIELDS = `
664
890
  }
665
891
  studios(perPage: 25) {
666
892
  nodes {
893
+ __typename
667
894
  id
668
895
  name
669
896
  siteUrl
@@ -672,6 +899,7 @@ var USER_FAVORITES_FIELDS = `
672
899
  }
673
900
  `;
674
901
  var MEDIA_LIST_FIELDS = `
902
+ __typename
675
903
  id
676
904
  mediaId
677
905
  status
@@ -691,6 +919,7 @@ var MEDIA_LIST_FIELDS = `
691
919
  }
692
920
  `;
693
921
  var STUDIO_FIELDS = `
922
+ __typename
694
923
  id
695
924
  name
696
925
  isAnimationStudio
@@ -699,6 +928,7 @@ var STUDIO_FIELDS = `
699
928
  media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
700
929
  pageInfo { total perPage currentPage lastPage hasNextPage }
701
930
  nodes {
931
+ __typename
702
932
  id
703
933
  title { romaji english native userPreferred }
704
934
  type
@@ -709,6 +939,7 @@ var STUDIO_FIELDS = `
709
939
  }
710
940
  `;
711
941
  var THREAD_FIELDS = `
942
+ __typename
712
943
  id
713
944
  title
714
945
  body(asHtml: false)
@@ -725,20 +956,24 @@ var THREAD_FIELDS = `
725
956
  updatedAt
726
957
  siteUrl
727
958
  user {
959
+ __typename
728
960
  id
729
961
  name
730
962
  avatar { large medium }
731
963
  }
732
964
  replyUser {
965
+ __typename
733
966
  id
734
967
  name
735
968
  avatar { large medium }
736
969
  }
737
970
  categories {
971
+ __typename
738
972
  id
739
973
  name
740
974
  }
741
975
  mediaCategories {
976
+ __typename
742
977
  id
743
978
  title { romaji english native userPreferred }
744
979
  type
@@ -746,6 +981,7 @@ var THREAD_FIELDS = `
746
981
  siteUrl
747
982
  }
748
983
  likes {
984
+ __typename
749
985
  id
750
986
  name
751
987
  }
@@ -1989,7 +2225,11 @@ async function getReview(client, id) {
1989
2225
  }
1990
2226
  async function searchReviews(client, options = {}) {
1991
2227
  const { mediaId, userId, sort, page = 1, perPage = 20 } = options;
1992
- return client.pagedRequest(QUERY_REVIEWS, { mediaId, userId, sort, page, perPage }, "reviews");
2228
+ return client.pagedRequest(
2229
+ QUERY_REVIEWS,
2230
+ { mediaId, userId, sort, page, perPage: clampPerPage(perPage) },
2231
+ "reviews"
2232
+ );
1993
2233
  }
1994
2234
 
1995
2235
  // src/client/staff.ts
@@ -2123,7 +2363,7 @@ function mapFavorites(fav) {
2123
2363
 
2124
2364
  // src/client/index.ts
2125
2365
  var DEFAULT_API_URL = "https://graphql.anilist.co";
2126
- var LIB_VERSION = "2.1.1" ;
2366
+ var LIB_VERSION = "2.1.3" ;
2127
2367
  var AniListClient = class {
2128
2368
  apiUrl;
2129
2369
  headers;
@@ -2168,10 +2408,25 @@ var AniListClient = class {
2168
2408
  /** @internal */
2169
2409
  async request(query, variables = {}) {
2170
2410
  const cacheKey = MemoryCache.key(query, variables);
2171
- const cached = await this.cacheAdapter.get(cacheKey);
2411
+ let cached;
2412
+ let isStale = false;
2413
+ if (this.cacheAdapter.getWithMeta) {
2414
+ const res = await this.cacheAdapter.getWithMeta(cacheKey);
2415
+ if (res) {
2416
+ cached = res.data;
2417
+ isStale = res.stale;
2418
+ }
2419
+ } else {
2420
+ cached = await this.cacheAdapter.get(cacheKey);
2421
+ }
2172
2422
  if (cached !== void 0) {
2423
+ if (isStale) {
2424
+ this.executeRequest(query, variables, cacheKey).catch((err) => {
2425
+ this.logger?.error("Background revalidation failed", { error: err.message, cacheKey });
2426
+ });
2427
+ }
2173
2428
  this.hooks.onCacheHit?.(cacheKey);
2174
- this.logger?.debug("Cache hit", { cacheKey });
2429
+ this.logger?.debug("Cache hit", { cacheKey, isStale });
2175
2430
  const meta = { durationMs: 0, fromCache: true };
2176
2431
  this._lastRequestMeta = meta;
2177
2432
  this.hooks.onResponse?.(query, 0, true);
@@ -2321,8 +2576,8 @@ var AniListClient = class {
2321
2576
  return getMediaByMalId(this, malId, type);
2322
2577
  }
2323
2578
  /** Get the detailed schedule for the current week, sorted by day. */
2324
- async getWeeklySchedule(date) {
2325
- return getWeeklySchedule(this, date);
2579
+ async getWeeklySchedule(date, idNotIn) {
2580
+ return getWeeklySchedule(this, date, idNotIn);
2326
2581
  }
2327
2582
  /** Get upcoming (not yet released) media. */
2328
2583
  async getPlanning(options = {}) {
@@ -2561,6 +2816,7 @@ exports.MediaSource = MediaSource;
2561
2816
  exports.MediaStatus = MediaStatus;
2562
2817
  exports.MediaType = MediaType;
2563
2818
  exports.MemoryCache = MemoryCache;
2819
+ exports.NormalizedCache = NormalizedCache;
2564
2820
  exports.RateLimiter = RateLimiter;
2565
2821
  exports.RecommendationSort = RecommendationSort;
2566
2822
  exports.RedisCache = RedisCache;