zenrows 2.1.1 → 2.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/README.md +93 -0
- package/dist/index.cjs +287 -24
- package/dist/index.d.cts +207 -1
- package/dist/index.d.ts +207 -1
- package/dist/index.js +284 -24
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import fetchRetry from "fetch-retry";
|
|
|
5
5
|
// package.json
|
|
6
6
|
var package_default = {
|
|
7
7
|
name: "zenrows",
|
|
8
|
-
version: "2.
|
|
8
|
+
version: "2.2.0",
|
|
9
9
|
description: "ZenRows Node SDK",
|
|
10
10
|
main: "./dist/index.cjs",
|
|
11
11
|
module: "./dist/index.js",
|
|
@@ -24,6 +24,7 @@ var package_default = {
|
|
|
24
24
|
scripts: {
|
|
25
25
|
build: "tsup src/index.ts --dts --format cjs,esm --clean",
|
|
26
26
|
test: "vitest",
|
|
27
|
+
"test:e2e": "vitest run tests/e2e",
|
|
27
28
|
format: "biome format --write .",
|
|
28
29
|
check: "biome check ."
|
|
29
30
|
},
|
|
@@ -175,12 +176,13 @@ var ZenRowsBatchError = class extends Error {
|
|
|
175
176
|
code;
|
|
176
177
|
extras;
|
|
177
178
|
constructor(status, problem, extras) {
|
|
178
|
-
const
|
|
179
|
+
const code = problem?.code ?? "internal";
|
|
180
|
+
const message = problem ? `${status} ${problem.title ?? "Error"}: ${problem.detail ?? code}` : `${status} (no problem body)`;
|
|
179
181
|
super(message);
|
|
180
182
|
this.name = "ZenRowsBatchError";
|
|
181
183
|
this.status = status;
|
|
182
|
-
this.problem = problem;
|
|
183
|
-
this.code =
|
|
184
|
+
this.problem = problem && { ...problem, code };
|
|
185
|
+
this.code = code;
|
|
184
186
|
this.extras = extras;
|
|
185
187
|
}
|
|
186
188
|
};
|
|
@@ -206,7 +208,7 @@ async function parseProblem(response) {
|
|
|
206
208
|
type: typeof record.type === "string" ? record.type : "about:blank",
|
|
207
209
|
title: typeof record.title === "string" ? record.title : "Error",
|
|
208
210
|
status: typeof record.status === "number" ? record.status : response.status,
|
|
209
|
-
code: typeof record.code === "string" ? record.code :
|
|
211
|
+
code: typeof record.code === "string" ? record.code : void 0,
|
|
210
212
|
detail: typeof record.detail === "string" ? record.detail : void 0,
|
|
211
213
|
instance: typeof record.instance === "string" ? record.instance : void 0
|
|
212
214
|
};
|
|
@@ -629,6 +631,7 @@ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE", "HEAD"
|
|
|
629
631
|
var BACKOFF_BASE_MS = 250;
|
|
630
632
|
var BACKOFF_CAP_MS = 1e4;
|
|
631
633
|
var DEFAULT_RETRIES = 3;
|
|
634
|
+
var RETRY_AFTER_CAP_MS = 3e4;
|
|
632
635
|
function isIdempotent(method, hasIdempotencyKey) {
|
|
633
636
|
return IDEMPOTENT_METHODS.has(method.toUpperCase()) || method.toUpperCase() === "POST" && hasIdempotencyKey;
|
|
634
637
|
}
|
|
@@ -641,41 +644,71 @@ function retryAfterMs(response) {
|
|
|
641
644
|
if (!raw) return void 0;
|
|
642
645
|
const secs = Number(raw);
|
|
643
646
|
if (Number.isNaN(secs) || secs < 0) return void 0;
|
|
644
|
-
return secs * 1e3;
|
|
647
|
+
return Math.min(secs * 1e3, RETRY_AFTER_CAP_MS);
|
|
645
648
|
}
|
|
646
649
|
function sleep(ms) {
|
|
647
650
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
648
651
|
}
|
|
652
|
+
var batchError = (response, problem, extras) => new ZenRowsBatchError(response.status, problem, extras);
|
|
653
|
+
function deadline(ms) {
|
|
654
|
+
if (ms === void 0) return { clear() {
|
|
655
|
+
} };
|
|
656
|
+
const controller = new AbortController();
|
|
657
|
+
const timer = setTimeout(
|
|
658
|
+
() => controller.abort(new DOMException(`request timed out after ${ms / 1e3}s`, "TimeoutError")),
|
|
659
|
+
ms
|
|
660
|
+
);
|
|
661
|
+
return { signal: controller.signal, clear: () => clearTimeout(timer) };
|
|
662
|
+
}
|
|
649
663
|
var BatchTransport = class {
|
|
650
|
-
constructor(baseURL, apiKey, retries = DEFAULT_RETRIES) {
|
|
664
|
+
constructor(baseURL, apiKey, retries = DEFAULT_RETRIES, errorFactory = batchError, timeoutMs) {
|
|
651
665
|
this.baseURL = baseURL;
|
|
652
666
|
this.apiKey = apiKey;
|
|
653
667
|
this.retries = retries;
|
|
668
|
+
this.errorFactory = errorFactory;
|
|
669
|
+
this.timeoutMs = timeoutMs;
|
|
654
670
|
}
|
|
655
671
|
baseURL;
|
|
656
672
|
apiKey;
|
|
657
673
|
retries;
|
|
658
|
-
|
|
674
|
+
errorFactory;
|
|
675
|
+
timeoutMs;
|
|
676
|
+
/** Send a request, parse the response, throw the factory's error on non-2xx. */
|
|
659
677
|
async requestJson(method, path, options = {}) {
|
|
660
|
-
const
|
|
661
|
-
if (!response.ok) {
|
|
662
|
-
const { problem, extras } = await parseProblem(response);
|
|
663
|
-
throw new ZenRowsBatchError(response.status, problem, extras);
|
|
664
|
-
}
|
|
665
|
-
const text = await response.text();
|
|
678
|
+
const text = await this.requestText(method, path, options);
|
|
666
679
|
if (!text) {
|
|
667
680
|
return void 0;
|
|
668
681
|
}
|
|
669
682
|
return JSON.parse(text);
|
|
670
683
|
}
|
|
671
|
-
/** Send a request and return
|
|
684
|
+
/** Send a request and return its body as text; throw the factory's error on non-2xx. */
|
|
685
|
+
async requestText(method, path, options = {}) {
|
|
686
|
+
const { response, clear } = await this.send(method, path, options);
|
|
687
|
+
try {
|
|
688
|
+
await this.throwUnlessOk(response);
|
|
689
|
+
return await response.text();
|
|
690
|
+
} finally {
|
|
691
|
+
clear();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Send a request and return the raw `Response`, for bodies read as a stream. The timeout
|
|
696
|
+
* stops once the headers of a 2xx arrive; an error body is still read within it.
|
|
697
|
+
*/
|
|
672
698
|
async requestRaw(method, path, options = {}) {
|
|
673
|
-
const response = await this.send(method, path, options);
|
|
699
|
+
const { response, clear } = await this.send(method, path, options);
|
|
700
|
+
try {
|
|
701
|
+
await this.throwUnlessOk(response);
|
|
702
|
+
} finally {
|
|
703
|
+
clear();
|
|
704
|
+
}
|
|
705
|
+
return response;
|
|
706
|
+
}
|
|
707
|
+
async throwUnlessOk(response) {
|
|
674
708
|
if (!response.ok) {
|
|
675
709
|
const { problem, extras } = await parseProblem(response);
|
|
676
|
-
throw
|
|
710
|
+
throw this.errorFactory(response, problem, extras);
|
|
677
711
|
}
|
|
678
|
-
return response;
|
|
679
712
|
}
|
|
680
713
|
buildUrl(path, query) {
|
|
681
714
|
const url = new URL(`${this.baseURL}${path}`);
|
|
@@ -702,16 +735,20 @@ var BatchTransport = class {
|
|
|
702
735
|
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
703
736
|
}
|
|
704
737
|
const idempotent = isIdempotent(method, Boolean(options.idempotencyKey));
|
|
738
|
+
const retryStatuses = options.retryStatuses ?? RETRYABLE_STATUSES;
|
|
705
739
|
let attempt = 0;
|
|
706
740
|
while (true) {
|
|
741
|
+
const { signal, clear } = deadline(this.timeoutMs);
|
|
707
742
|
let response;
|
|
708
743
|
try {
|
|
709
744
|
response = await fetch(url.toString(), {
|
|
710
745
|
method,
|
|
711
746
|
headers,
|
|
712
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
747
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
748
|
+
signal
|
|
713
749
|
});
|
|
714
750
|
} catch (error) {
|
|
751
|
+
clear();
|
|
715
752
|
if (idempotent && attempt < this.retries) {
|
|
716
753
|
await sleep(backoffMs(attempt));
|
|
717
754
|
attempt += 1;
|
|
@@ -719,13 +756,16 @@ var BatchTransport = class {
|
|
|
719
756
|
}
|
|
720
757
|
throw error;
|
|
721
758
|
}
|
|
722
|
-
if (idempotent && attempt < this.retries &&
|
|
759
|
+
if (idempotent && attempt < this.retries && retryStatuses.has(response.status)) {
|
|
760
|
+
response.body?.cancel().catch(() => {
|
|
761
|
+
});
|
|
762
|
+
clear();
|
|
723
763
|
const wait = retryAfterMs(response) ?? backoffMs(attempt);
|
|
724
764
|
await sleep(wait);
|
|
725
765
|
attempt += 1;
|
|
726
766
|
continue;
|
|
727
767
|
}
|
|
728
|
-
return response;
|
|
768
|
+
return { response, clear };
|
|
729
769
|
}
|
|
730
770
|
}
|
|
731
771
|
};
|
|
@@ -752,7 +792,7 @@ async function pollUntil(fetch2, {
|
|
|
752
792
|
backoff = 1.5,
|
|
753
793
|
jitter = 0.2
|
|
754
794
|
}) {
|
|
755
|
-
const
|
|
795
|
+
const deadline2 = Date.now() + timeout * 1e3;
|
|
756
796
|
let interval = initialInterval;
|
|
757
797
|
while (true) {
|
|
758
798
|
const value = await fetch2();
|
|
@@ -765,10 +805,10 @@ async function pollUntil(fetch2, {
|
|
|
765
805
|
);
|
|
766
806
|
}
|
|
767
807
|
const now = Date.now();
|
|
768
|
-
if (now >=
|
|
808
|
+
if (now >= deadline2) {
|
|
769
809
|
throw new WaiterTimeoutError(`waiter: timed out after ${timeout}s waiting for target state`);
|
|
770
810
|
}
|
|
771
|
-
const remainingSeconds = (
|
|
811
|
+
const remainingSeconds = (deadline2 - now) / 1e3;
|
|
772
812
|
const sleepSeconds = Math.min(interval, remainingSeconds) * (1 + jitter * (Math.random() * 2 - 1));
|
|
773
813
|
if (sleepSeconds > 0) {
|
|
774
814
|
await new Promise((resolve) => setTimeout(resolve, sleepSeconds * 1e3));
|
|
@@ -1166,6 +1206,220 @@ var ZenRowsBatchClient = class {
|
|
|
1166
1206
|
}
|
|
1167
1207
|
};
|
|
1168
1208
|
|
|
1209
|
+
// src/crawl/errors.ts
|
|
1210
|
+
var CRAWL_NOT_ENABLED_CODE = "REQS008";
|
|
1211
|
+
var ZenRowsCrawlError = class extends Error {
|
|
1212
|
+
status;
|
|
1213
|
+
problem;
|
|
1214
|
+
code;
|
|
1215
|
+
detail;
|
|
1216
|
+
extras;
|
|
1217
|
+
/** Seconds from the `Retry-After` header, e.g. 30 on a 429 `too_many_crawls`. */
|
|
1218
|
+
retryAfter;
|
|
1219
|
+
constructor(status, problem, extras, retryAfter) {
|
|
1220
|
+
let message;
|
|
1221
|
+
if (status === 403 && problem?.code === CRAWL_NOT_ENABLED_CODE) {
|
|
1222
|
+
message = `403 Crawl is not enabled for this account (${CRAWL_NOT_ENABLED_CODE})${problem.detail ? `: ${problem.detail}` : ""}`;
|
|
1223
|
+
} else if (problem) {
|
|
1224
|
+
message = `${status} ${problem.title ?? "Error"}: ${problem.detail ?? problem.code ?? "unknown"}`;
|
|
1225
|
+
} else {
|
|
1226
|
+
message = `${status} (no problem body)`;
|
|
1227
|
+
}
|
|
1228
|
+
super(message);
|
|
1229
|
+
this.name = "ZenRowsCrawlError";
|
|
1230
|
+
this.status = status;
|
|
1231
|
+
this.problem = problem;
|
|
1232
|
+
this.code = problem?.code;
|
|
1233
|
+
this.detail = problem?.detail;
|
|
1234
|
+
this.extras = extras;
|
|
1235
|
+
this.retryAfter = retryAfter;
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
function crawlError(response, problem, extras) {
|
|
1239
|
+
const raw = response.headers.get("Retry-After");
|
|
1240
|
+
const secs = raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
|
|
1241
|
+
return new ZenRowsCrawlError(
|
|
1242
|
+
response.status,
|
|
1243
|
+
problem,
|
|
1244
|
+
extras,
|
|
1245
|
+
Number.isNaN(secs) || secs < 0 ? void 0 : secs
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/crawl/client.ts
|
|
1250
|
+
var DEFAULT_CRAWL_API_URL = "https://api.zenrows.com/v1";
|
|
1251
|
+
var DEFAULT_TIMEOUT_SECONDS = 30;
|
|
1252
|
+
var DEFAULT_WAIT_TIMEOUT_SECONDS = 600;
|
|
1253
|
+
var POLL_INTERVAL_SECONDS = 2;
|
|
1254
|
+
var MAX_POLL_INTERVAL_SECONDS = 15;
|
|
1255
|
+
var POLL_BACKOFF = 1.5;
|
|
1256
|
+
var CREATE_RETRY_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
1257
|
+
var STOP_RETRY_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
1258
|
+
var MAX_STOP_RETRY_AFTER_SECONDS = 30;
|
|
1259
|
+
function contentIdOf(content) {
|
|
1260
|
+
let ref;
|
|
1261
|
+
if (typeof content === "string") ref = content;
|
|
1262
|
+
else if (content.content_url) ref = content.content_url;
|
|
1263
|
+
else {
|
|
1264
|
+
throw new TypeError(
|
|
1265
|
+
`content: result for ${content.url} has no content_url (content_status: ${content.content_status ?? "absent"})`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
const id = ref.split("/").pop();
|
|
1269
|
+
if (!id) throw new TypeError(`content: cannot read a content id from ${ref}`);
|
|
1270
|
+
return id;
|
|
1271
|
+
}
|
|
1272
|
+
async function* ndjsonLines(body) {
|
|
1273
|
+
if (!body) return;
|
|
1274
|
+
const decoder = new TextDecoder();
|
|
1275
|
+
let buffered = "";
|
|
1276
|
+
for await (const chunk of body) {
|
|
1277
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
1278
|
+
let newline = buffered.indexOf("\n");
|
|
1279
|
+
while (newline !== -1) {
|
|
1280
|
+
const line = buffered.slice(0, newline).trim();
|
|
1281
|
+
buffered = buffered.slice(newline + 1);
|
|
1282
|
+
if (line) yield JSON.parse(line);
|
|
1283
|
+
newline = buffered.indexOf("\n");
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const last = (buffered + decoder.decode()).trim();
|
|
1287
|
+
if (last) yield JSON.parse(last);
|
|
1288
|
+
}
|
|
1289
|
+
var ZenRowsCrawlClient = class {
|
|
1290
|
+
apiKey;
|
|
1291
|
+
transport;
|
|
1292
|
+
retries;
|
|
1293
|
+
constructor(apiKey, config = {}) {
|
|
1294
|
+
this.apiKey = apiKey;
|
|
1295
|
+
this.retries = config.retries ?? DEFAULT_RETRIES;
|
|
1296
|
+
this.transport = new BatchTransport(
|
|
1297
|
+
(config.baseURL ?? DEFAULT_CRAWL_API_URL).replace(/\/+$/, ""),
|
|
1298
|
+
apiKey,
|
|
1299
|
+
config.retries,
|
|
1300
|
+
crawlError,
|
|
1301
|
+
(config.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Start a crawl. Sends only the fields you set. With an `idempotencyKey`, a retry returns the
|
|
1306
|
+
* crawl the first request created instead of starting another, and 5xx and network failures
|
|
1307
|
+
* are retried. A 429 `too_many_crawls` is never retried: it throws `ZenRowsCrawlError` with
|
|
1308
|
+
* `retryAfter` set.
|
|
1309
|
+
*/
|
|
1310
|
+
create(params, opts = {}) {
|
|
1311
|
+
const body = {
|
|
1312
|
+
url: params.url,
|
|
1313
|
+
depth: params.depth,
|
|
1314
|
+
max_items: params.maxItems,
|
|
1315
|
+
max_pages: params.maxPages,
|
|
1316
|
+
include_patterns: params.includePatterns,
|
|
1317
|
+
exclude_patterns: params.excludePatterns,
|
|
1318
|
+
output_format: params.outputFormat
|
|
1319
|
+
};
|
|
1320
|
+
return this.transport.requestJson("POST", "/crawls", {
|
|
1321
|
+
body,
|
|
1322
|
+
idempotencyKey: opts.idempotencyKey,
|
|
1323
|
+
retryStatuses: CREATE_RETRY_STATUSES
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
/** Read a crawl's status, coverage and one page of results (`limit` 1–10000, default 1000). */
|
|
1327
|
+
get(crawlId, options = {}) {
|
|
1328
|
+
return this.transport.requestJson("GET", `/crawls/${encodeURIComponent(crawlId)}`, {
|
|
1329
|
+
query: options
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
/** One page of the account's crawls, newest first (`limit` 1–100, default 20). */
|
|
1333
|
+
list(options = {}) {
|
|
1334
|
+
return this.transport.requestJson("GET", "/crawls", { query: options });
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Every URL the crawl kept, following `next_cursor` (`limit` is the page size). On a running
|
|
1338
|
+
* crawl it yields the URLs kept so far and returns at the first empty page; it does not poll.
|
|
1339
|
+
* Call `wait()` first to read every result.
|
|
1340
|
+
*/
|
|
1341
|
+
async *results(crawlId, options = {}) {
|
|
1342
|
+
let cursor;
|
|
1343
|
+
while (true) {
|
|
1344
|
+
const page = await this.get(crawlId, { ...options, cursor });
|
|
1345
|
+
yield* page.results;
|
|
1346
|
+
if (page.next_cursor === null || page.results.length === 0) return;
|
|
1347
|
+
cursor = page.next_cursor;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Stop a running crawl. Idempotent: a crawl that already ended answers as it ended.
|
|
1352
|
+
* Because a stop is safe to repeat, `stop()` retries a 429, 502, 503 (including `crawl_busy`,
|
|
1353
|
+
* which means the stop was not saved and the crawl is still running) or 504 up to `retries`
|
|
1354
|
+
* times, waiting `Retry-After` seconds (at most 30; 1 when the header is missing or unusable)
|
|
1355
|
+
* each time, then throws the last error.
|
|
1356
|
+
*/
|
|
1357
|
+
async stop(crawlId) {
|
|
1358
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1359
|
+
try {
|
|
1360
|
+
return await this.transport.requestJson(
|
|
1361
|
+
"POST",
|
|
1362
|
+
`/crawls/${encodeURIComponent(crawlId)}/stop`
|
|
1363
|
+
);
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (!(error instanceof ZenRowsCrawlError) || !STOP_RETRY_STATUSES.has(error.status) || attempt >= this.retries) {
|
|
1366
|
+
throw error;
|
|
1367
|
+
}
|
|
1368
|
+
const waitSeconds = Math.min(error.retryAfter ?? 1, MAX_STOP_RETRY_AFTER_SECONDS);
|
|
1369
|
+
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1e3));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* One kept URL's page as fetched (HTML). Pass the content id, a `content_url`, or the
|
|
1375
|
+
* `CrawlResult` itself (its `content_url` is present once `content_status` is `fetched`).
|
|
1376
|
+
* Throws `TypeError` when no content id can be read.
|
|
1377
|
+
*/
|
|
1378
|
+
async content(crawlId, contentId) {
|
|
1379
|
+
return this.transport.requestText(
|
|
1380
|
+
"GET",
|
|
1381
|
+
`/crawls/${encodeURIComponent(crawlId)}/contents/${encodeURIComponent(contentIdOf(contentId))}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Every result of the crawl in one download: its `status` and the parsed lines (with the
|
|
1386
|
+
* page when the crawl has an `output_format`). On a running crawl, `status` is `running` and
|
|
1387
|
+
* the lines hold only what was kept so far.
|
|
1388
|
+
*/
|
|
1389
|
+
async download(crawlId) {
|
|
1390
|
+
const response = await this.transport.requestRaw(
|
|
1391
|
+
"GET",
|
|
1392
|
+
`/crawls/${encodeURIComponent(crawlId)}/download`
|
|
1393
|
+
);
|
|
1394
|
+
return {
|
|
1395
|
+
status: response.headers.get("X-Crawl-Status") ?? void 0,
|
|
1396
|
+
lines: ndjsonLines(response.body)
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Poll until the crawl ends (any status but `running`) or `timeout` seconds pass, and return
|
|
1401
|
+
* it, without results. A `failed` crawl resolves too: read `error`. On timeout the crawl is
|
|
1402
|
+
* returned with status `running` and keeps running.
|
|
1403
|
+
*/
|
|
1404
|
+
async wait(crawlId, options = {}) {
|
|
1405
|
+
const deadline2 = Date.now() + (options.timeout ?? DEFAULT_WAIT_TIMEOUT_SECONDS) * 1e3;
|
|
1406
|
+
let interval = POLL_INTERVAL_SECONDS;
|
|
1407
|
+
let latest;
|
|
1408
|
+
while (true) {
|
|
1409
|
+
latest = await this.get(crawlId, { limit: 1 });
|
|
1410
|
+
const remaining = deadline2 - Date.now();
|
|
1411
|
+
if (latest.status !== "running" || remaining <= 0) break;
|
|
1412
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
1413
|
+
await new Promise(
|
|
1414
|
+
(resolve) => setTimeout(resolve, Math.min(interval * 1e3 * jitter, remaining))
|
|
1415
|
+
);
|
|
1416
|
+
interval = Math.min(interval * POLL_BACKOFF, MAX_POLL_INTERVAL_SECONDS);
|
|
1417
|
+
}
|
|
1418
|
+
const { results: _results, next_cursor: _cursor, ...crawl } = latest;
|
|
1419
|
+
return crawl;
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1169
1423
|
// src/batch/schedule.ts
|
|
1170
1424
|
var DAYS_OF_WEEK = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
1171
1425
|
function validateTimezone(tz, field) {
|
|
@@ -1335,10 +1589,13 @@ var ZenRows = class {
|
|
|
1335
1589
|
fetchWithRetry;
|
|
1336
1590
|
/** Client for the Batch API (async job/run/task model) — see `./batch.ts`. */
|
|
1337
1591
|
batch;
|
|
1592
|
+
/** Client for the Crawl API (new; one start URL in, the URLs behind it out). See `./crawl/client.ts`. */
|
|
1593
|
+
crawl;
|
|
1338
1594
|
constructor(apiKey, clientConfig = {}) {
|
|
1339
1595
|
this.apiKey = apiKey;
|
|
1340
1596
|
this.clientConfig = clientConfig;
|
|
1341
1597
|
this.batch = new ZenRowsBatchClient(apiKey);
|
|
1598
|
+
this.crawl = new ZenRowsCrawlClient(apiKey, clientConfig.crawl);
|
|
1342
1599
|
const retries = this.clientConfig.retries ?? 0;
|
|
1343
1600
|
this.queue = fastq.promise(this, this.worker, this.clientConfig.concurrency ?? 5);
|
|
1344
1601
|
this.fetchWithRetry = fetchRetry(fetch, {
|
|
@@ -1469,6 +1726,7 @@ export {
|
|
|
1469
1726
|
AUTO_MIN_CREDITS,
|
|
1470
1727
|
At,
|
|
1471
1728
|
BASE_CREDITS,
|
|
1729
|
+
CRAWL_NOT_ENABLED_CODE,
|
|
1472
1730
|
Calendar,
|
|
1473
1731
|
CurrentRun,
|
|
1474
1732
|
DEFAULT_MAX_BYTES_PER_FILE,
|
|
@@ -1493,6 +1751,8 @@ export {
|
|
|
1493
1751
|
ZenRows,
|
|
1494
1752
|
ZenRowsBatchClient,
|
|
1495
1753
|
ZenRowsBatchError,
|
|
1754
|
+
ZenRowsCrawlClient,
|
|
1755
|
+
ZenRowsCrawlError,
|
|
1496
1756
|
downloadTaskToFile,
|
|
1497
1757
|
downloadTaskToMemory,
|
|
1498
1758
|
downloadToDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zenrows",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "ZenRows Node SDK",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup src/index.ts --dts --format cjs,esm --clean",
|
|
21
21
|
"test": "vitest",
|
|
22
|
+
"test:e2e": "vitest run tests/e2e",
|
|
22
23
|
"format": "biome format --write .",
|
|
23
24
|
"check": "biome check ."
|
|
24
25
|
},
|