soundcloud-api-ts 1.2.0 → 1.3.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.mjs CHANGED
@@ -1,6 +1,35 @@
1
+ import { SoundCloudError } from './chunk-GKNBLKPB.mjs';
2
+ export { SoundCloudError } from './chunk-GKNBLKPB.mjs';
3
+
1
4
  // src/client/http.ts
2
5
  var BASE_URL = "https://api.soundcloud.com";
6
+ var DEFAULT_RETRY = { maxRetries: 3, retryBaseDelay: 1e3 };
7
+ function delay(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ function isRetryable(status) {
11
+ return status === 429 || status >= 500 && status <= 599;
12
+ }
13
+ function getRetryDelay(response, attempt, config) {
14
+ if (response.status === 429) {
15
+ const retryAfter = response.headers.get("retry-after");
16
+ if (retryAfter) {
17
+ const seconds = Number(retryAfter);
18
+ if (!Number.isNaN(seconds)) return seconds * 1e3;
19
+ }
20
+ }
21
+ const base = config.retryBaseDelay * Math.pow(2, attempt);
22
+ return base + Math.random() * base * 0.1;
23
+ }
24
+ async function parseErrorBody(response) {
25
+ try {
26
+ return await response.json();
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ }
3
31
  async function scFetch(options, refreshCtx) {
32
+ const retryConfig = refreshCtx?.retry ?? DEFAULT_RETRY;
4
33
  const execute = async (tokenOverride) => {
5
34
  const url = `${BASE_URL}${options.path}`;
6
35
  const headers = {
@@ -24,32 +53,44 @@ async function scFetch(options, refreshCtx) {
24
53
  } else if (options.contentType) {
25
54
  headers["Content-Type"] = options.contentType;
26
55
  }
27
- const response = await fetch(url, {
28
- method: options.method,
29
- headers,
30
- body: fetchBody,
31
- redirect: "manual"
32
- });
33
- if (response.status === 302) {
34
- const location = response.headers.get("location");
35
- if (location) {
36
- return location;
56
+ let lastResponse;
57
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
58
+ const response = await fetch(url, {
59
+ method: options.method,
60
+ headers,
61
+ body: fetchBody,
62
+ redirect: "manual"
63
+ });
64
+ if (response.status === 302) {
65
+ const location = response.headers.get("location");
66
+ if (location) return location;
67
+ }
68
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
69
+ return void 0;
70
+ }
71
+ if (response.ok) {
72
+ return response.json();
73
+ }
74
+ if (!isRetryable(response.status)) {
75
+ const body2 = await parseErrorBody(response);
76
+ throw new SoundCloudError(response.status, response.statusText, body2);
77
+ }
78
+ lastResponse = response;
79
+ if (attempt < retryConfig.maxRetries) {
80
+ const delayMs = getRetryDelay(response, attempt, retryConfig);
81
+ retryConfig.onDebug?.(
82
+ `Retry ${attempt + 1}/${retryConfig.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
83
+ );
84
+ await delay(delayMs);
37
85
  }
38
86
  }
39
- if (response.status === 204 || response.headers.get("content-length") === "0") {
40
- return void 0;
41
- }
42
- if (!response.ok) {
43
- throw new Error(
44
- `SoundCloud API error: ${response.status} ${response.statusText}`
45
- );
46
- }
47
- return response.json();
87
+ const body = await parseErrorBody(lastResponse);
88
+ throw new SoundCloudError(lastResponse.status, lastResponse.statusText, body);
48
89
  };
49
90
  try {
50
91
  return await execute();
51
92
  } catch (err) {
52
- if (refreshCtx?.onTokenRefresh && err instanceof Error && err.message.includes("401")) {
93
+ if (refreshCtx?.onTokenRefresh && err instanceof SoundCloudError && err.status === 401) {
53
94
  const newToken = await refreshCtx.onTokenRefresh();
54
95
  refreshCtx.setToken(newToken.access_token, newToken.refresh_token);
55
96
  return execute(newToken.access_token);
@@ -57,21 +98,38 @@ async function scFetch(options, refreshCtx) {
57
98
  throw err;
58
99
  }
59
100
  }
60
- async function scFetchUrl(url, token) {
101
+ async function scFetchUrl(url, token, retryConfig) {
102
+ const config = retryConfig ?? DEFAULT_RETRY;
61
103
  const headers = { Accept: "application/json" };
62
104
  if (token) headers["Authorization"] = `OAuth ${token}`;
63
- const response = await fetch(url, { method: "GET", headers, redirect: "manual" });
64
- if (response.status === 302) {
65
- const location = response.headers.get("location");
66
- if (location) return location;
67
- }
68
- if (response.status === 204 || response.headers.get("content-length") === "0") {
69
- return void 0;
70
- }
71
- if (!response.ok) {
72
- throw new Error(`SoundCloud API error: ${response.status} ${response.statusText}`);
105
+ let lastResponse;
106
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
107
+ const response = await fetch(url, { method: "GET", headers, redirect: "manual" });
108
+ if (response.status === 302) {
109
+ const location = response.headers.get("location");
110
+ if (location) return location;
111
+ }
112
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
113
+ return void 0;
114
+ }
115
+ if (response.ok) {
116
+ return response.json();
117
+ }
118
+ if (!isRetryable(response.status)) {
119
+ const body2 = await parseErrorBody(response);
120
+ throw new SoundCloudError(response.status, response.statusText, body2);
121
+ }
122
+ lastResponse = response;
123
+ if (attempt < config.maxRetries) {
124
+ const delayMs = getRetryDelay(response, attempt, config);
125
+ config.onDebug?.(
126
+ `Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delayMs)}ms (status ${response.status})`
127
+ );
128
+ await delay(delayMs);
129
+ }
73
130
  }
74
- return response.json();
131
+ const body = await parseErrorBody(lastResponse);
132
+ throw new SoundCloudError(lastResponse.status, lastResponse.statusText, body);
75
133
  }
76
134
 
77
135
  // src/client/paginate.ts
@@ -124,14 +182,24 @@ var SoundCloudClient = class _SoundCloudClient {
124
182
  constructor(config) {
125
183
  this.config = config;
126
184
  const getToken = () => this._accessToken;
185
+ const retryConfig = {
186
+ maxRetries: config.maxRetries ?? 3,
187
+ retryBaseDelay: config.retryBaseDelay ?? 1e3,
188
+ onDebug: config.onDebug
189
+ };
127
190
  const refreshCtx = config.onTokenRefresh ? {
128
191
  getToken,
129
192
  onTokenRefresh: async () => {
130
193
  const result = await config.onTokenRefresh(this);
131
194
  return result;
132
195
  },
133
- setToken: (a, r) => this.setToken(a, r)
134
- } : void 0;
196
+ setToken: (a, r) => this.setToken(a, r),
197
+ retry: retryConfig
198
+ } : {
199
+ getToken,
200
+ setToken: (a, r) => this.setToken(a, r),
201
+ retry: retryConfig
202
+ };
135
203
  this.auth = new _SoundCloudClient.Auth(this.config);
136
204
  this.me = new _SoundCloudClient.Me(getToken, refreshCtx);
137
205
  this.users = new _SoundCloudClient.Users(getToken, refreshCtx);