webcrawlerapi-js 2.2.5 → 2.2.7

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 CHANGED
@@ -46,6 +46,23 @@ for (const item of job.job_items) {
46
46
  }
47
47
  ```
48
48
 
49
+ ## Batch scrape
50
+
51
+ ```javascript
52
+ const batch = await client.scrape({
53
+ url: ["https://books.toscrape.com/", "https://books.toscrape.com/catalogue/page-2.html"],
54
+ output_formats: ["markdown"],
55
+ max_concurrency: 5,
56
+ })
57
+
58
+ console.log(batch.status, batch.completed, batch.failed)
59
+ for (const item of batch.data) console.log(item.url, item.status, item.markdown_url)
60
+ ```
61
+
62
+ Passing an array as `url` to `scrape()` creates a batch and polls until it finishes (a single string still returns a single result). Batch-only options like `max_concurrency` and `ignore_invalid_urls` go in the same request. For manual control use `scrapeBatchAsync({ urls, ... })`, `getBatch(id, { limit, cursor })`, `getBatchErrors(id)` and `cancelBatch(id)`.
63
+
64
+ Full batch scrape documentation: [docs/batch-scrape.md](docs/batch-scrape.md).
65
+
49
66
  ## Run an agent
50
67
 
51
68
  ```javascript
@@ -1,4 +1,4 @@
1
- import { CrawlRequest, Job, JobId, ScrapeRequest, ScrapeResponse, ScrapeResponseError, ScrapeId, JobMarkdownResponse, MarkdownRequest, MarkdownResponse, AgentRunRequest, AgentRun } from "./model";
1
+ import { CrawlRequest, Job, JobId, ScrapeRequest, ScrapeResponse, ScrapeResponseError, ScrapeId, JobMarkdownResponse, MarkdownRequest, MarkdownResponse, AgentRunRequest, AgentRun, BatchScrapeRequest, BatchScrapeId, BatchScrape, BatchScrapeGetOptions, BatchScrapeErrors, ScrapeBatchRequest } from "./model";
2
2
  import { JobStatus, ErrorCode } from "./constants";
3
3
  import { WebcrawlerApiError } from "./errors";
4
4
  export { WebcrawlerApiError, JobStatus, ErrorCode };
@@ -14,7 +14,26 @@ export declare class WebcrawlerClient {
14
14
  getAgentJob(jobID: string): Promise<AgentRun>;
15
15
  runAgent(request: AgentRunRequest, maxPolls?: number): Promise<AgentRun>;
16
16
  getScrape(scrapeId: string): Promise<ScrapeResponse | ScrapeResponseError>;
17
+ /**
18
+ * Scrapes a single URL and waits for the result.
19
+ *
20
+ * Passing `url` as an array creates a batch scrape instead, polls until it reaches a
21
+ * terminal status (completed, failed or cancelled) and returns the first page of items;
22
+ * follow `next` with getBatch(id, { cursor }) for larger batches. Batch-only options
23
+ * (max_concurrency, ignore_invalid_urls, ...) go in the same request object. A batch throws a
24
+ * `timeout` error if it does not finish within an hour or maxPolls.
25
+ * For manual batch control use scrapeBatchAsync() + getBatch().
26
+ */
17
27
  scrape(request: ScrapeRequest, maxPolls?: number): Promise<ScrapeResponse | ScrapeResponseError>;
28
+ scrape(request: ScrapeBatchRequest, maxPolls?: number): Promise<BatchScrape>;
29
+ private scrapeSingle;
30
+ scrapeBatchAsync(request: BatchScrapeRequest): Promise<BatchScrapeId>;
31
+ getBatch(batchId: string, options?: BatchScrapeGetOptions): Promise<BatchScrape>;
32
+ getBatchErrors(batchId: string): Promise<BatchScrapeErrors>;
33
+ cancelBatch(batchId: string): Promise<{
34
+ status: string;
35
+ }>;
36
+ private scrapeBatchAndWait;
18
37
  crawl(crawlRequest: CrawlRequest, maxPolls?: number): Promise<Job>;
19
38
  crawlAsync(crawlRequest: CrawlRequest): Promise<JobId>;
20
39
  getJob(jobID: string): Promise<Job>;
@@ -29,6 +29,8 @@ export interface ScrapeResponse {
29
29
  page_title?: string;
30
30
  structured_data?: any;
31
31
  links?: string[];
32
+ /** Server-recommended delay before the next status poll, if provided */
33
+ recommended_pull_delay_ms?: number;
32
34
  }
33
35
  export interface ScrapeResponseError {
34
36
  success: boolean;
@@ -161,3 +163,76 @@ export interface AgentRunList {
161
163
  items: AgentRun[];
162
164
  total: number;
163
165
  }
166
+ export type BatchScrapeStatus = 'scraping' | 'completed' | 'failed' | 'cancelled';
167
+ export interface BatchScrapeRequest {
168
+ /** URLs to scrape (max is your plan's job items limit). */
169
+ urls: string[];
170
+ /** Default is ["markdown"]. */
171
+ output_formats?: ('markdown' | 'cleaned' | 'html' | 'links' | 'images')[];
172
+ main_content_only?: boolean;
173
+ /** CSS selectors to clean from the output. */
174
+ clean_selectors?: string;
175
+ /** Max URLs scraped in parallel for this batch. 0 or omitted means no per-batch cap. */
176
+ max_concurrency?: number;
177
+ /** Skip invalid URLs and report them in invalid_urls instead of failing the request (default: true). */
178
+ ignore_invalid_urls?: boolean;
179
+ /** Public http(s) URL called once when the batch finishes. */
180
+ webhook_url?: string;
181
+ /** Cache duration in seconds (default 604800). 0 bypasses the cache. */
182
+ max_age?: number;
183
+ respect_robots_txt?: boolean;
184
+ }
185
+ /** Request for scrape() with an array `url`: runs a batch scrape and waits for it. Same options as BatchScrapeRequest. */
186
+ export type ScrapeBatchRequest = Omit<BatchScrapeRequest, 'urls'> & {
187
+ /** URLs to scrape as a batch (max is your plan's job items limit). */
188
+ url: string[];
189
+ };
190
+ export interface BatchScrapeInvalidUrl {
191
+ url: string;
192
+ error_code: string;
193
+ error_message: string;
194
+ }
195
+ export interface BatchScrapeId {
196
+ id: string;
197
+ status_url: string;
198
+ invalid_urls: BatchScrapeInvalidUrl[];
199
+ }
200
+ export interface BatchScrapeItem {
201
+ url: string;
202
+ /** Item status, for example "done" or "error". */
203
+ status: string;
204
+ page_status_code?: number;
205
+ title?: string;
206
+ markdown_url?: string;
207
+ cleaned_url?: string;
208
+ html_url?: string;
209
+ from_cache?: boolean;
210
+ error_code?: string;
211
+ error_message?: string;
212
+ }
213
+ export interface BatchScrape {
214
+ id: string;
215
+ status: BatchScrapeStatus | string;
216
+ total: number;
217
+ completed: number;
218
+ failed: number;
219
+ canceled: number;
220
+ data: BatchScrapeItem[];
221
+ /** Cursor for the next page. Empty on the last page. */
222
+ next?: string;
223
+ }
224
+ export interface BatchScrapeGetOptions {
225
+ /** Items per page, 1 to 500 (default 100). */
226
+ limit?: number;
227
+ /** Value of `next` from the previous page. */
228
+ cursor?: string;
229
+ }
230
+ export interface BatchScrapeError {
231
+ url: string;
232
+ error_code: string;
233
+ error_message: string;
234
+ }
235
+ export interface BatchScrapeErrors {
236
+ errors: BatchScrapeError[];
237
+ robots_blocked: string[];
238
+ }
package/dist/api.js CHANGED
@@ -24,6 +24,12 @@ const BASE_PATH = "https://api.webcrawlerapi.com";
24
24
  const initialPullDelayMs = 2000;
25
25
  const MaxPullRetries = 100;
26
26
  const DEFAULT_POLL_DELAY_SECONDS = 2;
27
+ const SCRAPE_MAX_WAIT_MS = 400 * 1000;
28
+ // Effectively unbounded by count at the 2s default; elapsed time is the real limit
29
+ const SCRAPE_MAX_POLLS = 1000;
30
+ const BATCH_VERSION = "v2";
31
+ const BATCH_MAX_WAIT_MS = 3600 * 1000;
32
+ const BatchTerminalStatuses = ["completed", "failed", "cancelled", "canceled"];
27
33
  const AgentTerminalStatuses = ["done", "error", "canceled"];
28
34
  __exportStar(require("./model"), exports);
29
35
  const SCRAPE_VERSION = "v2";
@@ -183,17 +189,28 @@ class WebcrawlerClient {
183
189
  return {
184
190
  success: false,
185
191
  status: status,
186
- page_status_code: 0
192
+ page_status_code: 0,
193
+ recommended_pull_delay_ms: responseData.recommended_pull_delay_ms
187
194
  };
188
195
  }
189
196
  }
190
- async scrape(request, maxPolls = 100) {
197
+ async scrape(request, maxPolls = SCRAPE_MAX_POLLS) {
198
+ if (Array.isArray(request.url)) {
199
+ const { url, ...options } = request;
200
+ return await this.scrapeBatchAndWait({ ...options, urls: url }, maxPolls);
201
+ }
202
+ return await this.scrapeSingle(request, maxPolls);
203
+ }
204
+ async scrapeSingle(request, maxPolls) {
191
205
  // Start the scraping job
192
206
  const scrapeIdResponse = await this.scrapeAsync(request);
193
207
  const scrapeId = scrapeIdResponse.id;
208
+ const startedAt = Date.now();
194
209
  let polls = 0;
210
+ let delayMs = DEFAULT_POLL_DELAY_SECONDS * 1000;
195
211
  let result;
196
- while (polls < maxPolls) {
212
+ // Bounded by both elapsed time (SCRAPE_MAX_WAIT_MS) and maxPolls
213
+ while (polls < maxPolls && Date.now() - startedAt < SCRAPE_MAX_WAIT_MS) {
197
214
  result = await this.getScrape(scrapeId);
198
215
  // Return immediately if scrape is done
199
216
  if ('status' in result && result.status === "done") {
@@ -203,14 +220,73 @@ class WebcrawlerClient {
203
220
  if ('error_code' in result) {
204
221
  return result;
205
222
  }
206
- // Continue polling if status is in_progress or any other non-terminal status
207
- // Wait before next poll
208
- await new Promise(resolve => setTimeout(resolve, DEFAULT_POLL_DELAY_SECONDS * 1000));
223
+ // Use server-recommended delay when present, otherwise keep the default
224
+ const recommended = result.recommended_pull_delay_ms;
225
+ delayMs = recommended && recommended > 0 ? recommended : DEFAULT_POLL_DELAY_SECONDS * 1000;
226
+ // Don't sleep past the overall deadline
227
+ const remainingMs = SCRAPE_MAX_WAIT_MS - (Date.now() - startedAt);
228
+ if (remainingMs <= 0)
229
+ break;
230
+ await new Promise(resolve => setTimeout(resolve, Math.min(delayMs, remainingMs)));
209
231
  polls++;
210
232
  }
211
- // Return the last known state if maxPolls is reached
233
+ // Return the last known state on timeout
212
234
  return result;
213
235
  }
236
+ async scrapeBatchAsync(request) {
237
+ const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape`;
238
+ const requestOptions = {
239
+ 'method': 'POST',
240
+ 'headers': this.getDefaultHeaders(),
241
+ 'body': JSON.stringify(request),
242
+ };
243
+ return await this.sendRequest(url, requestOptions);
244
+ }
245
+ async getBatch(batchId, options = {}) {
246
+ const params = new URLSearchParams();
247
+ if (options.limit !== undefined)
248
+ params.set('limit', String(options.limit));
249
+ if (options.cursor)
250
+ params.set('cursor', options.cursor);
251
+ const query = params.toString();
252
+ const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape/${batchId}${query ? `?${query}` : ''}`;
253
+ const requestOptions = {
254
+ 'method': 'GET',
255
+ 'headers': this.getDefaultHeaders(true),
256
+ };
257
+ return await this.sendRequest(url, requestOptions);
258
+ }
259
+ async getBatchErrors(batchId) {
260
+ const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape/${batchId}/errors`;
261
+ const requestOptions = {
262
+ 'method': 'GET',
263
+ 'headers': this.getDefaultHeaders(true),
264
+ };
265
+ return await this.sendRequest(url, requestOptions);
266
+ }
267
+ async cancelBatch(batchId) {
268
+ const url = `${this.basePath}/${BATCH_VERSION}/batch/scrape/${batchId}`;
269
+ const requestOptions = {
270
+ 'method': 'DELETE',
271
+ 'headers': this.getDefaultHeaders(),
272
+ };
273
+ return await this.sendRequest(url, requestOptions);
274
+ }
275
+ async scrapeBatchAndWait(request, maxPolls) {
276
+ const created = await this.scrapeBatchAsync(request);
277
+ if (!created.id) {
278
+ throw new errors_1.WebcrawlerApiError('invalid_response', 'Failed to create batch scrape', 0);
279
+ }
280
+ const startedAt = Date.now();
281
+ for (let i = 0; i < maxPolls && Date.now() - startedAt < BATCH_MAX_WAIT_MS; i++) {
282
+ const batch = await this.getBatch(created.id);
283
+ if (BatchTerminalStatuses.includes(batch.status)) {
284
+ return batch;
285
+ }
286
+ await new Promise(resolve => setTimeout(resolve, DEFAULT_POLL_DELAY_SECONDS * 1000));
287
+ }
288
+ throw new errors_1.WebcrawlerApiError('timeout', 'Batch scrape took too long, use scrapeBatchAsync + getBatch to keep polling', 0);
289
+ }
214
290
  async crawl(crawlRequest, maxPolls = MaxPullRetries) {
215
291
  const url = `${this.basePath}/${this.apiVersion}/crawl`;
216
292
  const requestBody = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcrawlerapi-js",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
4
4
  "description": "JS client for WebcrawlerAPI",
5
5
  "main": "./dist/api.js",
6
6
  "types": "./dist/@types/api.d.ts",