@crawlee/basic 4.0.0-beta.154 → 4.0.0-beta.156

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.
@@ -7,7 +7,7 @@ import { RobotsTxtFile } from '@crawlee/utils';
7
7
  import { getDomain } from 'tldts';
8
8
  import { z } from 'zod';
9
9
  import { LruCache } from '@apify/datastructures';
10
- import { addTimeoutToPromise, extendTimeout, TimeoutError, tryCancel } from '@apify/timeout';
10
+ import { addTimeoutToPromise, extendTimeout, storage as timeoutStorage, TimeoutError, tryCancel } from '@apify/timeout';
11
11
  import { cryptoRandomObjectId } from '@apify/utilities';
12
12
  import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, timeoutExpiredKey, } from './request-timeout.js';
13
13
  import { createSendRequest } from './send-request.js';
@@ -797,115 +797,119 @@ export class BasicCrawler {
797
797
  * @param [options] Options for the request queue.
798
798
  */
799
799
  async run(requests, options) {
800
- if (this.running) {
801
- throw new Error('This crawler instance is already running, you can add more requests to it via `crawler.addRequests()`.');
802
- }
803
- const { purgeRequestQueue, ...addRequestsOptions } = options ?? {};
804
- if (this.hasFinishedBefore) {
805
- // When executing the run method for the second time explicitly,
806
- // we need to purge the RQ to allow processing the same requests again — this is important so users can
807
- // pass in failed requests back to the `crawler.run()`, otherwise they would be considered as handled and
808
- // ignored — as a failed request is still handled.
809
- // `purgeRequestQueue` unset purges only storage the crawler opened itself (see `#purgeableExtent`);
810
- // `true` also purges a caller-supplied manager, `false` purges nothing.
811
- if (purgeRequestQueue === undefined && this.#purgeableExtent === 'ambiguous') {
812
- throw new Error('Cannot decide what to purge before running again: `sameDomainDelaySecs` paces the request ' +
813
- 'manager you supplied, so the per-domain queues that have to be emptied are the ' +
814
- "crawler's while the manager underneath them is yours. Say which you want: " +
815
- '`run(requests, { purgeRequestQueue: true })` empties both, `false` empties neither.');
800
+ // A crawl is the top level of its own transaction and timeout scope, not a participant in the caller's.
801
+ return withDirectStorageAccess(async () => timeoutStorage.exit(async () => {
802
+ if (this.running) {
803
+ throw new Error('This crawler instance is already running, you can add more requests to it via `crawler.addRequests()`.');
816
804
  }
817
- if (purgeRequestQueue !== false && (this.#purgeableExtent === 'all' || purgeRequestQueue === true)) {
818
- // One call from the outside in reaches everything the manager wraps, a pacer's per-domain queues
819
- // included.
820
- await this.requestManager?.purge?.();
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
+ }
825
+ // A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
826
+ await this.#statisticsDep.ifOwned(async (stats) => {
827
+ stats.reset();
828
+ await stats.resetStore();
829
+ });
830
+ await this.#sessionPoolDep.ifOwned((pool) => pool.resetStore());
821
831
  }
822
- // A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
823
- await this.#statisticsDep.ifOwned(async (stats) => {
824
- stats.reset();
825
- await stats.resetStore();
826
- });
827
- await this.#sessionPoolDep.ifOwned((pool) => pool.resetStore());
828
- }
829
- this.#unexpectedStop = false;
830
- this.running = true;
831
- this.#loggedPerRun.clear();
832
- await purgeDefaultStorages({
833
- onlyPurgeOnce: true,
834
- storageBackend: serviceLocator.getStorageBackend(),
835
- configuration: serviceLocator.getConfiguration(),
836
- });
837
- if (requests) {
838
- await this.addRequests(requests, addRequestsOptions);
839
- }
840
- try {
841
- await this.init();
842
- await this.statistics.startCapturing();
843
- }
844
- catch (error) {
845
- // Clean up here before propagating, otherwise a failed startup would leave the process hanging.
846
- await this.teardown().catch((teardownError) => {
847
- this.log.exception(teardownError, 'Cleaning up after a failed crawler startup failed.');
832
+ this.#unexpectedStop = false;
833
+ this.running = true;
834
+ this.#loggedPerRun.clear();
835
+ await purgeDefaultStorages({
836
+ onlyPurgeOnce: true,
837
+ storageBackend: serviceLocator.getStorageBackend(),
838
+ configuration: serviceLocator.getConfiguration(),
848
839
  });
849
- // The run never began, so let the instance be run again instead of leaving it wedged as `running`.
850
- this.running = false;
851
- throw error;
852
- }
853
- const periodicLogger = this.getPeriodicLogger();
854
- this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
855
- const sigintHandler = async () => {
856
- this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
857
- await this.pauseOnMigration();
858
- await this.#autoscaledPool.abort();
859
- };
860
- // Attach a listener to handle migration and aborting events gracefully.
861
- const boundPauseOnMigration = this.pauseOnMigration.bind(this);
862
- process.once('SIGINT', sigintHandler);
863
- const eventManager = serviceLocator.getEventManager();
864
- eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
865
- eventManager.on(EventType.ABORTING, boundPauseOnMigration);
866
- let stats = {};
867
- try {
868
- await this.#autoscaledPool.run();
869
- }
870
- finally {
871
- await this.statistics.stopCapturing();
872
- await this.teardown();
873
- process.off('SIGINT', sigintHandler);
874
- eventManager.off(EventType.MIGRATING, boundPauseOnMigration);
875
- eventManager.off(EventType.ABORTING, boundPauseOnMigration);
876
- const finalStats = this.statistics.calculate();
877
- stats = {
878
- requestsFinished: this.statistics.state.requestsFinished,
879
- requestsFailed: this.statistics.state.requestsFailed,
880
- retryHistogram: this.statistics.requestRetryHistogram,
881
- ...finalStats,
882
- };
883
- this.log.info('Final request statistics:', stats);
884
- if (this.statistics.errorTracker.total !== 0) {
885
- const prettify = ([count, info]) => `${count}x: ${info.at(-1).trim()} (${info[0]})`;
886
- this.log.info(`Error analysis:`, {
887
- totalErrors: this.statistics.errorTracker.total,
888
- uniqueErrors: this.statistics.errorTracker.getUniqueErrorCount(),
889
- mostCommonErrors: this.statistics.errorTracker.getMostPopularErrors(3).map(prettify),
840
+ if (requests) {
841
+ await this.addRequests(requests, addRequestsOptions);
842
+ }
843
+ try {
844
+ await this.init();
845
+ await this.statistics.startCapturing();
846
+ }
847
+ catch (error) {
848
+ // Clean up here before propagating, otherwise a failed startup would leave the process hanging.
849
+ await this.teardown().catch((teardownError) => {
850
+ this.log.exception(teardownError, 'Cleaning up after a failed crawler startup failed.');
890
851
  });
852
+ // The run never began, so let the instance be run again instead of leaving it wedged as `running`.
853
+ this.running = false;
854
+ throw error;
891
855
  }
892
- const client = serviceLocator.getStorageBackend();
893
- if (client.teardown) {
894
- let finished = false;
895
- setTimeout(() => {
896
- if (!finished) {
897
- this.log.info('Waiting for the storage to write its state to file system.');
898
- }
899
- }, 1000);
900
- await client.teardown();
901
- finished = true;
856
+ const periodicLogger = this.getPeriodicLogger();
857
+ this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
858
+ const sigintHandler = async () => {
859
+ this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
860
+ await this.pauseOnMigration();
861
+ await this.#autoscaledPool.abort();
862
+ };
863
+ // Attach a listener to handle migration and aborting events gracefully.
864
+ const boundPauseOnMigration = this.pauseOnMigration.bind(this);
865
+ process.once('SIGINT', sigintHandler);
866
+ const eventManager = serviceLocator.getEventManager();
867
+ eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
868
+ eventManager.on(EventType.ABORTING, boundPauseOnMigration);
869
+ let stats = {};
870
+ try {
871
+ await this.#autoscaledPool.run();
902
872
  }
903
- periodicLogger.stop();
904
- 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' });
905
- this.running = false;
906
- this.hasFinishedBefore = true;
907
- }
908
- return stats;
873
+ finally {
874
+ await this.statistics.stopCapturing();
875
+ await this.teardown();
876
+ process.off('SIGINT', sigintHandler);
877
+ eventManager.off(EventType.MIGRATING, boundPauseOnMigration);
878
+ eventManager.off(EventType.ABORTING, boundPauseOnMigration);
879
+ const finalStats = this.statistics.calculate();
880
+ stats = {
881
+ requestsFinished: this.statistics.state.requestsFinished,
882
+ requestsFailed: this.statistics.state.requestsFailed,
883
+ retryHistogram: this.statistics.requestRetryHistogram,
884
+ ...finalStats,
885
+ };
886
+ this.log.info('Final request statistics:', stats);
887
+ if (this.statistics.errorTracker.total !== 0) {
888
+ const prettify = ([count, info]) => `${count}x: ${info.at(-1).trim()} (${info[0]})`;
889
+ this.log.info(`Error analysis:`, {
890
+ totalErrors: this.statistics.errorTracker.total,
891
+ uniqueErrors: this.statistics.errorTracker.getUniqueErrorCount(),
892
+ mostCommonErrors: this.statistics.errorTracker.getMostPopularErrors(3).map(prettify),
893
+ });
894
+ }
895
+ const client = serviceLocator.getStorageBackend();
896
+ if (client.teardown) {
897
+ let finished = false;
898
+ setTimeout(() => {
899
+ if (!finished) {
900
+ this.log.info('Waiting for the storage to write its state to file system.');
901
+ }
902
+ }, 1000);
903
+ await client.teardown();
904
+ finished = true;
905
+ }
906
+ periodicLogger.stop();
907
+ 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
+ this.running = false;
909
+ this.hasFinishedBefore = true;
910
+ }
911
+ return stats;
912
+ }));
909
913
  }
910
914
  /**
911
915
  * Gracefully stops the current run of the crawler.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/basic",
3
- "version": "4.0.0-beta.154",
3
+ "version": "4.0.0-beta.156",
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.154",
46
- "@crawlee/http-client": "4.0.0-beta.154",
47
- "@crawlee/types": "4.0.0-beta.154",
48
- "@crawlee/utils": "4.0.0-beta.154",
45
+ "@crawlee/core": "4.0.0-beta.156",
46
+ "@crawlee/http-client": "4.0.0-beta.156",
47
+ "@crawlee/types": "4.0.0-beta.156",
48
+ "@crawlee/utils": "4.0.0-beta.156",
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.154"
56
+ "@crawlee/impit-client": "^4.0.0-beta.156"
57
57
  },
58
58
  "lerna": {
59
59
  "command": {
@@ -62,5 +62,5 @@
62
62
  }
63
63
  }
64
64
  },
65
- "gitHead": "12177899d79c46dc6ea3046220fc896f657f650d"
65
+ "gitHead": "cad9cdee7cb09ec5a3662532768fea9e43b6ab4e"
66
66
  }