@crawlee/basic 3.0.0-alpha.2 → 3.0.0-alpha.20

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.
@@ -0,0 +1,690 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BasicCrawler = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const log_1 = tslib_1.__importDefault(require("@apify/log"));
6
+ const timeout_1 = require("@apify/timeout");
7
+ const utilities_1 = require("@apify/utilities");
8
+ const core_1 = require("@crawlee/core");
9
+ const ow_1 = tslib_1.__importStar(require("ow"));
10
+ /**
11
+ * Since there's no set number of seconds before the container is terminated after
12
+ * a migration event, we need some reasonable number to use for RequestList persistence.
13
+ * Once a migration event is received, the Crawler will be paused and it will wait for
14
+ * this long before persisting the RequestList state. This should allow most healthy
15
+ * requests to finish and be marked as handled, thus lowering the amount of duplicate
16
+ * results after migration.
17
+ * @ignore
18
+ */
19
+ const SAFE_MIGRATION_WAIT_MILLIS = 20000;
20
+ /**
21
+ * Provides a simple framework for parallel crawling of web pages.
22
+ * The URLs to crawl are fed either from a static list of URLs
23
+ * or from a dynamic queue of URLs enabling recursive crawling of websites.
24
+ *
25
+ * `BasicCrawler` is a low-level tool that requires the user to implement the page
26
+ * download and data extraction functionality themselves.
27
+ * If you want a crawler that already facilitates this functionality,
28
+ * please consider using {@link CheerioCrawler}, {@link PuppeteerCrawler} or {@link PlaywrightCrawler}.
29
+ *
30
+ * `BasicCrawler` invokes the user-provided {@link BasicCrawlerOptions.requestHandler}
31
+ * for each {@link Request} object, which represents a single URL to crawl.
32
+ * The {@link Request} objects are fed from the {@link RequestList} or the {@link RequestQueue}
33
+ * instances provided by the {@link BasicCrawlerOptions.requestList} or {@link BasicCrawlerOptions.requestQueue}
34
+ * constructor options, respectively.
35
+ *
36
+ * If both {@link BasicCrawlerOptions.requestList} and {@link BasicCrawlerOptions.requestQueue} options are used,
37
+ * the instance first processes URLs from the {@link RequestList} and automatically enqueues all of them
38
+ * to {@link RequestQueue} before it starts their processing. This ensures that a single URL is not crawled multiple times.
39
+ *
40
+ * The crawler finishes if there are no more {@link Request} objects to crawl.
41
+ *
42
+ * New requests are only dispatched when there is enough free CPU and memory available,
43
+ * using the functionality provided by the {@link AutoscaledPool} class.
44
+ * All {@link AutoscaledPool} configuration options can be passed to the `autoscaledPoolOptions`
45
+ * parameter of the `BasicCrawler` constructor. For user convenience, the `minConcurrency` and `maxConcurrency`
46
+ * {@link AutoscaledPool} options are available directly in the `BasicCrawler` constructor.
47
+ *
48
+ * **Example usage:**
49
+ *
50
+ * ```javascript
51
+ * const { gotScraping } = require('got-scraping');
52
+ *
53
+ * // Prepare a list of URLs to crawl
54
+ * const requestList = new RequestList({
55
+ * sources: [
56
+ * { url: 'http://www.example.com/page-1' },
57
+ * { url: 'http://www.example.com/page-2' },
58
+ * ],
59
+ * });
60
+ * await requestList.initialize();
61
+ *
62
+ * // Crawl the URLs
63
+ * const crawler = new BasicCrawler({
64
+ * requestList,
65
+ * handleRequestFunction: async ({ request }) => {
66
+ * // 'request' contains an instance of the Request class
67
+ * // Here we simply fetch the HTML of the page and store it to a dataset
68
+ * const { body } = await gotScraping({
69
+ * url: request.url,
70
+ * method: request.method,
71
+ * body: request.payload,
72
+ * headers: request.headers,
73
+ * });
74
+ *
75
+ * await Actor.pushData({
76
+ * url: request.url,
77
+ * html: body,
78
+ * })
79
+ * },
80
+ * });
81
+ *
82
+ * await crawler.run();
83
+ * ```
84
+ * @category Crawlers
85
+ */
86
+ class BasicCrawler {
87
+ /**
88
+ * All `BasicCrawler` parameters are passed via an options object.
89
+ */
90
+ constructor(options, config = core_1.Configuration.getGlobalConfig()) {
91
+ Object.defineProperty(this, "config", {
92
+ enumerable: true,
93
+ configurable: true,
94
+ writable: true,
95
+ value: config
96
+ });
97
+ /**
98
+ * Static list of URLs to be processed.
99
+ */
100
+ Object.defineProperty(this, "stats", {
101
+ enumerable: true,
102
+ configurable: true,
103
+ writable: true,
104
+ value: void 0
105
+ });
106
+ /**
107
+ * A reference to the underlying {@link RequestList} class that manages the crawler's {@link Request}s.
108
+ * Either `requestList` or `requestQueue` option must be provided (or both).
109
+ * Only available if used by the crawler.
110
+ */
111
+ Object.defineProperty(this, "requestList", {
112
+ enumerable: true,
113
+ configurable: true,
114
+ writable: true,
115
+ value: void 0
116
+ });
117
+ /**
118
+ * Dynamic queue of URLs to be processed. This is useful for recursive crawling of websites.
119
+ * A reference to the underlying {@link RequestQueue} class that manages the crawler's {@link Request}s.
120
+ * Either `requestList` or `requestQueue` option must be provided (or both).
121
+ * Only available if used by the crawler.
122
+ */
123
+ Object.defineProperty(this, "requestQueue", {
124
+ enumerable: true,
125
+ configurable: true,
126
+ writable: true,
127
+ value: void 0
128
+ });
129
+ /**
130
+ * A reference to the underlying {@link SessionPool} class that manages the crawler's {@link Session}s.
131
+ * Only available if used by the crawler.
132
+ */
133
+ Object.defineProperty(this, "sessionPool", {
134
+ enumerable: true,
135
+ configurable: true,
136
+ writable: true,
137
+ value: void 0
138
+ });
139
+ /**
140
+ * A reference to the underlying {@link AutoscaledPool} class that manages the concurrency of the crawler.
141
+ * Note that this property is only initialized after calling the {@link BasicCrawler.run} function.
142
+ * You can use it to change the concurrency settings on the fly,
143
+ * to pause the crawler by calling {@link AutoscaledPool.pause}
144
+ * or to abort it by calling {@link AutoscaledPool.abort}.
145
+ */
146
+ Object.defineProperty(this, "autoscaledPool", {
147
+ enumerable: true,
148
+ configurable: true,
149
+ writable: true,
150
+ value: void 0
151
+ });
152
+ Object.defineProperty(this, "log", {
153
+ enumerable: true,
154
+ configurable: true,
155
+ writable: true,
156
+ value: void 0
157
+ });
158
+ Object.defineProperty(this, "requestHandler", {
159
+ enumerable: true,
160
+ configurable: true,
161
+ writable: true,
162
+ value: void 0
163
+ });
164
+ Object.defineProperty(this, "failedRequestHandler", {
165
+ enumerable: true,
166
+ configurable: true,
167
+ writable: true,
168
+ value: void 0
169
+ });
170
+ Object.defineProperty(this, "requestHandlerTimeoutMillis", {
171
+ enumerable: true,
172
+ configurable: true,
173
+ writable: true,
174
+ value: void 0
175
+ });
176
+ Object.defineProperty(this, "internalTimeoutMillis", {
177
+ enumerable: true,
178
+ configurable: true,
179
+ writable: true,
180
+ value: void 0
181
+ });
182
+ Object.defineProperty(this, "maxRequestRetries", {
183
+ enumerable: true,
184
+ configurable: true,
185
+ writable: true,
186
+ value: void 0
187
+ });
188
+ Object.defineProperty(this, "handledRequestsCount", {
189
+ enumerable: true,
190
+ configurable: true,
191
+ writable: true,
192
+ value: void 0
193
+ });
194
+ Object.defineProperty(this, "sessionPoolOptions", {
195
+ enumerable: true,
196
+ configurable: true,
197
+ writable: true,
198
+ value: void 0
199
+ });
200
+ Object.defineProperty(this, "useSessionPool", {
201
+ enumerable: true,
202
+ configurable: true,
203
+ writable: true,
204
+ value: void 0
205
+ });
206
+ Object.defineProperty(this, "crawlingContexts", {
207
+ enumerable: true,
208
+ configurable: true,
209
+ writable: true,
210
+ value: new Map()
211
+ });
212
+ Object.defineProperty(this, "autoscaledPoolOptions", {
213
+ enumerable: true,
214
+ configurable: true,
215
+ writable: true,
216
+ value: void 0
217
+ });
218
+ Object.defineProperty(this, "events", {
219
+ enumerable: true,
220
+ configurable: true,
221
+ writable: true,
222
+ value: void 0
223
+ });
224
+ (0, ow_1.default)(options, 'BasicCrawlerOptions', ow_1.default.object.exactShape(BasicCrawler.optionsShape));
225
+ const { requestList, requestQueue, maxRequestRetries = 3, maxRequestsPerCrawl, autoscaledPoolOptions = {}, sessionPoolOptions = {}, useSessionPool = true,
226
+ // AutoscaledPool shorthands
227
+ minConcurrency, maxConcurrency,
228
+ // internal
229
+ log = log_1.default.child({ prefix: this.constructor.name }),
230
+ // Old and new request handler methods
231
+ handleRequestFunction, requestHandler, handleRequestTimeoutSecs, requestHandlerTimeoutSecs, handleFailedRequestFunction, failedRequestHandler, } = options;
232
+ this.requestList = requestList;
233
+ this.requestQueue = requestQueue;
234
+ this.log = log;
235
+ this.events = config.getEventManager();
236
+ this._handlePropertyNameChange({
237
+ newName: 'requestHandler',
238
+ oldName: 'handleRequestFunction',
239
+ propertyKey: 'requestHandler',
240
+ newProperty: requestHandler,
241
+ oldProperty: handleRequestFunction,
242
+ });
243
+ this._handlePropertyNameChange({
244
+ newName: 'failedRequestHandler',
245
+ oldName: 'handleFailedRequestFunction',
246
+ propertyKey: 'failedRequestHandler',
247
+ newProperty: failedRequestHandler,
248
+ oldProperty: handleFailedRequestFunction,
249
+ allowUndefined: true,
250
+ });
251
+ let newRequestHandlerTimeout;
252
+ if (!handleRequestTimeoutSecs) {
253
+ if (!requestHandlerTimeoutSecs) {
254
+ newRequestHandlerTimeout = 60000;
255
+ }
256
+ else {
257
+ newRequestHandlerTimeout = requestHandlerTimeoutSecs * 1000;
258
+ }
259
+ }
260
+ else if (requestHandlerTimeoutSecs) {
261
+ newRequestHandlerTimeout = requestHandlerTimeoutSecs * 1000;
262
+ }
263
+ this._handlePropertyNameChange({
264
+ newName: 'requestHandlerTimeoutSecs',
265
+ oldName: 'handleRequestTimeoutSecs',
266
+ propertyKey: 'requestHandlerTimeoutMillis',
267
+ newProperty: newRequestHandlerTimeout,
268
+ oldProperty: handleRequestTimeoutSecs ? handleRequestTimeoutSecs * 1000 : undefined,
269
+ });
270
+ const tryEnv = (val) => (val == null ? null : +val);
271
+ // allow at least 5min for internal timeouts
272
+ this.internalTimeoutMillis = tryEnv(process.env.CRAWLEE_INTERNAL_TIMEOUT) ?? Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
273
+ // override the default internal timeout of request queue to respect `requestHandlerTimeoutMillis`
274
+ if (this.requestQueue) {
275
+ this.requestQueue.internalTimeoutMillis = this.internalTimeoutMillis;
276
+ }
277
+ this.maxRequestRetries = maxRequestRetries;
278
+ this.handledRequestsCount = 0;
279
+ this.stats = new core_1.Statistics({ logMessage: `${log.getOptions().prefix} request statistics:`, config });
280
+ this.sessionPoolOptions = {
281
+ ...sessionPoolOptions,
282
+ log,
283
+ };
284
+ this.useSessionPool = useSessionPool;
285
+ this.crawlingContexts = new Map();
286
+ const maxSignedInteger = 2 ** 31 - 1;
287
+ if (this.requestHandlerTimeoutMillis > maxSignedInteger) {
288
+ log.warning(`requestHandlerTimeoutMillis ${this.requestHandlerTimeoutMillis}`
289
+ + `does not fit a signed 32-bit integer. Limiting the value to ${maxSignedInteger}`);
290
+ this.requestHandlerTimeoutMillis = maxSignedInteger;
291
+ }
292
+ let shouldLogMaxPagesExceeded = true;
293
+ const isMaxPagesExceeded = () => maxRequestsPerCrawl && maxRequestsPerCrawl <= this.handledRequestsCount;
294
+ const { isFinishedFunction } = autoscaledPoolOptions;
295
+ const basicCrawlerAutoscaledPoolConfiguration = {
296
+ minConcurrency,
297
+ maxConcurrency,
298
+ runTaskFunction: this._runTaskFunction.bind(this),
299
+ isTaskReadyFunction: async () => {
300
+ if (isMaxPagesExceeded()) {
301
+ if (shouldLogMaxPagesExceeded) {
302
+ log.info('Crawler reached the maxRequestsPerCrawl limit of '
303
+ + `${maxRequestsPerCrawl} requests and will shut down soon. Requests that are in progress will be allowed to finish.`);
304
+ shouldLogMaxPagesExceeded = false;
305
+ }
306
+ return false;
307
+ }
308
+ return this._isTaskReadyFunction();
309
+ },
310
+ isFinishedFunction: async () => {
311
+ if (isMaxPagesExceeded()) {
312
+ log.info(`Earlier, the crawler reached the maxRequestsPerCrawl limit of ${maxRequestsPerCrawl} requests `
313
+ + 'and all requests that were in progress at that time have now finished. '
314
+ + `In total, the crawler processed ${this.handledRequestsCount} requests and will shut down.`);
315
+ return true;
316
+ }
317
+ const isFinished = isFinishedFunction
318
+ ? await isFinishedFunction()
319
+ : await this._defaultIsFinishedFunction();
320
+ if (isFinished) {
321
+ const reason = isFinishedFunction
322
+ ? 'Crawler\'s custom isFinishedFunction() returned true, the crawler will shut down.'
323
+ : 'All the requests from request list and/or request queue have been processed, the crawler will shut down.';
324
+ log.info(reason);
325
+ }
326
+ return isFinished;
327
+ },
328
+ log,
329
+ };
330
+ this.autoscaledPoolOptions = { ...autoscaledPoolOptions, ...basicCrawlerAutoscaledPoolConfiguration };
331
+ // Attach a listener to handle migration and aborting events gracefully.
332
+ this.events.on("migrating" /* MIGRATING */, this._pauseOnMigration.bind(this));
333
+ this.events.on("aborting" /* ABORTING */, this._pauseOnMigration.bind(this));
334
+ }
335
+ /**
336
+ * Runs the crawler. Returns a promise that gets resolved once all the requests are processed.
337
+ */
338
+ async run() {
339
+ await this._init();
340
+ await this.stats.startCapturing();
341
+ try {
342
+ await this.autoscaledPool.run();
343
+ }
344
+ finally {
345
+ await this.teardown();
346
+ await this.stats.stopCapturing();
347
+ }
348
+ const finalStats = this.stats.calculate();
349
+ const stats = {
350
+ requestsFinished: this.stats.state.requestsFinished,
351
+ requestsFailed: this.stats.state.requestsFailed,
352
+ retryHistogram: this.stats.requestRetryHistogram,
353
+ ...finalStats,
354
+ };
355
+ this.log.info('Final request statistics:', stats);
356
+ return stats;
357
+ }
358
+ async getRequestQueue() {
359
+ this.requestQueue ?? (this.requestQueue = await core_1.RequestQueue.open());
360
+ return this.requestQueue;
361
+ }
362
+ /**
363
+ * Adds requests to be processed by the crawler
364
+ * @param requests The requests to add
365
+ * @param options Options for the request queue
366
+ */
367
+ async addRequests(requests, options = {}) {
368
+ (0, ow_1.default)(requests, ow_1.default.array.ofType(ow_1.default.any(ow_1.default.string, ow_1.default.object.partialShape({
369
+ url: ow_1.default.string,
370
+ id: ow_1.default.undefined,
371
+ }))));
372
+ (0, ow_1.default)(options, ow_1.default.object.exactShape({
373
+ forefront: ow_1.default.optional.boolean,
374
+ }));
375
+ const requestQueue = await this.getRequestQueue();
376
+ const builtRequests = (0, core_1.createRequests)(requests);
377
+ return requestQueue.addRequests(builtRequests);
378
+ }
379
+ async _init() {
380
+ // Initialize AutoscaledPool before awaiting _loadHandledRequestCount(),
381
+ // so that the caller can get a reference to it before awaiting the promise returned from run()
382
+ // (otherwise there would be no way)
383
+ this.autoscaledPool = new core_1.AutoscaledPool(this.autoscaledPoolOptions, this.config);
384
+ if (this.useSessionPool) {
385
+ this.sessionPool = await core_1.SessionPool.open(this.sessionPoolOptions);
386
+ // Assuming there are not more than 20 browsers running at once;
387
+ this.sessionPool.setMaxListeners(20);
388
+ }
389
+ await this._loadHandledRequestCount();
390
+ }
391
+ async _runRequestHandler(crawlingContext) {
392
+ await this.requestHandler(crawlingContext);
393
+ }
394
+ async _pauseOnMigration() {
395
+ if (this.autoscaledPool) {
396
+ // if run wasn't called, this is going to crash
397
+ await this.autoscaledPool.pause(SAFE_MIGRATION_WAIT_MILLIS)
398
+ .catch((err) => {
399
+ if (err.message.includes('running tasks did not finish')) {
400
+ this.log.error('The crawler was paused due to migration to another host, '
401
+ + 'but some requests did not finish in time. Those requests\' results may be duplicated.');
402
+ }
403
+ else {
404
+ throw err;
405
+ }
406
+ });
407
+ }
408
+ const requestListPersistPromise = (async () => {
409
+ if (this.requestList) {
410
+ if (await this.requestList.isFinished())
411
+ return;
412
+ await this.requestList.persistState()
413
+ .catch((err) => {
414
+ if (err.message.includes('Cannot persist state.')) {
415
+ this.log.error('The crawler attempted to persist its request list\'s state and failed due to missing or '
416
+ + 'invalid config. Make sure to use either RequestList.open() or the "stateKeyPrefix" option of RequestList '
417
+ + 'constructor to ensure your crawling state is persisted through host migrations and restarts.');
418
+ }
419
+ else {
420
+ this.log.exception(err, 'An unexpected error occured when the crawler '
421
+ + 'attempted to persist its request list\'s state.');
422
+ }
423
+ });
424
+ }
425
+ })();
426
+ await Promise.all([
427
+ requestListPersistPromise,
428
+ this.stats.persistState(),
429
+ ]);
430
+ }
431
+ /**
432
+ * Fetches request from either RequestList or RequestQueue. If request comes from a RequestList
433
+ * and RequestQueue is present then enqueues it to the queue first.
434
+ */
435
+ async _fetchNextRequest() {
436
+ if (!this.requestList)
437
+ return this.requestQueue.fetchNextRequest();
438
+ const request = await this.requestList.fetchNextRequest();
439
+ if (!this.requestQueue)
440
+ return request;
441
+ if (!request)
442
+ return this.requestQueue.fetchNextRequest();
443
+ try {
444
+ await this.requestQueue.addRequest(request, { forefront: true });
445
+ }
446
+ catch (err) {
447
+ // If requestQueue.addRequest() fails here then we must reclaim it back to
448
+ // the RequestList because probably it's not yet in the queue!
449
+ this.log.error('Adding of request from the RequestList to the RequestQueue failed, reclaiming request back to the list.', { request });
450
+ await this.requestList.reclaimRequest(request);
451
+ return null;
452
+ }
453
+ await this.requestList.markRequestHandled(request);
454
+ return this.requestQueue.fetchNextRequest();
455
+ }
456
+ /**
457
+ * Wrapper around requestHandler that fetches requests from RequestList/RequestQueue
458
+ * then retries them in a case of an error, etc.
459
+ */
460
+ async _runTaskFunction() {
461
+ const source = this.requestQueue || this.requestList || await this.getRequestQueue();
462
+ let request;
463
+ let session;
464
+ await this._timeoutAndRetry(async () => {
465
+ request = await this._fetchNextRequest();
466
+ }, this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
467
+ (0, timeout_1.tryCancel)();
468
+ if (this.useSessionPool) {
469
+ await this._timeoutAndRetry(async () => {
470
+ session = await this.sessionPool.getSession();
471
+ }, this.internalTimeoutMillis, `Fetching session timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
472
+ }
473
+ (0, timeout_1.tryCancel)();
474
+ if (!request)
475
+ return;
476
+ // Reset loadedUrl so an old one is not carried over to retries.
477
+ request.loadedUrl = undefined;
478
+ const statisticsId = request.id || request.uniqueKey;
479
+ this.stats.startJob(statisticsId);
480
+ // Shared crawling context
481
+ // @ts-expect-error It is assignable, but TS says otherwise...
482
+ const crawlingContext = {
483
+ id: (0, utilities_1.cryptoRandomObjectId)(10),
484
+ crawler: this,
485
+ log: this.log,
486
+ request,
487
+ session,
488
+ enqueueLinks: async (enqueueOptions) => {
489
+ return (0, core_1.enqueueLinks)({
490
+ ...enqueueOptions,
491
+ requestQueue: await this.getRequestQueue(),
492
+ });
493
+ },
494
+ };
495
+ this.crawlingContexts.set(crawlingContext.id, crawlingContext);
496
+ try {
497
+ await (0, timeout_1.addTimeoutToPromise)(() => this._runRequestHandler(crawlingContext), this.requestHandlerTimeoutMillis, `handleRequestFunction timed out after ${this.requestHandlerTimeoutMillis / 1000} seconds (${request.id}).`);
498
+ await this._timeoutAndRetry(() => source.markRequestHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
499
+ this.stats.finishJob(statisticsId);
500
+ this.handledRequestsCount++;
501
+ // reclaim session if request finishes successfully
502
+ session?.markGood();
503
+ }
504
+ catch (err) {
505
+ try {
506
+ await this._timeoutAndRetry(() => this._requestFunctionErrorHandler(err, crawlingContext, source), this.internalTimeoutMillis, `Handling request failure of ${request.url} (${request.id}) timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
507
+ }
508
+ catch (secondaryError) {
509
+ this.log.exception(secondaryError, 'runTaskFunction error handler threw an exception. '
510
+ + 'This places the crawler and its underlying storages into an unknown state and crawling will be terminated. '
511
+ + 'This may have happened due to an internal error of Apify\'s API or due to a misconfigured crawler. '
512
+ + 'If you are sure that there is no error in your code, selecting "Restart on error" in the actor\'s settings'
513
+ + 'will make sure that the run continues where it left off, if programmed to handle restarts correctly.');
514
+ throw secondaryError;
515
+ }
516
+ }
517
+ finally {
518
+ this.crawlingContexts.delete(crawlingContext.id);
519
+ }
520
+ }
521
+ /**
522
+ * Run async callback with given timeout and retry.
523
+ * @ignore
524
+ */
525
+ async _timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
526
+ try {
527
+ await (0, timeout_1.addTimeoutToPromise)(handler, timeout, error);
528
+ }
529
+ catch (e) {
530
+ if (retried <= maxRetries) { // we retry on any error, not just timeout
531
+ this.log.warning(`${e.message} (retrying ${retried}/${maxRetries})`);
532
+ return this._timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
533
+ }
534
+ throw e;
535
+ }
536
+ }
537
+ /**
538
+ * Returns true if either RequestList or RequestQueue have a request ready for processing.
539
+ */
540
+ async _isTaskReadyFunction() {
541
+ // First check RequestList, since it's only in memory.
542
+ const isRequestListEmpty = this.requestList ? (await this.requestList.isEmpty()) : true;
543
+ // If RequestList is not empty, task is ready, no reason to check RequestQueue.
544
+ if (!isRequestListEmpty)
545
+ return true;
546
+ // If RequestQueue is not empty, task is ready, return true, otherwise false.
547
+ return this.requestQueue ? !(await this.requestQueue.isEmpty()) : false;
548
+ }
549
+ /**
550
+ * Returns true if both RequestList and RequestQueue have all requests finished.
551
+ */
552
+ async _defaultIsFinishedFunction() {
553
+ const [isRequestListFinished, isRequestQueueFinished,] = await Promise.all([
554
+ this.requestList ? this.requestList.isFinished() : true,
555
+ this.requestQueue ? this.requestQueue.isFinished() : true,
556
+ ]);
557
+ // If both are finished, return true, otherwise return false.
558
+ return isRequestListFinished && isRequestQueueFinished;
559
+ }
560
+ /**
561
+ * Handles errors thrown by user provided handleRequestFunction()
562
+ */
563
+ async _requestFunctionErrorHandler(error, crawlingContext, source) {
564
+ const { request } = crawlingContext;
565
+ request.pushErrorMessage(error);
566
+ const shouldRetryRequest = !request.noRetry && request.retryCount < this.maxRequestRetries;
567
+ if (shouldRetryRequest) {
568
+ request.retryCount++;
569
+ const { url, retryCount, id } = request;
570
+ this.log.exception(error, 'handleRequestFunction failed, reclaiming failed request back to the list or queue', { url, retryCount, id });
571
+ await source.reclaimRequest(request);
572
+ }
573
+ else {
574
+ // If we get here, the request is either not retryable
575
+ // or failed more than retryCount times and will not be retried anymore.
576
+ // Mark the request as failed and do not retry.
577
+ this.handledRequestsCount++;
578
+ await source.markRequestHandled(request);
579
+ this.stats.failJob(request.id || request.url);
580
+ // @ts-expect-error It is assignable, but TS says otherwise...
581
+ const castedErrorContext = crawlingContext;
582
+ castedErrorContext.error = error;
583
+ await this._handleFailedRequestHandler(castedErrorContext); // This function prints an error message.
584
+ }
585
+ }
586
+ async _handleFailedRequestHandler(crawlingContext) {
587
+ if (this.failedRequestHandler) {
588
+ await this.failedRequestHandler(crawlingContext);
589
+ }
590
+ else {
591
+ const { id, url, method, uniqueKey } = crawlingContext.request;
592
+ this.log.exception(crawlingContext.error, 'Request failed and reached maximum retries', { id, url, method, uniqueKey });
593
+ }
594
+ }
595
+ /**
596
+ * Updates handledRequestsCount from possibly stored counts,
597
+ * usually after worker migration. Since one of the stores
598
+ * needs to have priority when both are present,
599
+ * it is the request queue, because generally, the request
600
+ * list will first be dumped into the queue and then left
601
+ * empty.
602
+ */
603
+ async _loadHandledRequestCount() {
604
+ if (this.requestQueue) {
605
+ this.handledRequestsCount = await this.requestQueue.handledCount();
606
+ }
607
+ else if (this.requestList) {
608
+ this.handledRequestsCount = this.requestList.handledCount();
609
+ }
610
+ }
611
+ async _executeHooks(hooks, ...args) {
612
+ if (Array.isArray(hooks) && hooks.length) {
613
+ for (const hook of hooks) {
614
+ await hook(...args);
615
+ }
616
+ }
617
+ }
618
+ /**
619
+ * Function for cleaning up after all request are processed.
620
+ * @ignore
621
+ */
622
+ async teardown() {
623
+ if (this.useSessionPool) {
624
+ await this.sessionPool.teardown();
625
+ }
626
+ }
627
+ _handlePropertyNameChange({ newProperty, newName, oldProperty, oldName, propertyKey, allowUndefined = false, }) {
628
+ if (newProperty && oldProperty) {
629
+ this.log.warning([
630
+ `Both "${newName}" and "${oldName}" were provided in the crawler options.`,
631
+ `"${oldName}" has been renamed to "${newName}", and will be removed in a future version.`,
632
+ `As such, "${newName}" will be used instead.`,
633
+ ].join('\n'));
634
+ // @ts-expect-error Assigning to possibly readonly properties
635
+ this[propertyKey] = newProperty;
636
+ }
637
+ else if (oldProperty) {
638
+ this.log.warning([
639
+ `"${oldName}" has been renamed to "${newName}", and will be removed in a future version.`,
640
+ `The provided value will be used, but you should rename "${oldName}" to "${newName}" in your crawler options.`,
641
+ ].join('\n'));
642
+ // @ts-expect-error Assigning to possibly readonly properties
643
+ this[propertyKey] = oldProperty;
644
+ }
645
+ else if (newProperty) {
646
+ // @ts-expect-error Assigning to possibly readonly properties
647
+ this[propertyKey] = newProperty;
648
+ }
649
+ else if (!allowUndefined) {
650
+ throw new ow_1.ArgumentError(`"${newName}" must be provided in the crawler options`, this.constructor);
651
+ }
652
+ }
653
+ _getCookieHeaderFromRequest(request) {
654
+ return request.headers?.Cookie ?? request.headers?.cookie ?? '';
655
+ }
656
+ }
657
+ exports.BasicCrawler = BasicCrawler;
658
+ Object.defineProperty(BasicCrawler, "optionsShape", {
659
+ enumerable: true,
660
+ configurable: true,
661
+ writable: true,
662
+ value: {
663
+ requestList: ow_1.default.optional.object.validate(core_1.validators.requestList),
664
+ requestQueue: ow_1.default.optional.object.validate(core_1.validators.requestQueue),
665
+ // Subclasses override this function instead of passing it
666
+ // in constructor, so this validation needs to apply only
667
+ // if the user creates an instance of BasicCrawler directly.
668
+ // TODO: remove .optional from requestHandler once migration period is over
669
+ requestHandler: ow_1.default.optional.function,
670
+ // TODO: remove in a future release
671
+ handleRequestFunction: ow_1.default.optional.function,
672
+ requestHandlerTimeoutSecs: ow_1.default.optional.number,
673
+ // TODO: remove in a future release
674
+ handleRequestTimeoutSecs: ow_1.default.optional.number,
675
+ failedRequestHandler: ow_1.default.optional.function,
676
+ // TODO: remove in a future release
677
+ handleFailedRequestFunction: ow_1.default.optional.function,
678
+ maxRequestRetries: ow_1.default.optional.number,
679
+ maxRequestsPerCrawl: ow_1.default.optional.number,
680
+ autoscaledPoolOptions: ow_1.default.optional.object,
681
+ sessionPoolOptions: ow_1.default.optional.object,
682
+ useSessionPool: ow_1.default.optional.boolean,
683
+ // AutoscaledPool shorthands
684
+ minConcurrency: ow_1.default.optional.number,
685
+ maxConcurrency: ow_1.default.optional.number,
686
+ // internal
687
+ log: ow_1.default.optional.object,
688
+ }
689
+ });
690
+ //# sourceMappingURL=basic-crawler.js.map