@crawlee/basic 4.0.0-beta.81 → 4.0.0-beta.83

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.
@@ -46,7 +46,13 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
46
46
  */
47
47
  requestHandler?: RequestHandler<ExtendedContext>;
48
48
  /**
49
- * Allows the user to extend the crawling context passed to the request handler with custom functionality.
49
+ * Allows the user to extend the crawling context with custom functionality (helpers, references, etc.).
50
+ *
51
+ * `extendContext` runs *before* navigation, so the returned members are visible to the
52
+ * `preNavigationHooks`, `postNavigationHooks`, and the `requestHandler` alike. As a consequence,
53
+ * the `context` passed to `extendContext` is the pre-navigation {@link CrawlingContext} and does
54
+ * **not** include navigation-dependent members (e.g. `page`, `response`, `$`, `body`). If you need
55
+ * those, use a `postNavigationHook` or the `requestHandler` instead.
50
56
  *
51
57
  * **Example usage:**
52
58
  *
@@ -66,7 +72,7 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
66
72
  * });
67
73
  * ```
68
74
  */
69
- extendContext?: (context: Context) => Awaitable<ContextExtension>;
75
+ extendContext?: (context: CrawlingContext) => Awaitable<ContextExtension>;
70
76
  /**
71
77
  * *Intended for BasicCrawler subclasses*. Prepares a context pipeline that transforms the initial crawling context into the shape given by the `Context` type parameter.
72
78
  *
@@ -405,24 +411,19 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
405
411
  * via {@link BasicCrawler.getRequestManager|`getRequestManager()`}.
406
412
  */
407
413
  protected requestManager?: IRequestManager;
414
+ /** Backs the {@link BasicCrawler.sessionPool|`sessionPool`} getter. */
415
+ private sessionPoolDep;
408
416
  /**
409
417
  * A reference to the underlying session pool that manages the crawler's {@link Session|sessions}. Typed as
410
418
  * {@link ISessionPool} so custom implementations can be plugged in via the `sessionPool` constructor option.
411
419
  */
412
- readonly sessionPool: ISessionPool;
413
- /**
414
- * Set when the crawler constructed its own {@link SessionPool} (no `sessionPool` option was provided).
415
- * Holds the same instance as `sessionPool`, but typed as the concrete class so the crawler can call
416
- * lifecycle methods (`resetStore`, `teardown`) that aren't part of {@link ISessionPool}. A user-supplied
417
- * pool is never owned and never torn down by the crawler.
418
- */
419
- private ownedSessionPool?;
420
+ get sessionPool(): ISessionPool;
420
421
  /**
421
- * Set when the crawler constructed its own request manager (no `requestManager`, `requestQueue`, or `requestList`
422
- * option was provided). The owned manager is purged (not dropped) between repeated `run()` calls.
423
- * A user-supplied manager is never purged by the crawler.
422
+ * Tracks **only** the queue the crawler opens for itself not the {@link RequestManagerTandem} that may wrap it
423
+ * around a user-supplied `requestList` so the owned-only purge between repeated `run()` calls never reaches
424
+ * through to a borrowed loader. Filled lazily in {@link BasicCrawler.openOwnedRequestQueue|`openOwnedRequestQueue()`}.
424
425
  */
425
- private ownedRequestManager?;
426
+ private ownedRequestQueue;
426
427
  /**
427
428
  * Whether the request-processing-time hint has already been forwarded to the request manager. The hint
428
429
  * derives only from `requestHandlerTimeoutMillis` (constant for the crawler's lifetime) and is raise-only,
@@ -637,7 +638,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
637
638
  getRequestQueue(): Promise<IRequestManager>;
638
639
  /**
639
640
  * Opens the default {@link RequestQueue}, applies the crawler's timeouts to it and records it as the
640
- * crawler-owned manager (so it gets purged between repeated `run()` calls).
641
+ * crawler-owned queue (so it gets purged between repeated `run()` calls).
641
642
  * @private
642
643
  */
643
644
  private openOwnedRequestQueue;
@@ -1,6 +1,6 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
- import { AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, CriticalError, Dataset, enqueueLinks, EnqueueStrategy, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, purgeDefaultStorages, RequestHandlerError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, Router, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, } from '@crawlee/core';
3
+ import { AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, CriticalError, Dataset, enqueueLinks, EnqueueStrategy, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, purgeDefaultStorages, RequestHandlerError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, Router, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, } from '@crawlee/core';
4
4
  import { FetchHttpClient } from '@crawlee/http-client';
5
5
  import { getObjectType, isAsyncIterable, isIterable, RobotsTxtFile, ROTATE_PROXY_ERRORS } from '@crawlee/utils';
6
6
  import { stringify } from 'csv-stringify/sync';
@@ -60,24 +60,21 @@ export class BasicCrawler {
60
60
  * via {@link BasicCrawler.getRequestManager|`getRequestManager()`}.
61
61
  */
62
62
  requestManager;
63
+ /** Backs the {@link BasicCrawler.sessionPool|`sessionPool`} getter. */
64
+ sessionPoolDep;
63
65
  /**
64
66
  * A reference to the underlying session pool that manages the crawler's {@link Session|sessions}. Typed as
65
67
  * {@link ISessionPool} so custom implementations can be plugged in via the `sessionPool` constructor option.
66
68
  */
67
- sessionPool;
68
- /**
69
- * Set when the crawler constructed its own {@link SessionPool} (no `sessionPool` option was provided).
70
- * Holds the same instance as `sessionPool`, but typed as the concrete class so the crawler can call
71
- * lifecycle methods (`resetStore`, `teardown`) that aren't part of {@link ISessionPool}. A user-supplied
72
- * pool is never owned and never torn down by the crawler.
73
- */
74
- ownedSessionPool;
69
+ get sessionPool() {
70
+ return this.sessionPoolDep.value;
71
+ }
75
72
  /**
76
- * Set when the crawler constructed its own request manager (no `requestManager`, `requestQueue`, or `requestList`
77
- * option was provided). The owned manager is purged (not dropped) between repeated `run()` calls.
78
- * A user-supplied manager is never purged by the crawler.
73
+ * Tracks **only** the queue the crawler opens for itself not the {@link RequestManagerTandem} that may wrap it
74
+ * around a user-supplied `requestList` so the owned-only purge between repeated `run()` calls never reaches
75
+ * through to a borrowed loader. Filled lazily in {@link BasicCrawler.openOwnedRequestQueue|`openOwnedRequestQueue()`}.
79
76
  */
80
- ownedRequestManager;
77
+ ownedRequestQueue = OwnedOrInjected.resolve();
81
78
  /**
82
79
  * Whether the request-processing-time hint has already been forwarded to the request manager. The hint
83
80
  * derives only from `requestHandlerTimeoutMillis` (constant for the crawler's lifetime) and is raise-only,
@@ -297,18 +294,12 @@ export class BasicCrawler {
297
294
  '`proxyInfo` they were created with. Configure proxies on the pool instead, ' +
298
295
  'e.g. via `addSession({ proxyInfo })` or a custom `createSessionFunction`.');
299
296
  }
300
- if (sessionPool) {
301
- this.sessionPool = sessionPool;
302
- }
303
- else {
304
- this.ownedSessionPool = new SessionPool({
305
- createSessionFunction: async (opts) => new Session({
306
- ...opts?.sessionOptions,
307
- proxyInfo: opts?.sessionOptions?.proxyInfo ?? (await this.proxyConfiguration?.newProxyInfo()),
308
- }),
309
- });
310
- this.sessionPool = this.ownedSessionPool;
311
- }
297
+ this.sessionPoolDep = OwnedOrInjected.resolve(sessionPool, () => new SessionPool({
298
+ createSessionFunction: async (opts) => new Session({
299
+ ...opts?.sessionOptions,
300
+ proxyInfo: opts?.sessionOptions?.proxyInfo ?? (await this.proxyConfiguration?.newProxyInfo()),
301
+ }),
302
+ }));
312
303
  this.blockedStatusCodes = new Set(blockedStatusCodesInput ?? BLOCKED_STATUS_CODES);
313
304
  const maxSignedInteger = 2 ** 31 - 1;
314
305
  if (this.requestHandlerTimeoutMillis > maxSignedInteger) {
@@ -507,13 +498,27 @@ export class BasicCrawler {
507
498
  return { enqueueLinks: enqueueLinksWrapper, addRequests, sendRequest };
508
499
  }
509
500
  buildFinalContextPipeline() {
510
- let contextPipeline = (this.contextPipelineOptions.contextPipelineBuilder?.() ??
501
+ const subclassPipeline = (this.contextPipelineOptions.contextPipelineBuilder?.() ??
511
502
  this.buildContextPipeline());
503
+ // `extendContext` runs *before* the subclass navigation pipeline (which includes the
504
+ // pre/post-navigation hooks). This makes the extension visible to those hooks and to the
505
+ // request handler alike. The trade-off is that `extendContext` cannot access
506
+ // navigation-dependent context members (e.g. `page`, `response`, `$`, `body`), as those
507
+ // don't exist yet at this point in the pipeline.
508
+ // The `extendContext` output (`ContextExtension`) is carried through the subclass pipeline at
509
+ // runtime (the pipeline copies each middleware's returned members onto the shared context), but
510
+ // TypeScript cannot express that `Context` transitively includes `ContextExtension` here. The
511
+ // casts below are sound because `buildFinalContextPipeline` is declared to return the fully
512
+ // resolved `ExtendedContext` (= `Context & ContextExtension`).
512
513
  const { extendContext } = this.contextPipelineOptions;
514
+ let contextPipeline;
513
515
  if (extendContext !== undefined) {
514
- contextPipeline = contextPipeline.compose({
515
- action: async (context) => await extendContext(context),
516
- });
516
+ contextPipeline = ContextPipeline.create()
517
+ .compose({ action: async (context) => await extendContext(context) })
518
+ .chain(subclassPipeline);
519
+ }
520
+ else {
521
+ contextPipeline = subclassPipeline;
517
522
  }
518
523
  contextPipeline = contextPipeline.compose({
519
524
  action: async (context) => {
@@ -620,17 +625,17 @@ export class BasicCrawler {
620
625
  // we need to purge the RQ to allow processing the same requests again — this is important so users can
621
626
  // pass in failed requests back to the `crawler.run()`, otherwise they would be considered as handled and
622
627
  // ignored — as a failed request is still handled.
623
- // By default (purgeRequestQueue unset or true), only the manager we created ourselves (ownedRequestManager) is purged.
628
+ // By default (`purgeRequestQueue` unset), only the queue we opened ourselves is purged.
624
629
  // When `purgeRequestQueue` is explicitly `true`, we also purge a user-supplied manager.
625
630
  // When `purgeRequestQueue` is explicitly `false`, nothing is purged.
626
631
  const shouldPurge = purgeRequestQueue !== false;
627
- const managerToPurge = this.ownedRequestManager ?? (purgeRequestQueue === true ? this.requestManager : undefined);
632
+ const managerToPurge = this.ownedRequestQueue.maybeValue ?? (purgeRequestQueue === true ? this.requestManager : undefined);
628
633
  if (managerToPurge?.purge && shouldPurge) {
629
634
  await managerToPurge.purge();
630
635
  }
631
636
  this.stats.reset();
632
637
  await this.stats.resetStore();
633
- await this.ownedSessionPool?.resetStore();
638
+ await this.sessionPoolDep.ifOwned((pool) => pool.resetStore());
634
639
  }
635
640
  this.unexpectedStop = false;
636
641
  this.running = true;
@@ -742,7 +747,7 @@ export class BasicCrawler {
742
747
  }
743
748
  /**
744
749
  * Opens the default {@link RequestQueue}, applies the crawler's timeouts to it and records it as the
745
- * crawler-owned manager (so it gets purged between repeated `run()` calls).
750
+ * crawler-owned queue (so it gets purged between repeated `run()` calls).
746
751
  * @private
747
752
  */
748
753
  async openOwnedRequestQueue() {
@@ -750,8 +755,7 @@ export class BasicCrawler {
750
755
  // subsequent instances get their own queue via a unique alias so they don't collide.
751
756
  const identifier = this.identity.instanceIndex === 0 ? null : { alias: `__default_${this.identity.id}__` };
752
757
  const requestQueue = await RequestQueue.open(identifier, { config: serviceLocator.getConfiguration() });
753
- this.ownedRequestManager = requestQueue;
754
- return requestQueue;
758
+ return this.ownedRequestQueue.set(requestQueue);
755
759
  }
756
760
  /**
757
761
  * Tells a request manager how long we expect to hold a fetched request, so that one backed by a
@@ -1350,7 +1354,7 @@ export class BasicCrawler {
1350
1354
  if (this._closeEvents) {
1351
1355
  await serviceLocator.getEventManager().close();
1352
1356
  }
1353
- await this.ownedSessionPool?.teardown();
1357
+ await this.sessionPoolDep.ifOwned((pool) => pool.teardown());
1354
1358
  await this.autoscaledPool?.abort();
1355
1359
  }
1356
1360
  _getCookieHeaderFromRequest(request) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/basic",
3
- "version": "4.0.0-beta.81",
3
+ "version": "4.0.0-beta.83",
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.3.2",
44
44
  "@apify/utilities": "^2.15.5",
45
- "@crawlee/core": "4.0.0-beta.81",
46
- "@crawlee/http-client": "4.0.0-beta.81",
47
- "@crawlee/types": "4.0.0-beta.81",
48
- "@crawlee/utils": "4.0.0-beta.81",
45
+ "@crawlee/core": "4.0.0-beta.83",
46
+ "@crawlee/http-client": "4.0.0-beta.83",
47
+ "@crawlee/types": "4.0.0-beta.83",
48
+ "@crawlee/utils": "4.0.0-beta.83",
49
49
  "csv-stringify": "^6.5.2",
50
50
  "fs-extra": "^11.3.0",
51
51
  "ow": "^2.0.0",
@@ -54,7 +54,7 @@
54
54
  "type-fest": "^4.41.0"
55
55
  },
56
56
  "optionalDependencies": {
57
- "@crawlee/impit-client": "^4.0.0-beta.81"
57
+ "@crawlee/impit-client": "^4.0.0-beta.83"
58
58
  },
59
59
  "lerna": {
60
60
  "command": {
@@ -63,5 +63,5 @@
63
63
  }
64
64
  }
65
65
  },
66
- "gitHead": "80dc6b4fc82237e63a51a71153809ec8dfd0cc50"
66
+ "gitHead": "5f25b90c4914d7e097732cf761e440aeba799137"
67
67
  }