scrapebadger 0.3.0 → 0.3.1
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/README.md +31 -5
- package/dist/{index-DQ_jDTcQ.d.cts → index-Cg0sNluO.d.cts} +22 -0
- package/dist/{index-DQ_jDTcQ.d.ts → index-Cg0sNluO.d.ts} +22 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +111 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +111 -19
- package/dist/index.mjs.map +1 -1
- package/dist/twitter/index.d.cts +1 -1
- package/dist/twitter/index.d.ts +1 -1
- package/dist/twitter/index.js +49 -10
- package/dist/twitter/index.js.map +1 -1
- package/dist/twitter/index.mjs +49 -10
- package/dist/twitter/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ The official Node.js/TypeScript client library for the [ScrapeBadger](https://sc
|
|
|
19
19
|
- **Full TypeScript Support** - Complete type definitions for all API endpoints
|
|
20
20
|
- **Modern ESM & CommonJS** - Works with both module systems
|
|
21
21
|
- **Async Iterators** - Automatic pagination with `for await...of` syntax
|
|
22
|
-
- **
|
|
22
|
+
- **Smart Rate Limit Handling** - Reads API rate limit headers and automatically throttles pagination to avoid hitting limits
|
|
23
|
+
- **Resilient Retries** - 10 automatic retries with exponential backoff on server errors, with colored console warnings on each retry
|
|
23
24
|
- **Error Handling** - Typed exceptions for different error scenarios
|
|
24
25
|
- **Tree Shakeable** - Import only what you need
|
|
25
26
|
|
|
@@ -345,20 +346,45 @@ const client = new ScrapeBadger({
|
|
|
345
346
|
// Required: Your API key (or use SCRAPEBADGER_API_KEY env var)
|
|
346
347
|
apiKey: "your-api-key",
|
|
347
348
|
|
|
348
|
-
// Optional: Custom base URL (default: https://
|
|
349
|
-
baseUrl: "https://
|
|
349
|
+
// Optional: Custom base URL (default: https://scrapebadger.com)
|
|
350
|
+
baseUrl: "https://scrapebadger.com",
|
|
350
351
|
|
|
351
352
|
// Optional: Request timeout in milliseconds (default: 30000)
|
|
352
353
|
timeout: 30000,
|
|
353
354
|
|
|
354
|
-
// Optional: Maximum retry attempts (default:
|
|
355
|
-
maxRetries:
|
|
355
|
+
// Optional: Maximum retry attempts (default: 10)
|
|
356
|
+
maxRetries: 10,
|
|
356
357
|
|
|
357
358
|
// Optional: Initial retry delay in milliseconds (default: 1000)
|
|
358
359
|
retryDelay: 1000,
|
|
359
360
|
});
|
|
360
361
|
```
|
|
361
362
|
|
|
363
|
+
### Retry Behavior
|
|
364
|
+
|
|
365
|
+
The SDK automatically retries requests that fail with server errors (5xx) or rate
|
|
366
|
+
limits (429) using exponential backoff (1s, 2s, 4s, 8s, ...). Each retry prints a
|
|
367
|
+
colored warning:
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
⚠ ScrapeBadger: 503 Service Unavailable — retrying in 4s (attempt 3/10)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Rate Limit Aware Pagination
|
|
374
|
+
|
|
375
|
+
When using `*All` pagination methods (e.g. `searchAll`, `getFollowersAll`), the SDK
|
|
376
|
+
reads `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers from each response.
|
|
377
|
+
When remaining requests drop below 20% of your tier's limit, pagination automatically
|
|
378
|
+
slows down to spread requests across the remaining window — preventing 429 errors:
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
⚠ ScrapeBadger: Rate limit: 25/300 remaining (resets in 42s), throttling pagination
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
This works transparently with all tier levels (Free: 60/min, Basic: 300/min,
|
|
385
|
+
Pro: 1000/min, Enterprise: 5000/min).
|
|
386
|
+
```
|
|
387
|
+
|
|
362
388
|
## API Reference
|
|
363
389
|
|
|
364
390
|
### Client
|
|
@@ -27,6 +27,15 @@ interface ResolvedConfig {
|
|
|
27
27
|
* Base HTTP client with retry logic and error handling.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
interface RateLimit {
|
|
31
|
+
limit: number;
|
|
32
|
+
remaining: number;
|
|
33
|
+
reset: number;
|
|
34
|
+
}
|
|
35
|
+
interface ResponseWithHeaders<T> {
|
|
36
|
+
data: T;
|
|
37
|
+
rateLimit?: RateLimit;
|
|
38
|
+
}
|
|
30
39
|
interface RequestOptions {
|
|
31
40
|
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
32
41
|
params?: Record<string, string | number | boolean | undefined>;
|
|
@@ -43,10 +52,22 @@ declare class BaseClient {
|
|
|
43
52
|
* Make an HTTP request to the API.
|
|
44
53
|
*/
|
|
45
54
|
request<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Make an HTTP request and return both data and rate limit headers.
|
|
57
|
+
*/
|
|
58
|
+
requestWithHeaders<T>(path: string, options?: RequestOptions): Promise<ResponseWithHeaders<T>>;
|
|
59
|
+
/**
|
|
60
|
+
* Internal method that builds the request and executes it, returning data and rate limit info.
|
|
61
|
+
*/
|
|
62
|
+
private requestRaw;
|
|
46
63
|
/**
|
|
47
64
|
* Execute request with exponential backoff retry logic.
|
|
48
65
|
*/
|
|
49
66
|
private executeWithRetry;
|
|
67
|
+
/**
|
|
68
|
+
* Parse rate limit headers from an HTTP response.
|
|
69
|
+
*/
|
|
70
|
+
private parseRateLimitHeaders;
|
|
50
71
|
/**
|
|
51
72
|
* Fetch with timeout support.
|
|
52
73
|
*/
|
|
@@ -64,6 +85,7 @@ declare class BaseClient {
|
|
|
64
85
|
/**
|
|
65
86
|
* Pagination utilities for the ScrapeBadger SDK.
|
|
66
87
|
*/
|
|
88
|
+
|
|
67
89
|
/**
|
|
68
90
|
* Response wrapper for paginated API responses.
|
|
69
91
|
*/
|
|
@@ -27,6 +27,15 @@ interface ResolvedConfig {
|
|
|
27
27
|
* Base HTTP client with retry logic and error handling.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
interface RateLimit {
|
|
31
|
+
limit: number;
|
|
32
|
+
remaining: number;
|
|
33
|
+
reset: number;
|
|
34
|
+
}
|
|
35
|
+
interface ResponseWithHeaders<T> {
|
|
36
|
+
data: T;
|
|
37
|
+
rateLimit?: RateLimit;
|
|
38
|
+
}
|
|
30
39
|
interface RequestOptions {
|
|
31
40
|
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
32
41
|
params?: Record<string, string | number | boolean | undefined>;
|
|
@@ -43,10 +52,22 @@ declare class BaseClient {
|
|
|
43
52
|
* Make an HTTP request to the API.
|
|
44
53
|
*/
|
|
45
54
|
request<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Make an HTTP request and return both data and rate limit headers.
|
|
57
|
+
*/
|
|
58
|
+
requestWithHeaders<T>(path: string, options?: RequestOptions): Promise<ResponseWithHeaders<T>>;
|
|
59
|
+
/**
|
|
60
|
+
* Internal method that builds the request and executes it, returning data and rate limit info.
|
|
61
|
+
*/
|
|
62
|
+
private requestRaw;
|
|
46
63
|
/**
|
|
47
64
|
* Execute request with exponential backoff retry logic.
|
|
48
65
|
*/
|
|
49
66
|
private executeWithRetry;
|
|
67
|
+
/**
|
|
68
|
+
* Parse rate limit headers from an HTTP response.
|
|
69
|
+
*/
|
|
70
|
+
private parseRateLimitHeaders;
|
|
50
71
|
/**
|
|
51
72
|
* Fetch with timeout support.
|
|
52
73
|
*/
|
|
@@ -64,6 +85,7 @@ declare class BaseClient {
|
|
|
64
85
|
/**
|
|
65
86
|
* Pagination utilities for the ScrapeBadger SDK.
|
|
66
87
|
*/
|
|
88
|
+
|
|
67
89
|
/**
|
|
68
90
|
* Response wrapper for paginated API responses.
|
|
69
91
|
*/
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BaseClient, T as TwitterClient, S as ScrapeBadgerConfig } from './index-
|
|
2
|
-
export { e as AccountRestrictedError, _ as ApiResponse, A as AuthenticationError, ae as BillingLog, af as BillingLogList, j as CommunitiesClient, J as Community, E as CommunityBanner, K as CommunityMember, F as CommunityRule, p as CommunityTweetType, C as ConflictError, ag as ConnectOptions, a6 as ConnectedEvent, a3 as CreateMonitorParams, ac as DeliveryLog, ad as DeliveryLogList, a9 as ErrorEvent, aj as FilterRuleCreate, ao as FilterRuleDeliveryLog, ap as FilterRuleDeliveryLogListResponse, am as FilterRuleListResponse, al as FilterRulePricingTier, aq as FilterRulePricingTiersResponse, ai as FilterRuleResponse, ah as FilterRuleStatus, ak as FilterRuleUpdate, an as FilterRuleValidateResponse, G as GeoClient, l as GeoSearchOptions, H as Hashtag, I as InsufficientCreditsError, g as IteratorOptions, D as List, $ as ListResponse, L as ListsClient, X as Location, M as Media, a0 as MonitorStatus, N as NotFoundError, P as PaginatedResponse, f as PaginationOptions, a7 as PingEvent, Z as Place, Y as PlaceTrends, r as Poll, q as PollOption, Q as QueryType, b as RateLimitError, R as ResolvedConfig, a as ScrapeBadgerError, c as ServerError, m as StreamClient, n as StreamEmitter, aa as StreamEvent, ab as StreamEventType, a1 as StreamMonitor, a2 as StreamMonitorList, a5 as StreamTweet, d as TimeoutError, O as Trend, o as TrendCategory, k as TrendsClient, w as Tweet, a8 as TweetEvent, u as TweetPlace, i as TweetsClient, a4 as UpdateMonitorParams, s as Url, x as User, y as UserAbout, z as UserIds, t as UserMention, U as UsersClient, V as ValidationError, W as WebSocketStreamError, h as collectAll, v as verifyWebhookSignature } from './index-
|
|
1
|
+
import { B as BaseClient, T as TwitterClient, S as ScrapeBadgerConfig } from './index-Cg0sNluO.cjs';
|
|
2
|
+
export { e as AccountRestrictedError, _ as ApiResponse, A as AuthenticationError, ae as BillingLog, af as BillingLogList, j as CommunitiesClient, J as Community, E as CommunityBanner, K as CommunityMember, F as CommunityRule, p as CommunityTweetType, C as ConflictError, ag as ConnectOptions, a6 as ConnectedEvent, a3 as CreateMonitorParams, ac as DeliveryLog, ad as DeliveryLogList, a9 as ErrorEvent, aj as FilterRuleCreate, ao as FilterRuleDeliveryLog, ap as FilterRuleDeliveryLogListResponse, am as FilterRuleListResponse, al as FilterRulePricingTier, aq as FilterRulePricingTiersResponse, ai as FilterRuleResponse, ah as FilterRuleStatus, ak as FilterRuleUpdate, an as FilterRuleValidateResponse, G as GeoClient, l as GeoSearchOptions, H as Hashtag, I as InsufficientCreditsError, g as IteratorOptions, D as List, $ as ListResponse, L as ListsClient, X as Location, M as Media, a0 as MonitorStatus, N as NotFoundError, P as PaginatedResponse, f as PaginationOptions, a7 as PingEvent, Z as Place, Y as PlaceTrends, r as Poll, q as PollOption, Q as QueryType, b as RateLimitError, R as ResolvedConfig, a as ScrapeBadgerError, c as ServerError, m as StreamClient, n as StreamEmitter, aa as StreamEvent, ab as StreamEventType, a1 as StreamMonitor, a2 as StreamMonitorList, a5 as StreamTweet, d as TimeoutError, O as Trend, o as TrendCategory, k as TrendsClient, w as Tweet, a8 as TweetEvent, u as TweetPlace, i as TweetsClient, a4 as UpdateMonitorParams, s as Url, x as User, y as UserAbout, z as UserIds, t as UserMention, U as UsersClient, V as ValidationError, W as WebSocketStreamError, h as collectAll, v as verifyWebhookSignature } from './index-Cg0sNluO.cjs';
|
|
3
3
|
import 'node:events';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BaseClient, T as TwitterClient, S as ScrapeBadgerConfig } from './index-
|
|
2
|
-
export { e as AccountRestrictedError, _ as ApiResponse, A as AuthenticationError, ae as BillingLog, af as BillingLogList, j as CommunitiesClient, J as Community, E as CommunityBanner, K as CommunityMember, F as CommunityRule, p as CommunityTweetType, C as ConflictError, ag as ConnectOptions, a6 as ConnectedEvent, a3 as CreateMonitorParams, ac as DeliveryLog, ad as DeliveryLogList, a9 as ErrorEvent, aj as FilterRuleCreate, ao as FilterRuleDeliveryLog, ap as FilterRuleDeliveryLogListResponse, am as FilterRuleListResponse, al as FilterRulePricingTier, aq as FilterRulePricingTiersResponse, ai as FilterRuleResponse, ah as FilterRuleStatus, ak as FilterRuleUpdate, an as FilterRuleValidateResponse, G as GeoClient, l as GeoSearchOptions, H as Hashtag, I as InsufficientCreditsError, g as IteratorOptions, D as List, $ as ListResponse, L as ListsClient, X as Location, M as Media, a0 as MonitorStatus, N as NotFoundError, P as PaginatedResponse, f as PaginationOptions, a7 as PingEvent, Z as Place, Y as PlaceTrends, r as Poll, q as PollOption, Q as QueryType, b as RateLimitError, R as ResolvedConfig, a as ScrapeBadgerError, c as ServerError, m as StreamClient, n as StreamEmitter, aa as StreamEvent, ab as StreamEventType, a1 as StreamMonitor, a2 as StreamMonitorList, a5 as StreamTweet, d as TimeoutError, O as Trend, o as TrendCategory, k as TrendsClient, w as Tweet, a8 as TweetEvent, u as TweetPlace, i as TweetsClient, a4 as UpdateMonitorParams, s as Url, x as User, y as UserAbout, z as UserIds, t as UserMention, U as UsersClient, V as ValidationError, W as WebSocketStreamError, h as collectAll, v as verifyWebhookSignature } from './index-
|
|
1
|
+
import { B as BaseClient, T as TwitterClient, S as ScrapeBadgerConfig } from './index-Cg0sNluO.js';
|
|
2
|
+
export { e as AccountRestrictedError, _ as ApiResponse, A as AuthenticationError, ae as BillingLog, af as BillingLogList, j as CommunitiesClient, J as Community, E as CommunityBanner, K as CommunityMember, F as CommunityRule, p as CommunityTweetType, C as ConflictError, ag as ConnectOptions, a6 as ConnectedEvent, a3 as CreateMonitorParams, ac as DeliveryLog, ad as DeliveryLogList, a9 as ErrorEvent, aj as FilterRuleCreate, ao as FilterRuleDeliveryLog, ap as FilterRuleDeliveryLogListResponse, am as FilterRuleListResponse, al as FilterRulePricingTier, aq as FilterRulePricingTiersResponse, ai as FilterRuleResponse, ah as FilterRuleStatus, ak as FilterRuleUpdate, an as FilterRuleValidateResponse, G as GeoClient, l as GeoSearchOptions, H as Hashtag, I as InsufficientCreditsError, g as IteratorOptions, D as List, $ as ListResponse, L as ListsClient, X as Location, M as Media, a0 as MonitorStatus, N as NotFoundError, P as PaginatedResponse, f as PaginationOptions, a7 as PingEvent, Z as Place, Y as PlaceTrends, r as Poll, q as PollOption, Q as QueryType, b as RateLimitError, R as ResolvedConfig, a as ScrapeBadgerError, c as ServerError, m as StreamClient, n as StreamEmitter, aa as StreamEvent, ab as StreamEventType, a1 as StreamMonitor, a2 as StreamMonitorList, a5 as StreamTweet, d as TimeoutError, O as Trend, o as TrendCategory, k as TrendsClient, w as Tweet, a8 as TweetEvent, u as TweetPlace, i as TweetsClient, a4 as UpdateMonitorParams, s as Url, x as User, y as UserAbout, z as UserIds, t as UserMention, U as UsersClient, V as ValidationError, W as WebSocketStreamError, h as collectAll, v as verifyWebhookSignature } from './index-Cg0sNluO.js';
|
|
3
3
|
import 'node:events';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.js
CHANGED
|
@@ -130,6 +130,19 @@ var BaseClient = class {
|
|
|
130
130
|
* Make an HTTP request to the API.
|
|
131
131
|
*/
|
|
132
132
|
async request(path, options = {}) {
|
|
133
|
+
const { data } = await this.requestRaw(path, options);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Make an HTTP request and return both data and rate limit headers.
|
|
138
|
+
*/
|
|
139
|
+
async requestWithHeaders(path, options = {}) {
|
|
140
|
+
return this.requestRaw(path, options);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Internal method that builds the request and executes it, returning data and rate limit info.
|
|
144
|
+
*/
|
|
145
|
+
async requestRaw(path, options = {}) {
|
|
133
146
|
const { method = "GET", params, body, headers = {} } = options;
|
|
134
147
|
const url = new URL(path, this.config.baseUrl);
|
|
135
148
|
if (params) {
|
|
@@ -143,7 +156,7 @@ var BaseClient = class {
|
|
|
143
156
|
"Content-Type": "application/json",
|
|
144
157
|
Accept: "application/json",
|
|
145
158
|
"X-API-Key": this.config.apiKey,
|
|
146
|
-
"User-Agent": "scrapebadger-node/0.1
|
|
159
|
+
"User-Agent": "scrapebadger-node/0.3.1",
|
|
147
160
|
...headers
|
|
148
161
|
};
|
|
149
162
|
const fetchOptions = {
|
|
@@ -162,8 +175,10 @@ var BaseClient = class {
|
|
|
162
175
|
let lastError;
|
|
163
176
|
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
164
177
|
try {
|
|
165
|
-
const
|
|
166
|
-
|
|
178
|
+
const httpResponse = await this.fetchWithTimeout(url, options);
|
|
179
|
+
const data = await this.handleResponse(httpResponse);
|
|
180
|
+
const rateLimit = this.parseRateLimitHeaders(httpResponse.headers);
|
|
181
|
+
return { data, rateLimit };
|
|
167
182
|
} catch (error) {
|
|
168
183
|
lastError = error;
|
|
169
184
|
if (error instanceof ScrapeBadgerError && !(error instanceof RateLimitError)) {
|
|
@@ -173,18 +188,56 @@ var BaseClient = class {
|
|
|
173
188
|
break;
|
|
174
189
|
}
|
|
175
190
|
const delay = this.config.retryDelay * Math.pow(2, attempt);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
191
|
+
const delaySec = Math.round(delay / 1e3);
|
|
192
|
+
const attemptNum = attempt + 1;
|
|
193
|
+
const maxRetries = this.config.maxRetries;
|
|
194
|
+
if (error instanceof RateLimitError) {
|
|
195
|
+
console.warn(
|
|
196
|
+
`\x1B[33m\u26A0 ScrapeBadger: 429 Rate Limited \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
|
|
197
|
+
);
|
|
198
|
+
if (error.retryAfter) {
|
|
199
|
+
const retryDelay = (error.retryAfter - Date.now() / 1e3) * 1e3;
|
|
200
|
+
if (retryDelay > 0 && retryDelay < 6e4) {
|
|
201
|
+
await this.sleep(retryDelay);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
181
204
|
}
|
|
205
|
+
} else if (error instanceof TimeoutError) {
|
|
206
|
+
console.warn(
|
|
207
|
+
`\x1B[33m\u26A0 ScrapeBadger: TimeoutError \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
|
|
208
|
+
);
|
|
209
|
+
} else if (error instanceof ServerError) {
|
|
210
|
+
console.warn(
|
|
211
|
+
`\x1B[33m\u26A0 ScrapeBadger: ${error.statusCode} ${error.message} \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
console.warn(
|
|
215
|
+
`\x1B[33m\u26A0 ScrapeBadger: ${error.name} \u2014 retrying in ${delaySec}s (attempt ${attemptNum}/${maxRetries})\x1B[0m`
|
|
216
|
+
);
|
|
182
217
|
}
|
|
183
218
|
await this.sleep(delay);
|
|
184
219
|
}
|
|
185
220
|
}
|
|
186
221
|
throw lastError ?? new ScrapeBadgerError("Request failed after retries");
|
|
187
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Parse rate limit headers from an HTTP response.
|
|
225
|
+
*/
|
|
226
|
+
parseRateLimitHeaders(headers) {
|
|
227
|
+
const limit = headers.get("X-RateLimit-Limit");
|
|
228
|
+
const remaining = headers.get("X-RateLimit-Remaining");
|
|
229
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
230
|
+
if (limit === null || remaining === null || reset === null) {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
const parsedLimit = parseInt(limit, 10);
|
|
234
|
+
const parsedRemaining = parseInt(remaining, 10);
|
|
235
|
+
const parsedReset = parseInt(reset, 10);
|
|
236
|
+
if (isNaN(parsedLimit) || isNaN(parsedRemaining) || isNaN(parsedReset)) {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
return { limit: parsedLimit, remaining: parsedRemaining, reset: parsedReset };
|
|
240
|
+
}
|
|
188
241
|
/**
|
|
189
242
|
* Fetch with timeout support.
|
|
190
243
|
*/
|
|
@@ -266,7 +319,7 @@ var BaseClient = class {
|
|
|
266
319
|
// src/internal/config.ts
|
|
267
320
|
var DEFAULT_BASE_URL = "https://scrapebadger.com";
|
|
268
321
|
var DEFAULT_TIMEOUT = 3e4;
|
|
269
|
-
var DEFAULT_MAX_RETRIES =
|
|
322
|
+
var DEFAULT_MAX_RETRIES = 10;
|
|
270
323
|
var DEFAULT_RETRY_DELAY = 1e3;
|
|
271
324
|
function resolveConfig(config) {
|
|
272
325
|
if (!config.apiKey) {
|
|
@@ -295,12 +348,26 @@ function createPaginatedResponse(data, cursor) {
|
|
|
295
348
|
hasMore: !!cursor
|
|
296
349
|
};
|
|
297
350
|
}
|
|
351
|
+
var RATE_LIMIT_WARN_THRESHOLD = 0.2;
|
|
298
352
|
async function* paginate(fetchPage, options = {}) {
|
|
299
353
|
const { maxItems } = options;
|
|
300
354
|
let cursor;
|
|
301
355
|
let totalYielded = 0;
|
|
302
356
|
do {
|
|
303
|
-
const response = await fetchPage(cursor);
|
|
357
|
+
const { response, rateLimit } = await fetchPage(cursor);
|
|
358
|
+
if (rateLimit) {
|
|
359
|
+
const { limit, remaining, reset } = rateLimit;
|
|
360
|
+
if (limit > 0 && remaining / limit < RATE_LIMIT_WARN_THRESHOLD) {
|
|
361
|
+
const nowSec = Date.now() / 1e3;
|
|
362
|
+
const windowRemainingSec = Math.max(reset - nowSec, 1);
|
|
363
|
+
const delayMs = remaining > 0 ? windowRemainingSec / remaining * 1e3 : windowRemainingSec * 1e3;
|
|
364
|
+
const resetInSec = Math.round(windowRemainingSec);
|
|
365
|
+
console.warn(
|
|
366
|
+
`\x1B[33m\u26A0 ScrapeBadger: Rate limit: ${remaining}/${limit} remaining (resets in ${resetInSec}s), throttling pagination\x1B[0m`
|
|
367
|
+
);
|
|
368
|
+
await sleep(delayMs);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
304
371
|
for (const item of response.data) {
|
|
305
372
|
yield item;
|
|
306
373
|
totalYielded++;
|
|
@@ -318,6 +385,9 @@ async function collectAll(generator) {
|
|
|
318
385
|
}
|
|
319
386
|
return items;
|
|
320
387
|
}
|
|
388
|
+
function sleep(ms) {
|
|
389
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
390
|
+
}
|
|
321
391
|
|
|
322
392
|
// src/twitter/tweets.ts
|
|
323
393
|
var TweetsClient = class {
|
|
@@ -515,7 +585,8 @@ var TweetsClient = class {
|
|
|
515
585
|
*/
|
|
516
586
|
async *getQuotesAll(tweetId, options = {}) {
|
|
517
587
|
const fetchPage = async (cursor) => {
|
|
518
|
-
|
|
588
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/tweets/tweet/${tweetId}/quotes`, { params: { cursor } });
|
|
589
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
519
590
|
};
|
|
520
591
|
yield* paginate(fetchPage, options);
|
|
521
592
|
}
|
|
@@ -582,7 +653,15 @@ var TweetsClient = class {
|
|
|
582
653
|
*/
|
|
583
654
|
async *searchAll(query, options = {}) {
|
|
584
655
|
const fetchPage = async (cursor) => {
|
|
585
|
-
|
|
656
|
+
const { data, rateLimit } = await this.client.requestWithHeaders("/v1/twitter/tweets/advanced_search", {
|
|
657
|
+
params: {
|
|
658
|
+
query,
|
|
659
|
+
query_type: options.queryType ?? "Top",
|
|
660
|
+
count: options.count,
|
|
661
|
+
cursor
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
586
665
|
};
|
|
587
666
|
yield* paginate(fetchPage, options);
|
|
588
667
|
}
|
|
@@ -626,7 +705,8 @@ var TweetsClient = class {
|
|
|
626
705
|
*/
|
|
627
706
|
async *getUserTweetsAll(username, options = {}) {
|
|
628
707
|
const fetchPage = async (cursor) => {
|
|
629
|
-
|
|
708
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/users/${username}/latest_tweets`, { params: { cursor } });
|
|
709
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
630
710
|
};
|
|
631
711
|
yield* paginate(fetchPage, options);
|
|
632
712
|
}
|
|
@@ -736,7 +816,8 @@ var UsersClient = class {
|
|
|
736
816
|
*/
|
|
737
817
|
async *getFollowersAll(username, options = {}) {
|
|
738
818
|
const fetchPage = async (cursor) => {
|
|
739
|
-
|
|
819
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/users/${username}/followers`, { params: { cursor } });
|
|
820
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
740
821
|
};
|
|
741
822
|
yield* paginate(fetchPage, options);
|
|
742
823
|
}
|
|
@@ -771,7 +852,8 @@ var UsersClient = class {
|
|
|
771
852
|
*/
|
|
772
853
|
async *getFollowingAll(username, options = {}) {
|
|
773
854
|
const fetchPage = async (cursor) => {
|
|
774
|
-
|
|
855
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/users/${username}/followings`, { params: { cursor } });
|
|
856
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
775
857
|
};
|
|
776
858
|
yield* paginate(fetchPage, options);
|
|
777
859
|
}
|
|
@@ -934,7 +1016,8 @@ var UsersClient = class {
|
|
|
934
1016
|
*/
|
|
935
1017
|
async *searchAll(query, options = {}) {
|
|
936
1018
|
const fetchPage = async (cursor) => {
|
|
937
|
-
|
|
1019
|
+
const { data, rateLimit } = await this.client.requestWithHeaders("/v1/twitter/users/search_users", { params: { query, cursor } });
|
|
1020
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
938
1021
|
};
|
|
939
1022
|
yield* paginate(fetchPage, options);
|
|
940
1023
|
}
|
|
@@ -994,7 +1077,8 @@ var ListsClient = class {
|
|
|
994
1077
|
*/
|
|
995
1078
|
async *getTweetsAll(listId, options = {}) {
|
|
996
1079
|
const fetchPage = async (cursor) => {
|
|
997
|
-
|
|
1080
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/lists/${listId}/tweets`, { params: { cursor } });
|
|
1081
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
998
1082
|
};
|
|
999
1083
|
yield* paginate(fetchPage, options);
|
|
1000
1084
|
}
|
|
@@ -1029,7 +1113,8 @@ var ListsClient = class {
|
|
|
1029
1113
|
*/
|
|
1030
1114
|
async *getMembersAll(listId, options = {}) {
|
|
1031
1115
|
const fetchPage = async (cursor) => {
|
|
1032
|
-
|
|
1116
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/lists/${listId}/members`, { params: { cursor } });
|
|
1117
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
1033
1118
|
};
|
|
1034
1119
|
yield* paginate(fetchPage, options);
|
|
1035
1120
|
}
|
|
@@ -1177,7 +1262,14 @@ var CommunitiesClient = class {
|
|
|
1177
1262
|
*/
|
|
1178
1263
|
async *getTweetsAll(communityId, options = {}) {
|
|
1179
1264
|
const fetchPage = async (cursor) => {
|
|
1180
|
-
|
|
1265
|
+
const { data, rateLimit } = await this.client.requestWithHeaders(`/v1/twitter/communities/${communityId}/tweets`, {
|
|
1266
|
+
params: {
|
|
1267
|
+
tweet_type: options.tweetType ?? "Top",
|
|
1268
|
+
count: options.count ?? 40,
|
|
1269
|
+
cursor
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
return { response: createPaginatedResponse(data.data ?? [], data.next_cursor), rateLimit };
|
|
1181
1273
|
};
|
|
1182
1274
|
yield* paginate(fetchPage, options);
|
|
1183
1275
|
}
|