tmdb-kit 1.1.0 → 1.2.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.js CHANGED
@@ -13,6 +13,15 @@ class TMDBRequestError extends TMDBError {
13
13
  }
14
14
  }
15
15
 
16
+ class TMDBTimeoutError extends TMDBError {
17
+ timeoutMs;
18
+ constructor(timeoutMs, message = `TMDB request timed out after ${timeoutMs}ms`) {
19
+ super(message);
20
+ this.name = "TMDBTimeoutError";
21
+ this.timeoutMs = timeoutMs;
22
+ }
23
+ }
24
+
16
25
  class TMDBResponseError extends TMDBError {
17
26
  body;
18
27
  headers;
@@ -135,6 +144,19 @@ function toId(value, name) {
135
144
  }
136
145
  return String(value);
137
146
  }
147
+ function toSeasonNumber(value) {
148
+ if (!Number.isInteger(value) || value < 0) {
149
+ throw new TMDBRequestError("seasonNumber must be a non-negative integer");
150
+ }
151
+ return String(value);
152
+ }
153
+ function toGroupId(value) {
154
+ const groupId = value.trim();
155
+ if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {
156
+ throw new TMDBRequestError("groupId must be a non-empty identifier");
157
+ }
158
+ return groupId;
159
+ }
138
160
  function toSearchQuery(value) {
139
161
  const query = value.trim();
140
162
  if (query.length === 0) {
@@ -268,23 +290,25 @@ class ConfigurationResource {
268
290
  constructor(transport) {
269
291
  this.#transport = transport;
270
292
  }
271
- details() {
272
- return this.#transport.get("/configuration");
293
+ details(options = {}) {
294
+ return this.#transport.get("/configuration", { signal: options.signal });
273
295
  }
274
- countries() {
275
- return this.#transport.get("/configuration/countries");
296
+ countries(options = {}) {
297
+ return this.#transport.get("/configuration/countries", { signal: options.signal });
276
298
  }
277
- jobs() {
278
- return this.#transport.get("/configuration/jobs");
299
+ jobs(options = {}) {
300
+ return this.#transport.get("/configuration/jobs", { signal: options.signal });
279
301
  }
280
- languages() {
281
- return this.#transport.get("/configuration/languages");
302
+ languages(options = {}) {
303
+ return this.#transport.get("/configuration/languages", { signal: options.signal });
282
304
  }
283
- primaryTranslations() {
284
- return this.#transport.get("/configuration/primary_translations");
305
+ primaryTranslations(options = {}) {
306
+ return this.#transport.get("/configuration/primary_translations", {
307
+ signal: options.signal
308
+ });
285
309
  }
286
- timezones() {
287
- return this.#transport.get("/configuration/timezones");
310
+ timezones(options = {}) {
311
+ return this.#transport.get("/configuration/timezones", { signal: options.signal });
288
312
  }
289
313
  }
290
314
 
@@ -298,14 +322,16 @@ class GenresResource {
298
322
  return this.#transport.get("/genre/movie/list", {
299
323
  query: withLanguage(this.#transport.defaults, {
300
324
  language: options.language
301
- })
325
+ }),
326
+ signal: options.signal
302
327
  });
303
328
  }
304
329
  tv(options = {}) {
305
330
  return this.#transport.get("/genre/tv/list", {
306
331
  query: withLanguage(this.#transport.defaults, {
307
332
  language: options.language
308
- })
333
+ }),
334
+ signal: options.signal
309
335
  });
310
336
  }
311
337
  }
@@ -334,22 +360,26 @@ class MoviesResource {
334
360
  }
335
361
  popular(options = {}) {
336
362
  return this.#transport.get("/movie/popular", {
337
- query: movieListQuery(this.#transport, options)
363
+ query: movieListQuery(this.#transport, options),
364
+ signal: options.signal
338
365
  });
339
366
  }
340
367
  topRated(options = {}) {
341
368
  return this.#transport.get("/movie/top_rated", {
342
- query: movieListQuery(this.#transport, options)
369
+ query: movieListQuery(this.#transport, options),
370
+ signal: options.signal
343
371
  });
344
372
  }
345
373
  nowPlaying(options = {}) {
346
374
  return this.#transport.get("/movie/now_playing", {
347
- query: movieListQuery(this.#transport, options)
375
+ query: movieListQuery(this.#transport, options),
376
+ signal: options.signal
348
377
  });
349
378
  }
350
379
  upcoming(options = {}) {
351
380
  return this.#transport.get("/movie/upcoming", {
352
- query: movieListQuery(this.#transport, options)
381
+ query: movieListQuery(this.#transport, options),
382
+ signal: options.signal
353
383
  });
354
384
  }
355
385
  details(movieId, options = {}) {
@@ -357,14 +387,16 @@ class MoviesResource {
357
387
  query: withLanguage(this.#transport.defaults, {
358
388
  append_to_response: toCommaList(options.appendToResponse),
359
389
  language: options.language
360
- })
390
+ }),
391
+ signal: options.signal
361
392
  });
362
393
  }
363
394
  credits(movieId, options = {}) {
364
395
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/credits`, {
365
396
  query: withLanguage(this.#transport.defaults, {
366
397
  language: options.language
367
- })
398
+ }),
399
+ signal: options.signal
368
400
  });
369
401
  }
370
402
  images(movieId, options = {}) {
@@ -372,24 +404,28 @@ class MoviesResource {
372
404
  query: withLanguage(this.#transport.defaults, {
373
405
  include_image_language: toCommaList(options.includeImageLanguage),
374
406
  language: options.language
375
- })
407
+ }),
408
+ signal: options.signal
376
409
  });
377
410
  }
378
411
  recommendations(movieId, options = {}) {
379
412
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/recommendations`, {
380
- query: movieListQuery(this.#transport, options)
413
+ query: movieListQuery(this.#transport, options),
414
+ signal: options.signal
381
415
  });
382
416
  }
383
417
  similar(movieId, options = {}) {
384
418
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/similar`, {
385
- query: movieListQuery(this.#transport, options)
419
+ query: movieListQuery(this.#transport, options),
420
+ signal: options.signal
386
421
  });
387
422
  }
388
423
  videos(movieId, options = {}) {
389
424
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/videos`, {
390
425
  query: withLanguage(this.#transport.defaults, {
391
426
  language: options.language
392
- })
427
+ }),
428
+ signal: options.signal
393
429
  });
394
430
  }
395
431
  }
@@ -409,21 +445,24 @@ class PeopleResource {
409
445
  }
410
446
  popular(options = {}) {
411
447
  return this.#transport.get("/person/popular", {
412
- query: peopleListQuery(this.#transport, options)
448
+ query: peopleListQuery(this.#transport, options),
449
+ signal: options.signal
413
450
  });
414
451
  }
415
452
  details(personId, options = {}) {
416
453
  return this.#transport.get(`/person/${toId(personId, "personId")}`, {
417
454
  query: withLanguage(this.#transport.defaults, {
418
455
  language: options.language
419
- })
456
+ }),
457
+ signal: options.signal
420
458
  });
421
459
  }
422
460
  combinedCredits(personId, options = {}) {
423
461
  return this.#transport.get(`/person/${toId(personId, "personId")}/combined_credits`, {
424
462
  query: withLanguage(this.#transport.defaults, {
425
463
  language: options.language
426
- })
464
+ }),
465
+ signal: options.signal
427
466
  });
428
467
  }
429
468
  }
@@ -442,22 +481,26 @@ class SearchResource {
442
481
  }
443
482
  movies(query, options = {}) {
444
483
  return this.#transport.get("/search/movie", {
445
- query: movieSearchQuery(this.#transport, query, options)
484
+ query: movieSearchQuery(this.#transport, query, options),
485
+ signal: options.signal
446
486
  });
447
487
  }
448
488
  tv(query, options = {}) {
449
489
  return this.#transport.get("/search/tv", {
450
- query: tvSearchQuery(this.#transport, query, options)
490
+ query: tvSearchQuery(this.#transport, query, options),
491
+ signal: options.signal
451
492
  });
452
493
  }
453
494
  people(query, options = {}) {
454
495
  return this.#transport.get("/search/person", {
455
- query: commonSearchQuery(this.#transport, query, options)
496
+ query: commonSearchQuery(this.#transport, query, options),
497
+ signal: options.signal
456
498
  });
457
499
  }
458
500
  async multi(query, options = {}) {
459
501
  const response = await this.#transport.get("/search/multi", {
460
- query: commonSearchQuery(this.#transport, query, options)
502
+ query: commonSearchQuery(this.#transport, query, options),
503
+ signal: options.signal
461
504
  });
462
505
  const excluded = options.exclude;
463
506
  if (excluded === undefined || excluded.length === 0) {
@@ -511,7 +554,8 @@ class TrendingResource {
511
554
  query: withLanguage(this.#transport.defaults, {
512
555
  language: options.language,
513
556
  page: options.page
514
- })
557
+ }),
558
+ signal: options.signal
515
559
  });
516
560
  }
517
561
  movies(timeWindow = "day", options = {}) {
@@ -519,7 +563,8 @@ class TrendingResource {
519
563
  query: withLanguage(this.#transport.defaults, {
520
564
  language: options.language,
521
565
  page: options.page
522
- })
566
+ }),
567
+ signal: options.signal
523
568
  });
524
569
  }
525
570
  tv(timeWindow = "day", options = {}) {
@@ -527,7 +572,8 @@ class TrendingResource {
527
572
  query: withLanguage(this.#transport.defaults, {
528
573
  language: options.language,
529
574
  page: options.page
530
- })
575
+ }),
576
+ signal: options.signal
531
577
  });
532
578
  }
533
579
  people(timeWindow = "day", options = {}) {
@@ -535,7 +581,8 @@ class TrendingResource {
535
581
  query: withLanguage(this.#transport.defaults, {
536
582
  language: options.language,
537
583
  page: options.page
538
- })
584
+ }),
585
+ signal: options.signal
539
586
  });
540
587
  }
541
588
  }
@@ -548,22 +595,26 @@ class TVResource {
548
595
  }
549
596
  popular(options = {}) {
550
597
  return this.#transport.get("/tv/popular", {
551
- query: tvListQuery(this.#transport, options)
598
+ query: tvListQuery(this.#transport, options),
599
+ signal: options.signal
552
600
  });
553
601
  }
554
602
  topRated(options = {}) {
555
603
  return this.#transport.get("/tv/top_rated", {
556
- query: tvListQuery(this.#transport, options)
604
+ query: tvListQuery(this.#transport, options),
605
+ signal: options.signal
557
606
  });
558
607
  }
559
608
  airingToday(options = {}) {
560
609
  return this.#transport.get("/tv/airing_today", {
561
- query: tvListQuery(this.#transport, options)
610
+ query: tvListQuery(this.#transport, options),
611
+ signal: options.signal
562
612
  });
563
613
  }
564
614
  onTheAir(options = {}) {
565
615
  return this.#transport.get("/tv/on_the_air", {
566
- query: tvListQuery(this.#transport, options)
616
+ query: tvListQuery(this.#transport, options),
617
+ signal: options.signal
567
618
  });
568
619
  }
569
620
  details(seriesId, options = {}) {
@@ -571,14 +622,16 @@ class TVResource {
571
622
  query: withLanguage(this.#transport.defaults, {
572
623
  append_to_response: toCommaList(options.appendToResponse),
573
624
  language: options.language
574
- })
625
+ }),
626
+ signal: options.signal
575
627
  });
576
628
  }
577
629
  credits(seriesId, options = {}) {
578
630
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/credits`, {
579
631
  query: withLanguage(this.#transport.defaults, {
580
632
  language: options.language
581
- })
633
+ }),
634
+ signal: options.signal
582
635
  });
583
636
  }
584
637
  images(seriesId, options = {}) {
@@ -586,32 +639,103 @@ class TVResource {
586
639
  query: withLanguage(this.#transport.defaults, {
587
640
  include_image_language: toCommaList(options.includeImageLanguage),
588
641
  language: options.language
589
- })
642
+ }),
643
+ signal: options.signal
590
644
  });
591
645
  }
592
646
  recommendations(seriesId, options = {}) {
593
647
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/recommendations`, {
594
- query: tvListQuery(this.#transport, options)
648
+ query: tvListQuery(this.#transport, options),
649
+ signal: options.signal
595
650
  });
596
651
  }
597
652
  similar(seriesId, options = {}) {
598
653
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/similar`, {
599
- query: tvListQuery(this.#transport, options)
654
+ query: tvListQuery(this.#transport, options),
655
+ signal: options.signal
600
656
  });
601
657
  }
602
658
  videos(seriesId, options = {}) {
603
659
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/videos`, {
604
660
  query: withLanguage(this.#transport.defaults, {
605
661
  language: options.language
606
- })
662
+ }),
663
+ signal: options.signal
607
664
  });
608
665
  }
609
666
  seasonDetails(seriesId, seasonNumber, options = {}) {
610
- return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toId(seasonNumber, "seasonNumber")}`, {
667
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}`, {
611
668
  query: withLanguage(this.#transport.defaults, {
612
669
  append_to_response: toCommaList(options.appendToResponse),
613
670
  language: options.language
614
- })
671
+ }),
672
+ signal: options.signal
673
+ });
674
+ }
675
+ seasonCredits(seriesId, seasonNumber, options = {}) {
676
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/credits`, {
677
+ query: withLanguage(this.#transport.defaults, {
678
+ language: options.language
679
+ }),
680
+ signal: options.signal
681
+ });
682
+ }
683
+ seasonAggregateCredits(seriesId, seasonNumber, options = {}) {
684
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/aggregate_credits`, {
685
+ query: withLanguage(this.#transport.defaults, {
686
+ language: options.language
687
+ }),
688
+ signal: options.signal
689
+ });
690
+ }
691
+ seasonImages(seriesId, seasonNumber, options = {}) {
692
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/images`, {
693
+ query: withLanguage(this.#transport.defaults, {
694
+ include_image_language: toCommaList(options.includeImageLanguage),
695
+ language: options.language
696
+ }),
697
+ signal: options.signal
698
+ });
699
+ }
700
+ seasonVideos(seriesId, seasonNumber, options = {}) {
701
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/videos`, {
702
+ query: withLanguage(this.#transport.defaults, {
703
+ language: options.language
704
+ }),
705
+ signal: options.signal
706
+ });
707
+ }
708
+ episodeDetails(seriesId, seasonNumber, episodeNumber, options = {}) {
709
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/episode/${toId(episodeNumber, "episodeNumber")}`, {
710
+ query: withLanguage(this.#transport.defaults, {
711
+ append_to_response: toCommaList(options.appendToResponse),
712
+ language: options.language
713
+ }),
714
+ signal: options.signal
715
+ });
716
+ }
717
+ aggregateCredits(seriesId, options = {}) {
718
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/aggregate_credits`, {
719
+ query: withLanguage(this.#transport.defaults, {
720
+ language: options.language
721
+ }),
722
+ signal: options.signal
723
+ });
724
+ }
725
+ episodeGroups(seriesId, options = {}) {
726
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/episode_groups`, {
727
+ query: withLanguage(this.#transport.defaults, {
728
+ language: options.language
729
+ }),
730
+ signal: options.signal
731
+ });
732
+ }
733
+ episodeGroup(groupId, options = {}) {
734
+ return this.#transport.get(`/tv/episode_group/${toGroupId(groupId)}`, {
735
+ query: withLanguage(this.#transport.defaults, {
736
+ language: options.language
737
+ }),
738
+ signal: options.signal
615
739
  });
616
740
  }
617
741
  }
@@ -634,13 +758,17 @@ class TMDBClient {
634
758
  tv;
635
759
  #transport;
636
760
  constructor(options) {
637
- const { transport, images, ...httpOptions } = options;
638
- const fallbackTransform = images?.transform;
639
- const builtInClient = transport === undefined ? new TMDBHttpClient({ ...httpOptions, images }) : undefined;
640
- this.#transport = builtInClient ?? transport;
761
+ let imageTransform = options.images?.transform;
762
+ if (options.transport === undefined) {
763
+ const builtInClient = new TMDBHttpClient(options);
764
+ this.#transport = builtInClient;
765
+ imageTransform = builtInClient.imageTransform;
766
+ } else {
767
+ this.#transport = options.transport;
768
+ }
641
769
  this.configuration = new ConfigurationResource(this.#transport);
642
770
  this.genres = new GenresResource(this.#transport);
643
- this.images = new ImagesHelper(this.#transport, builtInClient ? builtInClient.imageTransform : fallbackTransform);
771
+ this.images = new ImagesHelper(this.#transport, imageTransform);
644
772
  this.movies = new MoviesResource(this.#transport);
645
773
  this.people = new PeopleResource(this.#transport);
646
774
  this.search = new SearchResource(this.#transport);
@@ -654,6 +782,238 @@ class TMDBClient {
654
782
  function createTMDB(options) {
655
783
  return new TMDBClient(options);
656
784
  }
785
+ // src/helpers/cache.ts
786
+ function withMemoryCache(transport, options = {}) {
787
+ const ttlMs = options.ttlMs ?? 5 * 60000;
788
+ const maxEntries = options.maxEntries ?? 100;
789
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
790
+ throw new RangeError("ttlMs must be a positive number");
791
+ }
792
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
793
+ throw new RangeError("maxEntries must be a positive integer");
794
+ }
795
+ const entries = new Map;
796
+ return {
797
+ ...transport,
798
+ async get(path, requestOptions = {}) {
799
+ const key = cacheKey(path, requestOptions);
800
+ const cached = entries.get(key);
801
+ if (cached !== undefined) {
802
+ if (cached.expiresAt > Date.now()) {
803
+ entries.delete(key);
804
+ entries.set(key, cached);
805
+ return cached.value;
806
+ }
807
+ entries.delete(key);
808
+ }
809
+ const value = await transport.get(path, requestOptions);
810
+ entries.set(key, { expiresAt: Date.now() + ttlMs, value });
811
+ evictOverflow(entries, maxEntries);
812
+ return value;
813
+ },
814
+ clear() {
815
+ entries.clear();
816
+ }
817
+ };
818
+ }
819
+ function cacheKey(path, options) {
820
+ return `${path}?${serializeQuery(options.query)}|${serializeHeaders(options.headers)}`;
821
+ }
822
+ function serializeQuery(query) {
823
+ if (query === undefined) {
824
+ return "";
825
+ }
826
+ return Object.entries(query).filter(([, value]) => value !== undefined && value !== null).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${Array.isArray(value) ? value.map(String).join(",") : String(value)}`).join("&");
827
+ }
828
+ function serializeHeaders(headers) {
829
+ if (headers === undefined) {
830
+ return "";
831
+ }
832
+ return [...new Headers(headers)].sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}:${value}`).join("|");
833
+ }
834
+ function evictOverflow(entries, maxEntries) {
835
+ while (entries.size > maxEntries) {
836
+ entries.delete(entries.keys().next().value);
837
+ }
838
+ }
839
+ // src/helpers/pagination.ts
840
+ async function* paginate(fetchPage, options = {}) {
841
+ const startPage = options.startPage ?? 1;
842
+ const endPage = options.endPage ?? 500;
843
+ assertPositiveInteger(startPage, "startPage");
844
+ assertPositiveInteger(endPage, "endPage");
845
+ for (let page = startPage;page <= endPage; page += 1) {
846
+ options.signal?.throwIfAborted();
847
+ const response = await fetchPage(page);
848
+ yield response;
849
+ if (response.results.length === 0 || page >= response.total_pages) {
850
+ return;
851
+ }
852
+ }
853
+ }
854
+ async function* paginateItems(fetchPage, options = {}) {
855
+ for await (const page of paginate(fetchPage, options)) {
856
+ yield* page.results;
857
+ }
858
+ }
859
+ function assertPositiveInteger(value, name) {
860
+ if (!Number.isInteger(value) || value < 1) {
861
+ throw new RangeError(`${name} must be a positive integer`);
862
+ }
863
+ }
864
+ // src/helpers/retry.ts
865
+ var MAX_RETRY_AFTER_MS = 60000;
866
+ async function retry(task, options = {}) {
867
+ const retries = options.retries ?? 2;
868
+ if (!Number.isInteger(retries) || retries < 0) {
869
+ throw new RangeError("retries must be a non-negative integer");
870
+ }
871
+ const retryOn = options.retryOn ?? shouldRetry;
872
+ const signal = options.signal;
873
+ if (signal?.aborted) {
874
+ throw signal.reason;
875
+ }
876
+ for (let attempt = 0;; attempt += 1) {
877
+ try {
878
+ return await task();
879
+ } catch (error) {
880
+ if (signal?.aborted) {
881
+ throw signal.reason;
882
+ }
883
+ if (attempt >= retries || !retryOn(error, attempt)) {
884
+ throw error;
885
+ }
886
+ const delayMs = retryDelay(error, attempt, options);
887
+ options.onRetry?.({ attempt, delayMs, error });
888
+ await sleep(delayMs, signal);
889
+ }
890
+ }
891
+ }
892
+ function withRetry(transport, options = {}) {
893
+ return {
894
+ ...transport,
895
+ get(path, requestOptions) {
896
+ const { cleanup, signal } = combineSignals(options.signal, requestOptions?.signal);
897
+ const effectiveOptions = signal === undefined ? requestOptions : { ...requestOptions, signal };
898
+ return retry(() => transport.get(path, effectiveOptions), {
899
+ ...options,
900
+ signal
901
+ }).finally(cleanup);
902
+ }
903
+ };
904
+ }
905
+ function shouldRetry(error) {
906
+ if (error instanceof TMDBRateLimitError) {
907
+ return true;
908
+ }
909
+ if (error instanceof TMDBResponseError) {
910
+ return error.isServerError || error.status === 408;
911
+ }
912
+ if (error instanceof TMDBTimeoutError) {
913
+ return false;
914
+ }
915
+ if (error instanceof TMDBRequestError) {
916
+ if (error.cause === undefined || error.cause instanceof TMDBTimeoutError) {
917
+ return false;
918
+ }
919
+ return !isAbortError(error.cause);
920
+ }
921
+ return false;
922
+ }
923
+ function retryDelay(error, attempt, options) {
924
+ const retryAfter = retryAfterMs(error);
925
+ if (retryAfter !== undefined) {
926
+ return Math.min(Math.max(retryAfter, 0), MAX_RETRY_AFTER_MS);
927
+ }
928
+ if (error instanceof TMDBRateLimitError) {
929
+ return Math.min(Math.max(error.retryAfter * 1000, 0), MAX_RETRY_AFTER_MS);
930
+ }
931
+ const baseDelayMs = options.baseDelayMs ?? 250;
932
+ const maxDelayMs = options.maxDelayMs ?? 1e4;
933
+ const factor = options.factor ?? 2;
934
+ const delay = Math.min(maxDelayMs, baseDelayMs * factor ** attempt);
935
+ return options.jitter === false ? delay : Math.random() * delay;
936
+ }
937
+ function retryAfterMs(error) {
938
+ if (!(error instanceof TMDBResponseError)) {
939
+ return;
940
+ }
941
+ const header = error.headers.get("retry-after");
942
+ if (header === null || header.trim() === "") {
943
+ return;
944
+ }
945
+ return parseRetryAfter(header) * 1000;
946
+ }
947
+ function combineSignals(first, second) {
948
+ if (first === undefined || second === undefined || first === second) {
949
+ return { cleanup: noop, signal: first ?? second };
950
+ }
951
+ const controller = new AbortController;
952
+ const onAbort = () => controller.abort((first.aborted ? first : second).reason);
953
+ first.addEventListener("abort", onAbort, { once: true });
954
+ second.addEventListener("abort", onAbort, { once: true });
955
+ if (first.aborted || second.aborted) {
956
+ onAbort();
957
+ }
958
+ return {
959
+ cleanup() {
960
+ first.removeEventListener("abort", onAbort);
961
+ second.removeEventListener("abort", onAbort);
962
+ },
963
+ signal: controller.signal
964
+ };
965
+ }
966
+ function noop() {}
967
+ function isAbortError(error) {
968
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
969
+ }
970
+ function sleep(ms, signal) {
971
+ return new Promise((resolve, reject) => {
972
+ const onAbort = () => {
973
+ clearTimeout(timer);
974
+ signal?.removeEventListener("abort", onAbort);
975
+ reject(signal.reason);
976
+ };
977
+ const timer = setTimeout(() => {
978
+ signal?.removeEventListener("abort", onAbort);
979
+ resolve();
980
+ }, ms);
981
+ signal?.addEventListener("abort", onAbort, { once: true });
982
+ if (signal?.aborted) {
983
+ onAbort();
984
+ }
985
+ });
986
+ }
987
+ // src/helpers/timeout.ts
988
+ async function withTimeout(timeoutMs, task, options = {}) {
989
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
990
+ throw new RangeError("timeoutMs must be a positive number");
991
+ }
992
+ const parent = options.signal;
993
+ const controller = new AbortController;
994
+ const timeoutError = new TMDBTimeoutError(timeoutMs, options.message);
995
+ const onAbort = () => controller.abort(parent.reason);
996
+ parent?.addEventListener("abort", onAbort, { once: true });
997
+ if (parent?.aborted) {
998
+ parent.removeEventListener("abort", onAbort);
999
+ throw parent.reason;
1000
+ }
1001
+ const timer = setTimeout(() => controller.abort(timeoutError), timeoutMs);
1002
+ try {
1003
+ return await task(controller.signal);
1004
+ } catch (error) {
1005
+ if (controller.signal.reason === timeoutError) {
1006
+ throw timeoutError;
1007
+ }
1008
+ if (parent?.aborted) {
1009
+ throw parent.reason;
1010
+ }
1011
+ throw error;
1012
+ } finally {
1013
+ clearTimeout(timer);
1014
+ parent?.removeEventListener("abort", onAbort);
1015
+ }
1016
+ }
657
1017
  // src/search-guards.ts
658
1018
  function isMovieSearchResult(result) {
659
1019
  return isObjectWithMediaType(result, "movie");
@@ -737,9 +1097,11 @@ export {
737
1097
  KNOWN_STILL_SIZES,
738
1098
  TMDBClient,
739
1099
  TMDBError,
1100
+ TMDBHttpClient,
740
1101
  TMDBRateLimitError,
741
1102
  TMDBRequestError,
742
1103
  TMDBResponseError,
1104
+ TMDBTimeoutError,
743
1105
  buildImageUrl,
744
1106
  createTMDB,
745
1107
  isKnownBackdropSize,
@@ -753,8 +1115,14 @@ export {
753
1115
  isPerson,
754
1116
  isPersonSearchResult,
755
1117
  isTV,
756
- isTVSearchResult
1118
+ isTVSearchResult,
1119
+ paginate,
1120
+ paginateItems,
1121
+ retry,
1122
+ withMemoryCache,
1123
+ withRetry,
1124
+ withTimeout
757
1125
  };
758
1126
 
759
- //# debugId=00B61D5405D17A8964756E2164756E21
1127
+ //# debugId=24603BBDC4DA497C64756E2164756E21
760
1128
  //# sourceMappingURL=index.js.map