@tidal-music/api 0.28.0 → 0.29.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
@@ -333,16 +333,136 @@ function f(e) {
333
333
  return e.endsWith("/") ? e.substring(0, e.length - 1) : e;
334
334
  }
335
335
  //#endregion
336
+ //#region src/retry.ts
337
+ var p = {
338
+ network: {
339
+ baseDelayMs: 1e3,
340
+ maxDelayMs: 16e3,
341
+ maxRetries: 10
342
+ },
343
+ status: {
344
+ baseDelayMs: 500,
345
+ maxDelayMs: 16e3,
346
+ maxRetries: 3
347
+ },
348
+ timeout: {
349
+ baseDelayMs: 8e3,
350
+ maxDelayMs: 32e3,
351
+ maxRetries: 3
352
+ }
353
+ }, m = 1e4, h = new Set([
354
+ "GET",
355
+ "HEAD",
356
+ "OPTIONS"
357
+ ]), g = (e) => globalThis.fetch(e);
358
+ function _(e = {}, t = g) {
359
+ if (e.enabled === !1) return t;
360
+ let n = y(e);
361
+ return async (e) => {
362
+ if (!h.has(e.method.toUpperCase())) return t(e);
363
+ let r = {
364
+ network: 0,
365
+ status: 0,
366
+ timeout: 0
367
+ };
368
+ for (;;) {
369
+ if (e.signal.aborted) throw S(e.signal.reason);
370
+ let i = await w(t, e);
371
+ if (i.kind === "response") {
372
+ if (!b(i.response.status) || r.status >= n.status.maxRetries) return i.response;
373
+ await C(x(n.status, r.status), e.signal), r.status += 1;
374
+ continue;
375
+ }
376
+ let a = i.kind;
377
+ if (r[a] >= n[a].maxRetries) throw i.error;
378
+ await C(x(n[a], r[a]), e.signal), r[a] += 1;
379
+ }
380
+ };
381
+ }
382
+ function v(e, t) {
383
+ return {
384
+ baseDelayMs: t?.baseDelayMs ?? e.baseDelayMs,
385
+ maxDelayMs: t?.maxDelayMs ?? e.maxDelayMs,
386
+ maxRetries: t?.maxRetries ?? e.maxRetries
387
+ };
388
+ }
389
+ function y(e) {
390
+ return {
391
+ network: v(p.network, e.network),
392
+ status: v(p.status, e.status),
393
+ timeout: v(p.timeout, e.timeout)
394
+ };
395
+ }
396
+ function b(e) {
397
+ return e === 429 || e >= 500 && e < 600;
398
+ }
399
+ function x(e, t) {
400
+ let n = .8 + Math.random() * .2;
401
+ return Math.min(e.baseDelayMs * 2 ** t * n, e.maxDelayMs);
402
+ }
403
+ function S(e) {
404
+ return e instanceof Error ? e : Error(String(e));
405
+ }
406
+ function C(e, t) {
407
+ return new Promise((n, r) => {
408
+ if (t.aborted) {
409
+ r(S(t.reason));
410
+ return;
411
+ }
412
+ let i, a = () => {
413
+ clearTimeout(i), r(S(t.reason));
414
+ };
415
+ i = setTimeout(() => {
416
+ t.removeEventListener("abort", a), n();
417
+ }, e), t.addEventListener("abort", a, { once: !0 });
418
+ });
419
+ }
420
+ async function w(e, t) {
421
+ let n = new AbortController(), r = !1, i = setTimeout(() => {
422
+ r = !0, n.abort();
423
+ }, m), a = t.signal, o = () => {
424
+ n.abort();
425
+ };
426
+ a.aborted ? n.abort() : a.addEventListener("abort", o, { once: !0 });
427
+ try {
428
+ return {
429
+ kind: "response",
430
+ response: await T(await e(new Request(t, { signal: n.signal })))
431
+ };
432
+ } catch (e) {
433
+ if (a.aborted) throw e;
434
+ return r ? {
435
+ error: e,
436
+ kind: "timeout"
437
+ } : {
438
+ error: e,
439
+ kind: "network"
440
+ };
441
+ } finally {
442
+ clearTimeout(i), a.removeEventListener("abort", o);
443
+ }
444
+ }
445
+ async function T(e) {
446
+ if (!e.body) return e;
447
+ let t = await e.arrayBuffer();
448
+ return new Response(t, {
449
+ headers: e.headers,
450
+ status: e.status,
451
+ statusText: e.statusText
452
+ });
453
+ }
454
+ //#endregion
336
455
  //#region src/api.ts
337
- function p(e, t = "https://openapi.tidal.com/v2/") {
338
- let n = { async onRequest({ request: t }) {
456
+ function E(e, t = "https://openapi.tidal.com/v2/", n) {
457
+ let i = { async onRequest({ request: t }) {
339
458
  let n = await e.getCredentials();
340
459
  return t.headers.set("Authorization", `Bearer ${n.token}`), (t.method === "POST" || t.method === "PATCH" || t.method === "DELETE") && t.headers.set("Content-Type", "application/vnd.api+json"), t;
341
- } }, i = r({
460
+ } }, a = r({
342
461
  baseUrl: t,
462
+ fetch: _(n),
343
463
  querySerializer: { allowReserved: !0 }
344
464
  });
345
- return i.use(n), i;
465
+ return a.use(i), a;
346
466
  }
347
467
  //#endregion
348
- export { p as createAPIClient };
468
+ export { E as createAPIClient };
package/dist/src/api.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { CredentialsProvider } from '@tidal-music/common';
2
2
  import { paths } from './allAPI.generated.js';
3
+ import { RetryOptions } from './retry.js';
3
4
  /**
4
5
  * Create a Tidal API client with the provided credentials.
5
6
  *
6
7
  * @param credentialsProvider The credentials provider, from Auth module.
7
8
  * @param baseUrl Override the base URL to use for the API client.
9
+ * @param retryOptions Customize the retry mechanism applied to read-only
10
+ * (GET/HEAD/OPTIONS) requests. Pass `{ enabled: false }` to disable it.
8
11
  * @returns A Tidal API client.
9
12
  */
10
- export declare function createAPIClient(credentialsProvider: CredentialsProvider, baseUrl?: string): import('openapi-fetch').Client<paths, `${string}/${string}`>;
13
+ export declare function createAPIClient(credentialsProvider: CredentialsProvider, baseUrl?: string, retryOptions?: RetryOptions): import('openapi-fetch').Client<paths, `${string}/${string}`>;
@@ -8,3 +8,4 @@
8
8
  */
9
9
  export type { components } from './allAPI.generated.js';
10
10
  export { createAPIClient } from './api.js';
11
+ export type { RetryOptions } from './retry.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * HTTP retry mechanism for the TIDAL API client
3
+ *
4
+ * Read-only requests (GET/HEAD/OPTIONS) are retried with exponential backoff
5
+ * and jitter. The delay before a retry is
6
+ *
7
+ * D = min(B * 2 ** n * j, M)
8
+ *
9
+ * where B is the base delay, n is the zero-based retry number, j is a random
10
+ * jitter factor in [0.8, 1) and M is the maximum delay.
11
+ *
12
+ * Write requests (POST/PATCH/PUT/DELETE) are passed through untouched.
13
+ */
14
+ /** A fetch implementation compatible with openapi-fetch's `fetch` option. */
15
+ type FetchLike = (input: Request) => Promise<Response>;
16
+ type RetryPolicy = {
17
+ /** Base delay B in milliseconds. */
18
+ baseDelayMs: number;
19
+ /** Maximum delay M in milliseconds; the backoff is clamped to this. */
20
+ maxDelayMs: number;
21
+ /** Maximum number of retries N, in addition to the initial attempt. */
22
+ maxRetries: number;
23
+ };
24
+ type RetryCategory = 'network' | 'status' | 'timeout';
25
+ export type RetryOptions = Partial<Record<RetryCategory, Partial<RetryPolicy>>> & {
26
+ /** Set to `false` to disable retries and the read timeout entirely. */
27
+ enabled?: boolean;
28
+ };
29
+ /** Per-attempt read timeout in milliseconds. */
30
+ export declare const READ_TIMEOUT_MS = 10000;
31
+ /**
32
+ * Wraps a fetch implementation with a retry mechanism. The returned function
33
+ * matches openapi-fetch's `fetch` option signature, so it can be passed
34
+ * straight to `createClient({ fetch })`.
35
+ */
36
+ export declare function fetchWithRetry(options?: RetryOptions, baseFetch?: FetchLike): FetchLike;
37
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tidal-music/api",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"