zenrows 2.0.2 → 2.0.3

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.cjs CHANGED
@@ -30,7 +30,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- ZenRows: () => ZenRows
33
+ AUTO_MAX_CREDITS: () => AUTO_MAX_CREDITS,
34
+ AUTO_MIN_CREDITS: () => AUTO_MIN_CREDITS,
35
+ At: () => At,
36
+ BASE_CREDITS: () => BASE_CREDITS,
37
+ Calendar: () => Calendar,
38
+ CurrentRun: () => CurrentRun,
39
+ DEFAULT_MAX_BYTES_PER_FILE: () => DEFAULT_MAX_BYTES_PER_FILE,
40
+ DEFAULT_MAX_COUNT_IN_MEMORY: () => DEFAULT_MAX_COUNT_IN_MEMORY,
41
+ DEFAULT_MAX_FILES: () => DEFAULT_MAX_FILES,
42
+ DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY: () => DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY,
43
+ Daily: () => Daily,
44
+ ExportHandle: () => ExportHandle,
45
+ ExportRef: () => ExportRef,
46
+ JS_AND_PROXY_CREDITS: () => JS_AND_PROXY_CREDITS,
47
+ JS_CREDITS: () => JS_CREDITS,
48
+ JobHandle: () => JobHandle,
49
+ JobRef: () => JobRef,
50
+ Monthly: () => Monthly,
51
+ PREMIUM_PROXY_CREDITS: () => PREMIUM_PROXY_CREDITS,
52
+ Rate: () => Rate,
53
+ RunHandle: () => RunHandle,
54
+ RunRef: () => RunRef,
55
+ WaiterFailureError: () => WaiterFailureError,
56
+ WaiterTimeoutError: () => WaiterTimeoutError,
57
+ Weekly: () => Weekly,
58
+ ZenRows: () => ZenRows,
59
+ ZenRowsBatchClient: () => ZenRowsBatchClient,
60
+ ZenRowsBatchError: () => ZenRowsBatchError,
61
+ downloadTaskToFile: () => downloadTaskToFile,
62
+ downloadTaskToMemory: () => downloadTaskToMemory,
63
+ downloadToDir: () => downloadToDir,
64
+ downloadToMemory: () => downloadToMemory,
65
+ estimateCost: () => estimateCost,
66
+ parseProblem: () => parseProblem,
67
+ pollUntil: () => pollUntil
34
68
  });
35
69
  module.exports = __toCommonJS(index_exports);
36
70
  var import_fastq = __toESM(require("fastq"), 1);
@@ -75,10 +109,11 @@ var package_default = {
75
109
  "@biomejs/biome": "1.8.1",
76
110
  "@types/async-retry": "^1.4.9",
77
111
  "@types/node": "^20.19.39",
112
+ "@vitest/coverage-v8": "1.6.1",
78
113
  msw: "^2.13.4",
79
114
  tsup: "^8.5.1",
80
115
  typescript: "^5.9.3",
81
- vitest: "^1.6.1"
116
+ vitest: "^3.2.6"
82
117
  },
83
118
  dependencies: {
84
119
  fastq: "^1.20.1",
@@ -86,16 +121,1283 @@ var package_default = {
86
121
  }
87
122
  };
88
123
 
124
+ // src/batch/download.ts
125
+ var import_node_fs = require("fs");
126
+ var import_promises = require("fs/promises");
127
+ var import_node_path = require("path");
128
+ var import_node_stream = require("stream");
129
+ var import_promises2 = require("stream/promises");
130
+ var DEFAULT_MAX_FILES = 1e5;
131
+ var DEFAULT_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;
132
+ var DEFAULT_MAX_COUNT_IN_MEMORY = 1e4;
133
+ var DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY = 500 * 1024 * 1024;
134
+ async function fetchResultBody(task, maxBytes) {
135
+ if (!task.result_url) {
136
+ throw new Error(`task ${task.task_id} has no result_url (not a successful task?)`);
137
+ }
138
+ const response = await fetch(task.result_url);
139
+ if (!response.ok) {
140
+ throw new Error(`downloading task ${task.task_id} failed with status ${response.status}`);
141
+ }
142
+ const buffer = Buffer.from(await response.arrayBuffer());
143
+ if (maxBytes !== void 0 && buffer.byteLength > maxBytes) {
144
+ throw new Error(
145
+ `task ${task.task_id}'s body (${buffer.byteLength} bytes) exceeds maxBytesPerFile (${maxBytes})`
146
+ );
147
+ }
148
+ return buffer;
149
+ }
150
+ async function runPool(items, concurrency, worker) {
151
+ let cursor = 0;
152
+ const errors = [];
153
+ async function next() {
154
+ while (cursor < items.length) {
155
+ const index = cursor++;
156
+ try {
157
+ await worker(items[index], index);
158
+ } catch (error) {
159
+ errors.push(error);
160
+ }
161
+ }
162
+ }
163
+ const workers = Array.from({ length: Math.max(1, concurrency) }, () => next());
164
+ await Promise.all(workers);
165
+ if (errors.length) {
166
+ throw errors[0];
167
+ }
168
+ }
169
+ function defaultFileName(task, useExternalId) {
170
+ const base = useExternalId && task.external_id ? task.external_id : task.task_id;
171
+ return base.replace(/[^a-zA-Z0-9._-]/g, "_");
172
+ }
173
+ async function downloadToDir(results, targetDir, {
174
+ nameFn,
175
+ useExternalId = false,
176
+ concurrency = 1,
177
+ maxFiles = DEFAULT_MAX_FILES,
178
+ maxBytesPerFile = DEFAULT_MAX_BYTES_PER_FILE
179
+ } = {}) {
180
+ await (0, import_promises.mkdir)(targetDir, { recursive: true });
181
+ const tasks = [];
182
+ for await (const task of results) {
183
+ tasks.push(task);
184
+ if (tasks.length > maxFiles) {
185
+ throw new Error(`result count exceeds maxFiles (${maxFiles})`);
186
+ }
187
+ }
188
+ await runPool(tasks, concurrency, async (task) => {
189
+ const body = await fetchResultBody(task, maxBytesPerFile);
190
+ const fileName = nameFn ? nameFn(task) : defaultFileName(task, useExternalId);
191
+ const filePath = (0, import_node_path.join)(targetDir, fileName);
192
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(filePath), { recursive: true });
193
+ await (0, import_promises2.pipeline)(import_node_stream.Readable.from(body), (0, import_node_fs.createWriteStream)(filePath));
194
+ });
195
+ return tasks.length;
196
+ }
197
+ async function downloadToMemory(results, {
198
+ concurrency = 1,
199
+ maxCount = DEFAULT_MAX_COUNT_IN_MEMORY,
200
+ maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY,
201
+ maxBytesPerFile = DEFAULT_MAX_BYTES_PER_FILE
202
+ } = {}) {
203
+ const tasks = [];
204
+ for await (const task of results) {
205
+ tasks.push(task);
206
+ if (tasks.length > maxCount) {
207
+ throw new Error(`result count exceeds maxCount (${maxCount})`);
208
+ }
209
+ }
210
+ const downloaded = new Array(tasks.length);
211
+ let totalBytes = 0;
212
+ await runPool(tasks, concurrency, async (task, index) => {
213
+ const body = await fetchResultBody(task, maxBytesPerFile);
214
+ totalBytes += body.byteLength;
215
+ if (totalBytes > maxTotalBytes) {
216
+ throw new Error(`total downloaded bytes exceeds maxTotalBytes (${maxTotalBytes})`);
217
+ }
218
+ downloaded[index] = { taskId: task.task_id, externalId: task.external_id, url: task.url, body };
219
+ });
220
+ return downloaded;
221
+ }
222
+ async function downloadTaskToFile(task, target) {
223
+ const body = await fetchResultBody(task);
224
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(target), { recursive: true });
225
+ await (0, import_promises2.pipeline)(import_node_stream.Readable.from(body), (0, import_node_fs.createWriteStream)(target));
226
+ }
227
+ async function downloadTaskToMemory(task) {
228
+ return fetchResultBody(task);
229
+ }
230
+
231
+ // src/batch/errors.ts
232
+ var ZenRowsBatchError = class extends Error {
233
+ status;
234
+ problem;
235
+ code;
236
+ extras;
237
+ constructor(status, problem, extras) {
238
+ const message = problem ? `${status} ${problem.title ?? "Error"}: ${problem.detail ?? problem.code ?? "unknown"}` : `${status} (no problem body)`;
239
+ super(message);
240
+ this.name = "ZenRowsBatchError";
241
+ this.status = status;
242
+ this.problem = problem;
243
+ this.code = problem?.code ?? "internal";
244
+ this.extras = extras;
245
+ }
246
+ };
247
+ var PROBLEM_STANDARD_KEYS = /* @__PURE__ */ new Set(["type", "title", "status", "code", "detail", "instance"]);
248
+ async function parseProblem(response) {
249
+ let body;
250
+ try {
251
+ body = await response.json();
252
+ } catch {
253
+ return { problem: void 0, extras: void 0 };
254
+ }
255
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
256
+ return { problem: void 0, extras: void 0 };
257
+ }
258
+ const record = body;
259
+ const extras = {};
260
+ for (const [key, value] of Object.entries(record)) {
261
+ if (!PROBLEM_STANDARD_KEYS.has(key)) {
262
+ extras[key] = value;
263
+ }
264
+ }
265
+ const problem = {
266
+ type: typeof record.type === "string" ? record.type : "about:blank",
267
+ title: typeof record.title === "string" ? record.title : "Error",
268
+ status: typeof record.status === "number" ? record.status : response.status,
269
+ code: typeof record.code === "string" ? record.code : "internal",
270
+ detail: typeof record.detail === "string" ? record.detail : void 0,
271
+ instance: typeof record.instance === "string" ? record.instance : void 0
272
+ };
273
+ return { problem, extras: Object.keys(extras).length ? extras : void 0 };
274
+ }
275
+
276
+ // src/batch/estimate.ts
277
+ var BASE_CREDITS = 1;
278
+ var JS_CREDITS = 5;
279
+ var PREMIUM_PROXY_CREDITS = 10;
280
+ var JS_AND_PROXY_CREDITS = 25;
281
+ var AUTO_MIN_CREDITS = 1;
282
+ var AUTO_MAX_CREDITS = 25;
283
+ var TIER_ORDER = [
284
+ "base",
285
+ "js_render",
286
+ "premium_proxy",
287
+ "js_render+premium_proxy",
288
+ "auto"
289
+ ];
290
+ var TRUTHY_STRINGS = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
291
+ function truthy(value) {
292
+ if (typeof value === "boolean") return value;
293
+ if (typeof value === "number") return value !== 0;
294
+ if (typeof value === "string") return TRUTHY_STRINGS.has(value.trim().toLowerCase());
295
+ return false;
296
+ }
297
+ function isAuto(params) {
298
+ return String(params.mode ?? "").trim().toLowerCase() === "auto";
299
+ }
300
+ function costForParams(params) {
301
+ if (isAuto(params)) {
302
+ return { tier: "auto", min: AUTO_MIN_CREDITS, max: AUTO_MAX_CREDITS };
303
+ }
304
+ const js = truthy(params.js_render);
305
+ const px = truthy(params.premium_proxy);
306
+ if (js && px) {
307
+ return {
308
+ tier: "js_render+premium_proxy",
309
+ min: JS_AND_PROXY_CREDITS,
310
+ max: JS_AND_PROXY_CREDITS
311
+ };
312
+ }
313
+ if (px) {
314
+ return { tier: "premium_proxy", min: PREMIUM_PROXY_CREDITS, max: PREMIUM_PROXY_CREDITS };
315
+ }
316
+ if (js) {
317
+ return { tier: "js_render", min: JS_CREDITS, max: JS_CREDITS };
318
+ }
319
+ return { tier: "base", min: BASE_CREDITS, max: BASE_CREDITS };
320
+ }
321
+ function taskParams(task) {
322
+ if (typeof task === "string") return {};
323
+ const record = task;
324
+ return record.zenrows_params ?? {};
325
+ }
326
+ function estimateCost(tasks, zenrowsParams) {
327
+ const jobParams = zenrowsParams ?? {};
328
+ const agg = /* @__PURE__ */ new Map();
329
+ let totalMin = 0;
330
+ let totalMax = 0;
331
+ let count = 0;
332
+ for (const task of tasks) {
333
+ count += 1;
334
+ const merged = { ...jobParams, ...taskParams(task) };
335
+ const cost = costForParams(merged);
336
+ totalMin += cost.min;
337
+ totalMax += cost.max;
338
+ const existing = agg.get(cost.tier);
339
+ if (existing) {
340
+ existing.count += 1;
341
+ } else {
342
+ agg.set(cost.tier, { count: 1, min: cost.min, max: cost.max });
343
+ }
344
+ }
345
+ const breakdown = TIER_ORDER.filter((tier) => agg.has(tier)).map((tier) => {
346
+ const entry = agg.get(tier);
347
+ if (!entry) throw new Error("unreachable");
348
+ return { tier, count: entry.count, unitMin: entry.min, unitMax: entry.max };
349
+ });
350
+ return {
351
+ taskCount: count,
352
+ min: totalMin,
353
+ max: totalMax,
354
+ breakdown,
355
+ exact: totalMin === totalMax
356
+ };
357
+ }
358
+
359
+ // src/batch/resources.ts
360
+ var RunRef = class {
361
+ constructor(client, jobId, runId) {
362
+ this.jobId = jobId;
363
+ this.runId = runId;
364
+ this.#client = client;
365
+ }
366
+ jobId;
367
+ runId;
368
+ #client;
369
+ async load() {
370
+ return new RunHandle(
371
+ this.#client,
372
+ this.jobId,
373
+ this.runId,
374
+ await this.#client._getRunData(this.jobId, this.runId)
375
+ );
376
+ }
377
+ async delete() {
378
+ await this.#client._deleteRun(this.jobId, this.runId);
379
+ }
380
+ results(options = {}) {
381
+ return this.#client.iterResults(this.jobId, { runId: this.runId, status: options.status });
382
+ }
383
+ taskHistory(taskId) {
384
+ return this.#client.getTaskHistory(this.jobId, taskId, { runId: this.runId });
385
+ }
386
+ downloadToDir(targetDir, options = {}) {
387
+ return this.#client.downloadToDir(this.jobId, this.runId, targetDir, {
388
+ status: "successful",
389
+ ...options
390
+ });
391
+ }
392
+ downloadToMemory(options = {}) {
393
+ return this.#client.downloadToMemory(this.jobId, this.runId, {
394
+ status: "successful",
395
+ ...options
396
+ });
397
+ }
398
+ downloadTaskToFile(task, target) {
399
+ return this.#client.downloadTaskToFile(task, target);
400
+ }
401
+ downloadTaskToMemory(task) {
402
+ return this.#client.downloadTaskToMemory(task);
403
+ }
404
+ async wait(options = {}) {
405
+ const run = await this.#client.waitForRun(this.jobId, { ...options, runId: this.runId });
406
+ return new RunHandle(this.#client, this.jobId, this.runId, run);
407
+ }
408
+ startExport() {
409
+ return this.#client.startResultsExport(this.jobId, this.runId);
410
+ }
411
+ export(exportId) {
412
+ return new ExportRef(this.#client, this.jobId, this.runId, exportId);
413
+ }
414
+ downloadAllResults(targetPath, options = {}) {
415
+ return this.#client.downloadAllResults(this.jobId, this.runId, targetPath, options);
416
+ }
417
+ };
418
+ var RunHandle = class extends RunRef {
419
+ constructor(client, jobId, runId, data) {
420
+ super(client, jobId, runId);
421
+ this.data = data;
422
+ }
423
+ data;
424
+ get status() {
425
+ return this.data.status;
426
+ }
427
+ get stats() {
428
+ return this.data.stats;
429
+ }
430
+ };
431
+ var ExportRef = class {
432
+ constructor(client, jobId, runId, exportId, startResponse) {
433
+ this.jobId = jobId;
434
+ this.runId = runId;
435
+ this.exportId = exportId;
436
+ this.startResponse = startResponse;
437
+ this.#client = client;
438
+ }
439
+ jobId;
440
+ runId;
441
+ exportId;
442
+ startResponse;
443
+ #client;
444
+ async load() {
445
+ const data = await this.#client._getExport(this.jobId, this.runId, this.exportId);
446
+ return new ExportHandle(
447
+ this.#client,
448
+ this.jobId,
449
+ this.runId,
450
+ this.exportId,
451
+ data,
452
+ this.startResponse
453
+ );
454
+ }
455
+ async wait(options = {}) {
456
+ const data = await this.#client.waitForExport(this.jobId, this.runId, this.exportId, options);
457
+ return new ExportHandle(
458
+ this.#client,
459
+ this.jobId,
460
+ this.runId,
461
+ this.exportId,
462
+ data,
463
+ this.startResponse
464
+ );
465
+ }
466
+ /** Stream the export zip to `targetPath`. The export must already be `completed`. */
467
+ async downloadToPath(targetPath) {
468
+ const handle = await this.load();
469
+ if (handle.data.status !== "completed" || !handle.data.download_url) {
470
+ throw new Error(handle.data.error ?? "export not completed");
471
+ }
472
+ const { mkdir: mkdir2 } = await import("fs/promises");
473
+ const { dirname: dirname2 } = await import("path");
474
+ const { createWriteStream: createWriteStream2 } = await import("fs");
475
+ const { Readable: Readable2 } = await import("stream");
476
+ const { pipeline: pipeline2 } = await import("stream/promises");
477
+ await mkdir2(dirname2(targetPath), { recursive: true });
478
+ const response = await fetch(handle.data.download_url);
479
+ if (!response.ok) {
480
+ throw new Error(`export download failed with status ${response.status}`);
481
+ }
482
+ await pipeline2(Readable2.fromWeb(response.body), createWriteStream2(targetPath));
483
+ return targetPath;
484
+ }
485
+ };
486
+ var ExportHandle = class extends ExportRef {
487
+ constructor(client, jobId, runId, exportId, data, startResponse) {
488
+ super(client, jobId, runId, exportId, startResponse);
489
+ this.data = data;
490
+ }
491
+ data;
492
+ get status() {
493
+ return this.data.status;
494
+ }
495
+ };
496
+ var CurrentRun = class {
497
+ constructor(client, jobId) {
498
+ this.jobId = jobId;
499
+ this.#client = client;
500
+ }
501
+ jobId;
502
+ #client;
503
+ async currentRunId() {
504
+ const job = await this.#client._getJobData(this.jobId);
505
+ if (!job.latest_run) {
506
+ throw new Error(`job ${this.jobId} has no run yet`);
507
+ }
508
+ return job.latest_run.run_id;
509
+ }
510
+ async load() {
511
+ const job = await this.#client._getJobData(this.jobId);
512
+ if (!job.latest_run) {
513
+ throw new Error(`job ${this.jobId} has no run yet`);
514
+ }
515
+ return new RunHandle(this.#client, this.jobId, job.latest_run.run_id, job.latest_run);
516
+ }
517
+ async pause() {
518
+ const run = await this.#client._postPause(this.jobId);
519
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
520
+ }
521
+ async resume() {
522
+ const run = await this.#client._postResume(this.jobId);
523
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
524
+ }
525
+ async stop() {
526
+ const run = await this.#client._postStop(this.jobId);
527
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
528
+ }
529
+ cancel() {
530
+ return this.stop();
531
+ }
532
+ async wait(options = {}) {
533
+ const run = await this.#client.waitForRun(this.jobId, options);
534
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
535
+ }
536
+ results(options = {}) {
537
+ return this.#client.iterResults(this.jobId, { status: options.status });
538
+ }
539
+ taskHistory(taskId) {
540
+ return this.#client.getTaskHistory(this.jobId, taskId);
541
+ }
542
+ downloadToDir(targetDir, options = {}) {
543
+ return this.#client.downloadToDir(this.jobId, void 0, targetDir, {
544
+ status: "successful",
545
+ ...options
546
+ });
547
+ }
548
+ downloadToMemory(options = {}) {
549
+ return this.#client.downloadToMemory(this.jobId, void 0, {
550
+ status: "successful",
551
+ ...options
552
+ });
553
+ }
554
+ downloadTaskToFile(task, target) {
555
+ return this.#client.downloadTaskToFile(task, target);
556
+ }
557
+ downloadTaskToMemory(task) {
558
+ return this.#client.downloadTaskToMemory(task);
559
+ }
560
+ async startExport() {
561
+ return this.#client.startResultsExport(this.jobId, await this.currentRunId());
562
+ }
563
+ async downloadAllResults(targetPath, options = {}) {
564
+ return this.#client.downloadAllResults(
565
+ this.jobId,
566
+ await this.currentRunId(),
567
+ targetPath,
568
+ options
569
+ );
570
+ }
571
+ };
572
+ var ScheduleControls = class {
573
+ constructor(client, jobId) {
574
+ this.jobId = jobId;
575
+ this.#client = client;
576
+ }
577
+ jobId;
578
+ #client;
579
+ async pause() {
580
+ return new JobHandle(
581
+ this.#client,
582
+ this.jobId,
583
+ await this.#client._postScheduleState(this.jobId, "paused")
584
+ );
585
+ }
586
+ async resume() {
587
+ return new JobHandle(
588
+ this.#client,
589
+ this.jobId,
590
+ await this.#client._postScheduleState(this.jobId, "active")
591
+ );
592
+ }
593
+ async update(schedule) {
594
+ return new JobHandle(
595
+ this.#client,
596
+ this.jobId,
597
+ await this.#client._putSchedule(this.jobId, schedule.toRequestBody())
598
+ );
599
+ }
600
+ };
601
+ var JobRef = class {
602
+ constructor(client, jobId, submitResponse) {
603
+ this.jobId = jobId;
604
+ this.submitResponse = submitResponse;
605
+ this.#client = client;
606
+ }
607
+ jobId;
608
+ submitResponse;
609
+ #client;
610
+ runFacet;
611
+ scheduleFacet;
612
+ /** Operations on the CURRENT run — pause/resume/stop/wait/results/downloads/startExport. */
613
+ get run() {
614
+ if (!this.runFacet) this.runFacet = new CurrentRun(this.#client, this.jobId);
615
+ return this.runFacet;
616
+ }
617
+ /** Operations on the schedule — pause/resume/update (scheduled jobs only). */
618
+ get schedule() {
619
+ if (!this.scheduleFacet) this.scheduleFacet = new ScheduleControls(this.#client, this.jobId);
620
+ return this.scheduleFacet;
621
+ }
622
+ /** The job status from the submit response — only known on refs from a `submit*` call. */
623
+ get status() {
624
+ return this.submitResponse?.status;
625
+ }
626
+ get acceptedTasks() {
627
+ return this.submitResponse?.accepted_tasks;
628
+ }
629
+ async load() {
630
+ return new JobHandle(this.#client, this.jobId, await this.#client._getJobData(this.jobId));
631
+ }
632
+ async close() {
633
+ return new JobHandle(this.#client, this.jobId, await this.#client._postClose(this.jobId));
634
+ }
635
+ async delete() {
636
+ await this.#client._delete(this.jobId);
637
+ }
638
+ async rerun(options = {}) {
639
+ const resp = await this.#client._postRerun(this.jobId, options);
640
+ return new RunHandle(this.#client, this.jobId, resp.latest_run.run_id, resp.latest_run);
641
+ }
642
+ /** Shortcut for `rerun({ status: "failed" })` (or `"failed,pending"` with `includePending`). */
643
+ retryFailed(options = {}) {
644
+ const status = options.includePending ? ["failed", "pending"] : "failed";
645
+ return this.rerun({ status, idempotencyKey: options.idempotencyKey });
646
+ }
647
+ addTasks(tasks, options = {}) {
648
+ return this.#client._postTasks(this.jobId, tasks, options);
649
+ }
650
+ async *runs(pageSize) {
651
+ yield* this.#client.iterRuns(this.jobId, pageSize);
652
+ }
653
+ addFileInput(data, options) {
654
+ return this.#client.uploadCsv(data, options);
655
+ }
656
+ getWebhook() {
657
+ return this.#client.getJobWebhook(this.jobId);
658
+ }
659
+ setWebhook(url, signature) {
660
+ return this.#client.putJobWebhook(this.jobId, { url, signature });
661
+ }
662
+ async deleteWebhook() {
663
+ await this.#client.deleteJobWebhook(this.jobId);
664
+ }
665
+ /** Block until the current run's async-carrier ingestion has finished writing task rows. */
666
+ async waitForIngest(options = {}) {
667
+ return new JobHandle(
668
+ this.#client,
669
+ this.jobId,
670
+ await this.#client._waitForIngestRaw(this.jobId, options),
671
+ this.submitResponse
672
+ );
673
+ }
674
+ };
675
+ var JobHandle = class extends JobRef {
676
+ constructor(client, jobId, data, submitResponse) {
677
+ super(client, jobId, submitResponse);
678
+ this.data = data;
679
+ }
680
+ data;
681
+ get status() {
682
+ return this.data.status;
683
+ }
684
+ };
685
+
686
+ // src/batch/transport.ts
687
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
688
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]);
689
+ var BACKOFF_BASE_MS = 250;
690
+ var BACKOFF_CAP_MS = 1e4;
691
+ var DEFAULT_RETRIES = 3;
692
+ function isIdempotent(method, hasIdempotencyKey) {
693
+ return IDEMPOTENT_METHODS.has(method.toUpperCase()) || method.toUpperCase() === "POST" && hasIdempotencyKey;
694
+ }
695
+ function backoffMs(attempt) {
696
+ const base = Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_CAP_MS);
697
+ return base * (1 + (Math.random() * 2 - 1) * 0.2);
698
+ }
699
+ function retryAfterMs(response) {
700
+ const raw = response.headers.get("Retry-After");
701
+ if (!raw) return void 0;
702
+ const secs = Number(raw);
703
+ if (Number.isNaN(secs) || secs < 0) return void 0;
704
+ return secs * 1e3;
705
+ }
706
+ function sleep(ms) {
707
+ return new Promise((resolve) => setTimeout(resolve, ms));
708
+ }
709
+ var BatchTransport = class {
710
+ constructor(baseURL, apiKey, retries = DEFAULT_RETRIES) {
711
+ this.baseURL = baseURL;
712
+ this.apiKey = apiKey;
713
+ this.retries = retries;
714
+ }
715
+ baseURL;
716
+ apiKey;
717
+ retries;
718
+ /** Send a request, parse the response, throw `ZenRowsBatchError` on non-2xx. */
719
+ async requestJson(method, path, options = {}) {
720
+ const response = await this.send(method, path, options);
721
+ if (!response.ok) {
722
+ const { problem, extras } = await parseProblem(response);
723
+ throw new ZenRowsBatchError(response.status, problem, extras);
724
+ }
725
+ const text = await response.text();
726
+ if (!text) {
727
+ return void 0;
728
+ }
729
+ return JSON.parse(text);
730
+ }
731
+ /** Send a request and return the raw `Response` — used for endpoints that don't return JSON. */
732
+ async requestRaw(method, path, options = {}) {
733
+ const response = await this.send(method, path, options);
734
+ if (!response.ok) {
735
+ const { problem, extras } = await parseProblem(response);
736
+ throw new ZenRowsBatchError(response.status, problem, extras);
737
+ }
738
+ return response;
739
+ }
740
+ buildUrl(path, query) {
741
+ const url = new URL(`${this.baseURL}${path}`);
742
+ if (query) {
743
+ for (const [key, value] of Object.entries(query)) {
744
+ if (value !== void 0) {
745
+ url.searchParams.append(key, String(value));
746
+ }
747
+ }
748
+ }
749
+ return url;
750
+ }
751
+ async send(method, path, options) {
752
+ const url = this.buildUrl(path, options.query);
753
+ const headers = {
754
+ "X-API-Key": this.apiKey,
755
+ "User-Agent": `zenrows/${package_default.version} node`,
756
+ ...options.headers
757
+ };
758
+ if (options.body !== void 0) {
759
+ headers["Content-Type"] = "application/json";
760
+ }
761
+ if (options.idempotencyKey) {
762
+ headers["Idempotency-Key"] = options.idempotencyKey;
763
+ }
764
+ const idempotent = isIdempotent(method, Boolean(options.idempotencyKey));
765
+ let attempt = 0;
766
+ while (true) {
767
+ let response;
768
+ try {
769
+ response = await fetch(url.toString(), {
770
+ method,
771
+ headers,
772
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
773
+ });
774
+ } catch (error) {
775
+ if (idempotent && attempt < this.retries) {
776
+ await sleep(backoffMs(attempt));
777
+ attempt += 1;
778
+ continue;
779
+ }
780
+ throw error;
781
+ }
782
+ if (idempotent && attempt < this.retries && RETRYABLE_STATUSES.has(response.status)) {
783
+ const wait = retryAfterMs(response) ?? backoffMs(attempt);
784
+ await sleep(wait);
785
+ attempt += 1;
786
+ continue;
787
+ }
788
+ return response;
789
+ }
790
+ }
791
+ };
792
+
793
+ // src/batch/waiters.ts
794
+ var WaiterTimeoutError = class extends Error {
795
+ constructor(message) {
796
+ super(message);
797
+ this.name = "WaiterTimeoutError";
798
+ }
799
+ };
800
+ var WaiterFailureError = class extends Error {
801
+ constructor(message) {
802
+ super(message);
803
+ this.name = "WaiterFailureError";
804
+ }
805
+ };
806
+ async function pollUntil(fetch2, {
807
+ isDone,
808
+ isFailure,
809
+ timeout = 300,
810
+ initialInterval = 1,
811
+ maxInterval = 15,
812
+ backoff = 1.5,
813
+ jitter = 0.2
814
+ }) {
815
+ const deadline = Date.now() + timeout * 1e3;
816
+ let interval = initialInterval;
817
+ while (true) {
818
+ const value = await fetch2();
819
+ if (isDone(value)) {
820
+ return value;
821
+ }
822
+ if (isFailure?.(value)) {
823
+ throw new WaiterFailureError(
824
+ `waiter: resource entered failure state (${JSON.stringify(value)})`
825
+ );
826
+ }
827
+ const now = Date.now();
828
+ if (now >= deadline) {
829
+ throw new WaiterTimeoutError(`waiter: timed out after ${timeout}s waiting for target state`);
830
+ }
831
+ const remainingSeconds = (deadline - now) / 1e3;
832
+ const sleepSeconds = Math.min(interval, remainingSeconds) * (1 + jitter * (Math.random() * 2 - 1));
833
+ if (sleepSeconds > 0) {
834
+ await new Promise((resolve) => setTimeout(resolve, sleepSeconds * 1e3));
835
+ }
836
+ interval = Math.min(interval * backoff, maxInterval);
837
+ }
838
+ }
839
+
840
+ // src/batch/client.ts
841
+ var DEFAULT_BATCH_API_URL = "https://async.api.zenrows.com/v1";
842
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "stopped", "deleted"]);
843
+ var TERMINAL_EXPORT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed"]);
844
+ function coerceUrl(item) {
845
+ return typeof item === "string" ? { url: item } : item;
846
+ }
847
+ var ZenRowsBatchClient = class {
848
+ apiKey;
849
+ transport;
850
+ constructor(apiKey, config = {}) {
851
+ this.apiKey = apiKey;
852
+ this.transport = new BatchTransport(
853
+ config.baseURL ?? DEFAULT_BATCH_API_URL,
854
+ apiKey,
855
+ config.retries
856
+ );
857
+ }
858
+ // ===== submit =====
859
+ async submitJob(options = {}) {
860
+ const { idempotencyKey, waitForIngest, ...body } = options;
861
+ const resp = await this.transport.requestJson("POST", "/jobs", { body, idempotencyKey });
862
+ const ref = new JobRef(this, resp.job_id, resp);
863
+ if (waitForIngest && resp.latest_run && resp.latest_run.ingest_status === "pending") {
864
+ return ref.waitForIngest();
865
+ }
866
+ return ref;
867
+ }
868
+ /** Submit a one-shot scraping job (closed, all tasks known upfront). */
869
+ async submitRegular(urls, fileInputId, opts = {}) {
870
+ if (urls !== void 0 && fileInputId !== void 0) {
871
+ throw new Error("submitRegular: pass urls OR fileInputId, not both.");
872
+ }
873
+ if (urls === void 0 && fileInputId === void 0) {
874
+ throw new Error(
875
+ "submitRegular: closed jobs require urls or fileInputId (use submitOpen for the open/extend pattern)."
876
+ );
877
+ }
878
+ return this.submitJob({
879
+ type: "regular",
880
+ status: "closed",
881
+ tasks: urls?.map(coerceUrl),
882
+ file_input_id: fileInputId,
883
+ zenrows_params: opts.zenrowsParams,
884
+ external_id: opts.externalId,
885
+ name: opts.name,
886
+ metadata: opts.metadata,
887
+ webhook: opts.webhook,
888
+ idempotencyKey: opts.idempotencyKey,
889
+ waitForIngest: opts.waitForIngest
890
+ });
891
+ }
892
+ /** Submit a streaming-style job that stays open for more tasks via `JobRef.addTasks`. */
893
+ submitOpen(urls, opts = {}) {
894
+ return this.submitJob({
895
+ type: "regular",
896
+ status: "open",
897
+ tasks: urls?.map(coerceUrl),
898
+ zenrows_params: opts.zenrowsParams,
899
+ external_id: opts.externalId,
900
+ name: opts.name,
901
+ metadata: opts.metadata,
902
+ webhook: opts.webhook,
903
+ idempotencyKey: opts.idempotencyKey,
904
+ waitForIngest: opts.waitForIngest
905
+ });
906
+ }
907
+ /** Submit a scheduled job. `schedule` is one of the `At`/`Rate`/`Calendar` builders. */
908
+ async submitScheduled(schedule, urls, fileInputId, opts = {}) {
909
+ if (urls !== void 0 && fileInputId !== void 0) {
910
+ throw new Error("submitScheduled: pass urls OR fileInputId, not both.");
911
+ }
912
+ return this.submitJob({
913
+ type: "scheduled",
914
+ status: "closed",
915
+ tasks: urls?.map(coerceUrl),
916
+ file_input_id: fileInputId,
917
+ zenrows_params: opts.zenrowsParams,
918
+ external_id: opts.externalId,
919
+ name: opts.name,
920
+ metadata: opts.metadata,
921
+ webhook: opts.webhook,
922
+ idempotencyKey: opts.idempotencyKey,
923
+ // biome-ignore lint/suspicious/noExplicitAny: schedule builder output matches JobSchedule at the wire boundary
924
+ ...{ schedule: schedule.toRequestBody() }
925
+ });
926
+ }
927
+ // ===== cost estimation (local, no API call) =====
928
+ estimateCost(tasks, zenrowsParams) {
929
+ return estimateCost(tasks, zenrowsParams);
930
+ }
931
+ // ===== jobs =====
932
+ /** A `JobRef` for an existing job with no network call — prefer this to act on a known id. */
933
+ job(jobId) {
934
+ return new JobRef(this, jobId);
935
+ }
936
+ async getJob(jobId) {
937
+ return new JobHandle(this, jobId, await this._getJobData(jobId));
938
+ }
939
+ async listJobs(options = {}) {
940
+ return this.transport.requestJson("GET", "/jobs", { query: options });
941
+ }
942
+ async *iterJobs(options = {}) {
943
+ let cursor;
944
+ while (true) {
945
+ const page = await this.listJobs({ ...options, cursor });
946
+ for (const job of page.jobs) {
947
+ yield new JobHandle(this, job.job_id, job);
948
+ }
949
+ cursor = page.next_cursor;
950
+ if (!cursor) return;
951
+ }
952
+ }
953
+ // ===== runs =====
954
+ /** A `RunRef` for an existing run with no network call. */
955
+ run(jobId, runId) {
956
+ return new RunRef(this, jobId, runId);
957
+ }
958
+ async getRun(jobId, runId) {
959
+ return new RunHandle(this, jobId, runId, await this._getRunData(jobId, runId));
960
+ }
961
+ async listRuns(jobId, options = {}) {
962
+ return this.transport.requestJson("GET", `/jobs/${jobId}/runs`, { query: options });
963
+ }
964
+ async *iterRuns(jobId, pageSize) {
965
+ let cursor;
966
+ while (true) {
967
+ const page = await this.listRuns(jobId, { limit: pageSize, cursor });
968
+ for (const run of page.runs) {
969
+ yield new RunHandle(this, jobId, run.run_id, run);
970
+ }
971
+ cursor = page.next_cursor;
972
+ if (!cursor) return;
973
+ }
974
+ }
975
+ // ===== results / content =====
976
+ async getResults(jobId, options = {}) {
977
+ const { runId, ...query } = options;
978
+ const path = runId ? `/jobs/${jobId}/runs/${runId}/results` : `/jobs/${jobId}/results`;
979
+ return this.transport.requestJson("GET", path, { query });
980
+ }
981
+ async *iterResults(jobId, options = {}) {
982
+ let cursor;
983
+ while (true) {
984
+ const page = await this.getResults(jobId, { ...options, cursor });
985
+ yield* page.results;
986
+ cursor = page.next_cursor;
987
+ if (!cursor) return;
988
+ }
989
+ }
990
+ /** Returns the scraped page's raw content as-is (HTML/text, not JSON). */
991
+ async getTaskContent(jobId, taskId, options = {}) {
992
+ const path = options.runId ? `/jobs/${jobId}/runs/${options.runId}/tasks/${taskId}/content` : `/jobs/${jobId}/tasks/${taskId}/content`;
993
+ const response = await this.transport.requestRaw("GET", path);
994
+ return response.text();
995
+ }
996
+ getTaskHistory(jobId, taskId, options = {}) {
997
+ const path = options.runId ? `/jobs/${jobId}/runs/${options.runId}/tasks/${taskId}/history` : `/jobs/${jobId}/tasks/${taskId}/history`;
998
+ return this.transport.requestJson("GET", path);
999
+ }
1000
+ // ===== downloads =====
1001
+ downloadToDir(jobId, runId, targetDir, opts = {}) {
1002
+ return downloadToDir(
1003
+ this.iterResults(jobId, { runId, status: opts.status ?? "successful" }),
1004
+ targetDir,
1005
+ opts
1006
+ );
1007
+ }
1008
+ downloadToMemory(jobId, runId, opts = {}) {
1009
+ return downloadToMemory(
1010
+ this.iterResults(jobId, { runId, status: opts.status ?? "successful" }),
1011
+ opts
1012
+ );
1013
+ }
1014
+ downloadTaskToFile(task, target) {
1015
+ return downloadTaskToFile(task, target);
1016
+ }
1017
+ downloadTaskToMemory(task) {
1018
+ return downloadTaskToMemory(task);
1019
+ }
1020
+ // ===== webhooks =====
1021
+ getJobWebhook(jobId) {
1022
+ return this.transport.requestJson("GET", `/jobs/${jobId}/webhook`);
1023
+ }
1024
+ putJobWebhook(jobId, config) {
1025
+ return this.transport.requestJson("PUT", `/jobs/${jobId}/webhook`, {
1026
+ body: { signature: false, ...config }
1027
+ });
1028
+ }
1029
+ async deleteJobWebhook(jobId) {
1030
+ await this.transport.requestJson("DELETE", `/jobs/${jobId}/webhook`);
1031
+ }
1032
+ testWebhook(config) {
1033
+ return this.transport.requestJson("POST", "/webhook/test", {
1034
+ body: { signature: false, ...config }
1035
+ });
1036
+ }
1037
+ // ===== HMAC key lifecycle =====
1038
+ listHmacKeys() {
1039
+ return this.transport.requestJson("GET", "/hmac/keys");
1040
+ }
1041
+ /** Capture the returned `secret` HERE — it is not revealed again. */
1042
+ rotateHmacKey() {
1043
+ return this.transport.requestJson("POST", "/hmac/keys/rotate");
1044
+ }
1045
+ finalizeHmacKey() {
1046
+ return this.transport.requestJson("POST", "/hmac/keys/rotate/finalize");
1047
+ }
1048
+ async cancelHmacRotation() {
1049
+ await this.transport.requestJson("DELETE", "/hmac/keys/rotate");
1050
+ }
1051
+ // ===== file inputs (CSV uploads) =====
1052
+ createJobInput(body) {
1053
+ return this.transport.requestJson("POST", "/job_inputs", { body });
1054
+ }
1055
+ /** Allocate a CSV slot + PUT the body. Returns the `file_input_id` to pass to `submitJob`. */
1056
+ async uploadCsv(data, options) {
1057
+ const created = await this.createJobInput({
1058
+ type: "csv",
1059
+ csv: {
1060
+ delimiter: options.delimiter ?? ",",
1061
+ quote: options.quote ?? '"',
1062
+ header: options.header ?? false,
1063
+ fields: options.fields
1064
+ }
1065
+ });
1066
+ const headers = { "Content-Type": "text/csv", ...created.upload.headers ?? {} };
1067
+ const response = await fetch(created.upload.url, {
1068
+ method: created.upload.method,
1069
+ headers,
1070
+ body: data
1071
+ });
1072
+ if (!response.ok) {
1073
+ throw new ZenRowsBatchError(response.status, void 0);
1074
+ }
1075
+ return created.file_input_id;
1076
+ }
1077
+ // ===== results exports =====
1078
+ async startResultsExport(jobId, runId) {
1079
+ const resp = await this._postExportStart(jobId, runId);
1080
+ return new ExportRef(this, jobId, runId, resp.export_id, resp);
1081
+ }
1082
+ async getResultsExport(jobId, runId, exportId) {
1083
+ return new ExportHandle(
1084
+ this,
1085
+ jobId,
1086
+ runId,
1087
+ exportId,
1088
+ await this._getExport(jobId, runId, exportId)
1089
+ );
1090
+ }
1091
+ async waitForExport(jobId, runId, exportId, options = {}) {
1092
+ const target = options.targetStatuses ?? TERMINAL_EXPORT_STATUSES;
1093
+ return pollUntil(() => this._getExport(jobId, runId, exportId), {
1094
+ isDone: (e) => target.has(e.status),
1095
+ timeout: options.timeout ?? 600,
1096
+ initialInterval: options.pollInterval ?? 2,
1097
+ maxInterval: options.maxPollInterval ?? 15
1098
+ });
1099
+ }
1100
+ /** Start an export, wait for it, and save the zip to `targetPath`. Capped at 1 GiB per run. */
1101
+ async downloadAllResults(jobId, runId, targetPath, options = {}) {
1102
+ const exportRef = await this.startResultsExport(jobId, runId);
1103
+ const final = await this.waitForExport(jobId, runId, exportRef.exportId, {
1104
+ timeout: options.waitTimeout ?? 600,
1105
+ pollInterval: options.pollInterval ?? 2
1106
+ });
1107
+ if (final.status !== "completed" || !final.download_url) {
1108
+ throw new Error(final.error ?? "export completed but server returned no download_url");
1109
+ }
1110
+ const { mkdir: mkdir2 } = await import("fs/promises");
1111
+ const { dirname: dirname2 } = await import("path");
1112
+ const { createWriteStream: createWriteStream2 } = await import("fs");
1113
+ const { Readable: Readable2 } = await import("stream");
1114
+ const { pipeline: pipeline2 } = await import("stream/promises");
1115
+ await mkdir2(dirname2(targetPath), { recursive: true });
1116
+ const response = await fetch(final.download_url);
1117
+ if (!response.ok) {
1118
+ throw new ZenRowsBatchError(response.status, void 0);
1119
+ }
1120
+ await pipeline2(Readable2.fromWeb(response.body), createWriteStream2(targetPath));
1121
+ return targetPath;
1122
+ }
1123
+ // ===== waiter =====
1124
+ async waitForRun(jobId, options = {}) {
1125
+ return this._waitForRunRaw(jobId, options);
1126
+ }
1127
+ // ==========================================================
1128
+ // "raw" methods — no resource wrapping; used by handles internally.
1129
+ // ==========================================================
1130
+ /** @internal */
1131
+ async _getJobData(jobId) {
1132
+ return this.transport.requestJson("GET", `/jobs/${jobId}`);
1133
+ }
1134
+ /** @internal */
1135
+ async _getRunData(jobId, runId) {
1136
+ return this.transport.requestJson("GET", `/jobs/${jobId}/runs/${runId}`);
1137
+ }
1138
+ /** @internal */
1139
+ async _postClose(jobId) {
1140
+ return this.transport.requestJson("POST", `/jobs/${jobId}/close`);
1141
+ }
1142
+ /** @internal */
1143
+ async _postStop(jobId) {
1144
+ return this.transport.requestJson("POST", `/jobs/${jobId}/stop`);
1145
+ }
1146
+ /** @internal */
1147
+ async _postPause(jobId) {
1148
+ return this.transport.requestJson("POST", `/jobs/${jobId}/pause`);
1149
+ }
1150
+ /** @internal */
1151
+ async _postResume(jobId) {
1152
+ return this.transport.requestJson("POST", `/jobs/${jobId}/resume`);
1153
+ }
1154
+ /** @internal */
1155
+ async _delete(jobId) {
1156
+ await this.transport.requestJson("DELETE", `/jobs/${jobId}`);
1157
+ }
1158
+ /** @internal */
1159
+ async _deleteRun(jobId, runId) {
1160
+ await this.transport.requestJson("DELETE", `/jobs/${jobId}/runs/${runId}`);
1161
+ }
1162
+ /** @internal */
1163
+ async _postRerun(jobId, options = {}) {
1164
+ const status = Array.isArray(options.status) ? options.status.join(",") : options.status;
1165
+ return this.transport.requestJson("POST", `/jobs/${jobId}/rerun`, {
1166
+ query: status ? { status } : void 0,
1167
+ idempotencyKey: options.idempotencyKey
1168
+ });
1169
+ }
1170
+ /** @internal */
1171
+ async _putSchedule(jobId, schedule) {
1172
+ return this.transport.requestJson("PUT", `/jobs/${jobId}/schedule`, { body: schedule });
1173
+ }
1174
+ /** @internal */
1175
+ async _postScheduleState(jobId, state) {
1176
+ return this.transport.requestJson("POST", `/jobs/${jobId}/schedule/state`, {
1177
+ body: { schedule_state: state }
1178
+ });
1179
+ }
1180
+ /** @internal */
1181
+ async _postTasks(jobId, tasks, options = {}) {
1182
+ return this.transport.requestJson("POST", `/jobs/${jobId}/tasks`, {
1183
+ body: { tasks, last_batch: options.lastBatch ?? false }
1184
+ });
1185
+ }
1186
+ /** @internal */
1187
+ async _postExportStart(jobId, runId) {
1188
+ return this.transport.requestJson("POST", `/jobs/${jobId}/runs/${runId}/exports`);
1189
+ }
1190
+ /** @internal */
1191
+ async _getExport(jobId, runId, exportId) {
1192
+ return this.transport.requestJson("GET", `/jobs/${jobId}/runs/${runId}/exports/${exportId}`);
1193
+ }
1194
+ /** @internal */
1195
+ async _waitForRunRaw(jobId, options) {
1196
+ const target = options.targetStatuses ?? TERMINAL_RUN_STATUSES;
1197
+ const fetchRun = async () => {
1198
+ if (!options.runId) {
1199
+ const job = await this._getJobData(jobId);
1200
+ return job.latest_run;
1201
+ }
1202
+ return this._getRunData(jobId, options.runId);
1203
+ };
1204
+ return pollUntil(fetchRun, {
1205
+ isDone: (run) => run !== void 0 && target.has(run.status),
1206
+ isFailure: (run) => run !== void 0 && Boolean(options.failureStatuses?.has(run.status)),
1207
+ timeout: options.timeout ?? 300,
1208
+ initialInterval: options.pollInterval ?? 2,
1209
+ maxInterval: options.maxPollInterval ?? 15
1210
+ // biome-ignore lint/suspicious/noExplicitAny: fetchRun can resolve undefined for a not-yet-fired scheduled job
1211
+ });
1212
+ }
1213
+ /** @internal */
1214
+ async _waitForIngestRaw(jobId, options = {}) {
1215
+ return pollUntil(() => this._getJobData(jobId), {
1216
+ isDone: (job) => {
1217
+ const run = job.latest_run;
1218
+ if (!run || TERMINAL_RUN_STATUSES.has(run.status)) return true;
1219
+ return run.ingest_status !== "pending";
1220
+ },
1221
+ timeout: options.timeout ?? 300,
1222
+ initialInterval: options.pollInterval ?? 2,
1223
+ maxInterval: options.maxPollInterval ?? 15
1224
+ });
1225
+ }
1226
+ };
1227
+
1228
+ // src/batch/schedule.ts
1229
+ var DAYS_OF_WEEK = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
1230
+ function validateTimezone(tz, field) {
1231
+ if (!tz) {
1232
+ throw new Error(`${field} is required (IANA name, e.g. "Europe/Berlin")`);
1233
+ }
1234
+ try {
1235
+ new Intl.DateTimeFormat(void 0, { timeZone: tz });
1236
+ } catch {
1237
+ throw new Error(`${field}: ${JSON.stringify(tz)} is not a valid IANA timezone`);
1238
+ }
1239
+ }
1240
+ function validateFullHour(s) {
1241
+ if (s.length !== 5 || s[2] !== ":" || s.slice(3) !== "00") {
1242
+ throw new Error(
1243
+ `times_of_day entry ${JSON.stringify(s)} must be on the hour ("HH:00"); minute granularity is rejected.`
1244
+ );
1245
+ }
1246
+ const h = Number(s.slice(0, 2));
1247
+ if (!Number.isInteger(h) || h < 0 || h > 23) {
1248
+ throw new Error(`times_of_day entry ${JSON.stringify(s)} is not a valid hour (00..23)`);
1249
+ }
1250
+ }
1251
+ function hasTzSuffix(raw) {
1252
+ const trimmed = raw.trim();
1253
+ for (const sep of ["T", " "]) {
1254
+ const i = trimmed.indexOf(sep);
1255
+ if (i >= 0) {
1256
+ const tail = trimmed.slice(i + 1);
1257
+ if (tail.endsWith("Z") || tail.endsWith("z")) return true;
1258
+ return tail.length >= 6 && (tail[tail.length - 6] === "+" || tail[tail.length - 6] === "-");
1259
+ }
1260
+ }
1261
+ return false;
1262
+ }
1263
+ function formatLocalNaive(date) {
1264
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
1265
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
1266
+ }
1267
+ var At = class {
1268
+ at;
1269
+ timezone;
1270
+ constructor(at, timezone) {
1271
+ validateTimezone(timezone, "At.timezone");
1272
+ if (at instanceof Date) {
1273
+ this.at = formatLocalNaive(at);
1274
+ } else {
1275
+ if (!at.trim()) {
1276
+ throw new Error("At.at must be a non-empty ISO timestamp string.");
1277
+ }
1278
+ if (hasTzSuffix(at)) {
1279
+ throw new Error(
1280
+ `At.at ${JSON.stringify(at)} must be tz-naive (no "Z", no offset). Supply timezone separately to keep DST transitions deterministic.`
1281
+ );
1282
+ }
1283
+ this.at = at;
1284
+ }
1285
+ this.timezone = timezone;
1286
+ }
1287
+ toRequestBody() {
1288
+ return { at: this.at, timezone: this.timezone };
1289
+ }
1290
+ };
1291
+ var Rate = class {
1292
+ every;
1293
+ unit;
1294
+ constructor(every, unit) {
1295
+ if (!Number.isInteger(every)) {
1296
+ throw new TypeError(`Rate.every must be an integer, got ${every}`);
1297
+ }
1298
+ if (every < 1) {
1299
+ throw new Error(`Rate.every must be >= 1, got ${every}`);
1300
+ }
1301
+ if (unit !== "minute" && unit !== "hour" && unit !== "day") {
1302
+ throw new Error(
1303
+ `Rate.unit must be one of "minute", "hour", "day"; got ${JSON.stringify(unit)}`
1304
+ );
1305
+ }
1306
+ this.every = every;
1307
+ this.unit = unit;
1308
+ }
1309
+ toRequestBody() {
1310
+ return { rate: { every: this.every, unit: this.unit } };
1311
+ }
1312
+ };
1313
+ var Daily = class {
1314
+ };
1315
+ var Weekly = class {
1316
+ days;
1317
+ constructor(days) {
1318
+ if (!days.length) {
1319
+ throw new Error("Weekly.days must be non-empty");
1320
+ }
1321
+ for (const d of days) {
1322
+ if (!DAYS_OF_WEEK.includes(d)) {
1323
+ throw new Error(
1324
+ `Weekly.days entry ${JSON.stringify(d)} is not a valid day (use one of ${DAYS_OF_WEEK.join(", ")})`
1325
+ );
1326
+ }
1327
+ }
1328
+ this.days = days;
1329
+ }
1330
+ };
1331
+ var Monthly = class {
1332
+ days;
1333
+ constructor(days) {
1334
+ if (!days.length) {
1335
+ throw new Error("Monthly.days must be non-empty");
1336
+ }
1337
+ for (const d of days) {
1338
+ if (!Number.isInteger(d)) {
1339
+ throw new TypeError(`Monthly.days entries must be integers, got ${d}`);
1340
+ }
1341
+ if (d < 1 || d > 31) {
1342
+ throw new Error(`Monthly.days entry ${d} is out of range (1..31)`);
1343
+ }
1344
+ }
1345
+ this.days = days;
1346
+ }
1347
+ };
1348
+ var Calendar = class {
1349
+ timesOfDay;
1350
+ cadence;
1351
+ timezone;
1352
+ constructor(timesOfDay, cadence, timezone) {
1353
+ validateTimezone(timezone, "Calendar.timezone");
1354
+ if (!timesOfDay.length) {
1355
+ throw new Error("Calendar.times_of_day must be non-empty");
1356
+ }
1357
+ for (const t of timesOfDay) {
1358
+ validateFullHour(t);
1359
+ }
1360
+ this.timesOfDay = timesOfDay;
1361
+ this.cadence = cadence;
1362
+ this.timezone = timezone;
1363
+ }
1364
+ toRequestBody() {
1365
+ let cadenceBody;
1366
+ if (this.cadence instanceof Daily) {
1367
+ cadenceBody = { daily: {} };
1368
+ } else if (this.cadence instanceof Weekly) {
1369
+ cadenceBody = { weekly: { days: [...this.cadence.days] } };
1370
+ } else {
1371
+ cadenceBody = { monthly: { days: [...this.cadence.days] } };
1372
+ }
1373
+ return {
1374
+ calendar: { times_of_day: [...this.timesOfDay], cadence: cadenceBody },
1375
+ timezone: this.timezone
1376
+ };
1377
+ }
1378
+ };
1379
+
89
1380
  // src/index.ts
90
1381
  var API_URL = "https://api.zenrows.com/v1/";
1382
+ async function isAuth010(response) {
1383
+ try {
1384
+ const body = await response.clone().json();
1385
+ return typeof body?.code === "string" && body.code.toUpperCase() === "AUTH010";
1386
+ } catch {
1387
+ return false;
1388
+ }
1389
+ }
91
1390
  var ZenRows = class {
92
1391
  apiKey;
93
1392
  clientConfig;
94
1393
  queue;
95
1394
  fetchWithRetry;
1395
+ /** Client for the Batch API (async job/run/task model) — see `./batch.ts`. */
1396
+ batch;
96
1397
  constructor(apiKey, clientConfig = {}) {
97
1398
  this.apiKey = apiKey;
98
1399
  this.clientConfig = clientConfig;
1400
+ this.batch = new ZenRowsBatchClient(apiKey);
99
1401
  const retries = this.clientConfig.retries ?? 0;
100
1402
  this.queue = import_fastq.default.promise(this, this.worker, this.clientConfig.concurrency ?? 5);
101
1403
  this.fetchWithRetry = (0, import_fetch_retry.default)(fetch, {
@@ -112,9 +1414,47 @@ var ZenRows = class {
112
1414
  }
113
1415
  });
114
1416
  }
115
- get(url, config, { headers = {} } = {}) {
1417
+ /**
1418
+ * Fetch a URL through Zenrows — the main page-scraping product. This is the primary entry
1419
+ * point; `get()` remains as a deprecated alias for existing callers.
1420
+ */
1421
+ fetch(url, config, { headers = {} } = {}) {
116
1422
  return this.queue.push({ url, config, headers });
117
1423
  }
1424
+ /** @deprecated Use `fetch()` instead. Kept for backward compatibility. */
1425
+ get(url, config, opts = {}) {
1426
+ return this.fetch(url, config, opts);
1427
+ }
1428
+ /**
1429
+ * Fetch a URL and run it through Extract — Zenrows' AI-powered structured extraction
1430
+ * (beta). `mode` defaults to `"auto"`; pass `"native"` or `"standard"` to pick a
1431
+ * different extraction contract. This is a thin, typed wrapper over `fetch()` with the
1432
+ * `extract` param set — no separate endpoint or auth.
1433
+ *
1434
+ * Also sends Adaptive Stealth Mode (`mode: "auto"`) by default, so a target that needs
1435
+ * `js_render`/`premium_proxy` gets escalated automatically instead of failing with
1436
+ * REQS002 — pass `adaptiveStealth: false` to disable that and set `js_render`/
1437
+ * `premium_proxy` yourself.
1438
+ *
1439
+ * `extract=auto` is a domain-gated open beta: when the target domain isn't enabled yet,
1440
+ * the API returns a 402 with `code: "AUTH010"`. By default this method catches that and
1441
+ * retries once with `autoparse: true` instead of returning the error response — pass
1442
+ * `fallbackToAutoparse: false` to disable that and get the raw AUTH010 response back.
1443
+ */
1444
+ async extract(url, config, opts = {}) {
1445
+ const {
1446
+ extract = "auto",
1447
+ fallbackToAutoparse = true,
1448
+ adaptiveStealth = true,
1449
+ ...rest
1450
+ } = config ?? {};
1451
+ const mode = adaptiveStealth ? "auto" : void 0;
1452
+ const response = await this.fetch(url, { ...rest, extract, mode }, opts);
1453
+ if (response.status === 402 && extract === "auto" && fallbackToAutoparse && await isAuth010(response)) {
1454
+ return this.fetch(url, { ...rest, autoparse: true, mode }, opts);
1455
+ }
1456
+ return response;
1457
+ }
118
1458
  post(url, config, { headers = {}, data = {} } = {
119
1459
  headers: { "Content-Type": "application/x-www-form-urlencoded" }
120
1460
  }) {
@@ -185,5 +1525,39 @@ var ZenRows = class {
185
1525
  };
186
1526
  // Annotate the CommonJS export names for ESM import in node:
187
1527
  0 && (module.exports = {
188
- ZenRows
1528
+ AUTO_MAX_CREDITS,
1529
+ AUTO_MIN_CREDITS,
1530
+ At,
1531
+ BASE_CREDITS,
1532
+ Calendar,
1533
+ CurrentRun,
1534
+ DEFAULT_MAX_BYTES_PER_FILE,
1535
+ DEFAULT_MAX_COUNT_IN_MEMORY,
1536
+ DEFAULT_MAX_FILES,
1537
+ DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY,
1538
+ Daily,
1539
+ ExportHandle,
1540
+ ExportRef,
1541
+ JS_AND_PROXY_CREDITS,
1542
+ JS_CREDITS,
1543
+ JobHandle,
1544
+ JobRef,
1545
+ Monthly,
1546
+ PREMIUM_PROXY_CREDITS,
1547
+ Rate,
1548
+ RunHandle,
1549
+ RunRef,
1550
+ WaiterFailureError,
1551
+ WaiterTimeoutError,
1552
+ Weekly,
1553
+ ZenRows,
1554
+ ZenRowsBatchClient,
1555
+ ZenRowsBatchError,
1556
+ downloadTaskToFile,
1557
+ downloadTaskToMemory,
1558
+ downloadToDir,
1559
+ downloadToMemory,
1560
+ estimateCost,
1561
+ parseProblem,
1562
+ pollUntil
189
1563
  });