@crawlee/basic 4.0.0-beta.157 → 4.0.0-beta.159

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.
@@ -434,7 +434,8 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
434
434
  }, CrawlingContext>;
435
435
  get contextPipeline(): ContextPipeline<CrawlingContext, ExtendedContext>;
436
436
  running: boolean;
437
- hasFinishedBefore: boolean;
437
+ /** Whether a `run()` on this instance has already finished - a repeated one continues where it left off. */
438
+ get hasFinishedBefore(): boolean;
438
439
  get log(): CrawleeLogger;
439
440
  protected readonly requestHandler: RequestHandler<ExtendedContext>;
440
441
  protected readonly internalTimeoutMillis: number;
@@ -553,8 +554,12 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
553
554
  * We can use the `requests` parameter to enqueue the initial requests — it is a shortcut for
554
555
  * running {@link BasicCrawler.addRequests|`crawler.addRequests()`} before {@link BasicCrawler.run|`crawler.run()`}.
555
556
  *
557
+ * Calling `run()` again on the same instance keeps crawling the same request manager - requests the previous
558
+ * run handled (a failed one counts as handled) are not processed again. Purge the queue or open a fresh one
559
+ * if that is what you want.
560
+ *
556
561
  * @param [requests] The requests to add.
557
- * @param [options] Options for the request queue.
562
+ * @param [options] Options for adding the initial requests.
558
563
  */
559
564
  run(requests?: TypedRequestsLike<Routes>, options?: CrawlerRunOptions): Promise<FinalStatistics>;
560
565
  /**
@@ -766,17 +771,6 @@ export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions, En
766
771
  export interface CrawlerAddRequestsResult extends AddRequestsBatchedResult {
767
772
  }
768
773
  export interface CrawlerRunOptions extends CrawlerAddRequestsOptions {
769
- /**
770
- * Controls whether the request queue is purged between repeated `run()` calls on the same crawler instance.
771
- * Purging clears all requests and resets internal counters, allowing the same URLs to be processed again.
772
- *
773
- * - **`undefined`** (default) — only the crawler's own (auto-created) queue is purged.
774
- * A user-supplied `requestQueue` is left untouched.
775
- * - **`true`** — the queue is always purged, even if it was supplied by the user.
776
- * - **`false`** — nothing is purged. Only genuinely new requests will be processed;
777
- * note that even a failed request is considered handled.
778
- */
779
- purgeRequestQueue?: boolean;
780
774
  }
781
775
  /**
782
776
  * Creates new {@link Router} instance that works based on request labels.
@@ -182,8 +182,12 @@ export class BasicCrawler {
182
182
  return this.#contextPipeline;
183
183
  }
184
184
  running = false;
185
- hasFinishedBefore = false;
185
+ #hasFinishedBefore = false;
186
186
  #unexpectedStop = false;
187
+ /** Whether a `run()` on this instance has already finished - a repeated one continues where it left off. */
188
+ get hasFinishedBefore() {
189
+ return this.#hasFinishedBefore;
190
+ }
187
191
  #log;
188
192
  get log() {
189
193
  return this.#log;
@@ -195,16 +199,6 @@ export class BasicCrawler {
195
199
  internalTimeoutMillis;
196
200
  #maxRequestRetries;
197
201
  #maxCrawlDepth;
198
- /**
199
- * How much of {@link BasicCrawler.requestManager} the crawler may empty between repeated `run()` calls.
200
- *
201
- * - `all` — nothing under it came from the caller, so one `purge()` on the outside covers everything.
202
- * - `none` — the caller supplied it and the crawler put nothing of its own inside.
203
- * - `ambiguous` — the caller supplied it, but `sameDomainDelaySecs` put the crawler's own per-domain queues
204
- * underneath: purging empties the caller's storage too, skipping leaves ours stale. A repeated `run()` asks
205
- * rather than guessing.
206
- */
207
- #purgeableExtent;
208
202
  #maxRequestsPerCrawl;
209
203
  get handledRequestsCount() {
210
204
  return this.statistics.state.requestsFinished + this.statistics.state.requestsFailed;
@@ -357,14 +351,6 @@ export class BasicCrawler {
357
351
  }) ??
358
352
  false);
359
353
  const pacerNeeded = sameDomainDelaySecs > 0 && !floorTaken;
360
- // Our per-domain queues under a manager the caller owns is the case with no right answer; a floor
361
- // it took leaves nothing of ours behind. See `#purgeableExtent`.
362
- if (suppliedManager === undefined) {
363
- this.#purgeableExtent = 'all';
364
- }
365
- else {
366
- this.#purgeableExtent = pacerNeeded ? 'ambiguous' : 'none';
367
- }
368
354
  // Built here rather than at first use so it can sit *inside* the tandem below, which is where a
369
355
  // loader's transferred requests pass through it.
370
356
  const writableManager = pacerNeeded
@@ -793,8 +779,12 @@ export class BasicCrawler {
793
779
  * We can use the `requests` parameter to enqueue the initial requests — it is a shortcut for
794
780
  * running {@link BasicCrawler.addRequests|`crawler.addRequests()`} before {@link BasicCrawler.run|`crawler.run()`}.
795
781
  *
782
+ * Calling `run()` again on the same instance keeps crawling the same request manager - requests the previous
783
+ * run handled (a failed one counts as handled) are not processed again. Purge the queue or open a fresh one
784
+ * if that is what you want.
785
+ *
796
786
  * @param [requests] The requests to add.
797
- * @param [options] Options for the request queue.
787
+ * @param [options] Options for adding the initial requests.
798
788
  */
799
789
  async run(requests, options) {
800
790
  // A crawl is the top level of its own transaction and timeout scope, not a participant in the caller's.
@@ -802,26 +792,7 @@ export class BasicCrawler {
802
792
  if (this.running) {
803
793
  throw new Error('This crawler instance is already running, you can add more requests to it via `crawler.addRequests()`.');
804
794
  }
805
- const { purgeRequestQueue, ...addRequestsOptions } = options ?? {};
806
- if (this.hasFinishedBefore) {
807
- // When executing the run method for the second time explicitly,
808
- // we need to purge the RQ to allow processing the same requests again — this is important so users can
809
- // pass in failed requests back to the `crawler.run()`, otherwise they would be considered as handled and
810
- // ignored — as a failed request is still handled.
811
- // `purgeRequestQueue` unset purges only storage the crawler opened itself (see `#purgeableExtent`);
812
- // `true` also purges a caller-supplied manager, `false` purges nothing.
813
- if (purgeRequestQueue === undefined && this.#purgeableExtent === 'ambiguous') {
814
- throw new Error('Cannot decide what to purge before running again: `sameDomainDelaySecs` paces the request ' +
815
- 'manager you supplied, so the per-domain queues that have to be emptied are the ' +
816
- "crawler's while the manager underneath them is yours. Say which you want: " +
817
- '`run(requests, { purgeRequestQueue: true })` empties both, `false` empties neither.');
818
- }
819
- if (purgeRequestQueue !== false &&
820
- (this.#purgeableExtent === 'all' || purgeRequestQueue === true)) {
821
- // One call from the outside in reaches everything the manager wraps, a pacer's per-domain queues
822
- // included.
823
- await this.requestManager?.purge?.();
824
- }
795
+ if (this.#hasFinishedBefore) {
825
796
  // A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
826
797
  await this.#statisticsDep.ifOwned(async (stats) => {
827
798
  stats.reset();
@@ -838,7 +809,7 @@ export class BasicCrawler {
838
809
  configuration: serviceLocator.getConfiguration(),
839
810
  });
840
811
  if (requests) {
841
- await this.addRequests(requests, addRequestsOptions);
812
+ await this.addRequests(requests, options);
842
813
  }
843
814
  try {
844
815
  await this.init();
@@ -884,6 +855,22 @@ export class BasicCrawler {
884
855
  ...finalStats,
885
856
  };
886
857
  this.log.info('Final request statistics:', stats);
858
+ // A crawl that did nothing while the manager holds only handled requests is a mistake whoever
859
+ // handled them - this run, another crawler on the same queue, or a previous process. Starting
860
+ // against handled requests is not: that is what resuming a crawl looks like.
861
+ if (stats.requestsFinished + stats.requestsFailed === 0) {
862
+ // Never let the diagnostic itself break the run.
863
+ const alreadyHandled = (await this.requestManager?.getHandledCount().catch(() => 0)) ?? 0;
864
+ if (alreadyHandled > 0) {
865
+ this.log.warningOnce('This crawl processed no requests - the request manager holds ' +
866
+ `${alreadyHandled} request${alreadyHandled === 1 ? '' : 's'}, all of them ` +
867
+ 'already handled, and a failed request counts as handled too. Nothing ' +
868
+ 'empties a queue between runs, so to crawl them again, purge it ' +
869
+ '(`await queue.purge()`) or use a fresh one (e.g. ' +
870
+ '`RequestQueue.open({ alias: "second-run" })`) with a freshly created ' +
871
+ 'crawler instance.');
872
+ }
873
+ }
887
874
  if (this.statistics.errorTracker.total !== 0) {
888
875
  const prettify = ([count, info]) => `${count}x: ${info.at(-1).trim()} (${info[0]})`;
889
876
  this.log.info(`Error analysis:`, {
@@ -906,7 +893,7 @@ export class BasicCrawler {
906
893
  periodicLogger.stop();
907
894
  this.setStatusMessage(`Finished! Total ${this.statistics.state.requestsFinished + this.statistics.state.requestsFailed} requests: ${this.statistics.state.requestsFinished} succeeded, ${this.statistics.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' });
908
895
  this.running = false;
909
- this.hasFinishedBefore = true;
896
+ this.#hasFinishedBefore = true;
910
897
  }
911
898
  return stats;
912
899
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/basic",
3
- "version": "4.0.0-beta.157",
3
+ "version": "4.0.0-beta.159",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -42,10 +42,10 @@
42
42
  "@apify/datastructures": "^2.0.0",
43
43
  "@apify/timeout": "^0.4.4",
44
44
  "@apify/utilities": "^2.15.5",
45
- "@crawlee/core": "4.0.0-beta.157",
46
- "@crawlee/http-client": "4.0.0-beta.157",
47
- "@crawlee/types": "4.0.0-beta.157",
48
- "@crawlee/utils": "4.0.0-beta.157",
45
+ "@crawlee/core": "4.0.0-beta.159",
46
+ "@crawlee/http-client": "4.0.0-beta.159",
47
+ "@crawlee/types": "4.0.0-beta.159",
48
+ "@crawlee/utils": "4.0.0-beta.159",
49
49
  "csv-stringify": "^6.5.2",
50
50
  "tldts": "^7.0.6",
51
51
  "tslib": "^2.8.1",
@@ -53,7 +53,7 @@
53
53
  "zod": "^4.4.3"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@crawlee/impit-client": "^4.0.0-beta.157"
56
+ "@crawlee/impit-client": "4.0.0-beta.159"
57
57
  },
58
58
  "lerna": {
59
59
  "command": {
@@ -62,5 +62,5 @@
62
62
  }
63
63
  }
64
64
  },
65
- "gitHead": "a1b3d4f72a2759a7179af6648083f57d022125a9"
65
+ "gitHead": "9f91c03967915be4e9b077a717e0e3494ad0a101"
66
66
  }