@crawlee/playwright 4.0.0-beta.162 → 4.0.0-beta.164

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.
@@ -198,6 +198,24 @@ export declare class AdaptivePlaywrightCrawler<ContextExtension = Dictionary<nev
198
198
  protected runRequestHandler(crawlingContext: CrawlingContext): Promise<void>;
199
199
  private enqueueLinks;
200
200
  private createLogProxy;
201
+ /**
202
+ * Number of rendering type detections that have not settled yet, including results the predictor is
203
+ * still persisting.
204
+ */
205
+ get inFlightRenderingTypeDetectionCount(): number;
206
+ /**
207
+ * Waits for in-flight rendering type detections to settle, bounded by `timeoutMillis` (defaults to the
208
+ * internal timeout).
209
+ */
210
+ drainRenderingDetections({ timeoutMillis }?: {
211
+ timeoutMillis?: number;
212
+ }): Promise<void>;
213
+ /**
214
+ * Stops the crawler immediately, but not before rendering type detections already under way (and results
215
+ * the predictor is still persisting) have settled - see
216
+ * {@link AdaptivePlaywrightCrawler.drainRenderingDetections|`drainRenderingDetections()`}. Requests
217
+ * that are still running are not waited for, unlike {@link BasicCrawler.stop|`stop()`}.
218
+ */
201
219
  teardown(): Promise<void>;
202
220
  }
203
221
  export declare function createAdaptivePlaywrightRouter<Context extends AdaptivePlaywrightCrawlerContext = AdaptivePlaywrightCrawlerContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
@@ -1,3 +1,4 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
1
2
  import { isDeepStrictEqual } from 'node:util';
2
3
  import { BasicCrawler } from '@crawlee/basic';
3
4
  import { extractUrlsFromPage } from '@crawlee/browser';
@@ -76,6 +77,14 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
76
77
  * a discarded attempt's enqueues must never reach the queue.
77
78
  */
78
79
  #attemptWritePolicy;
80
+ /**
81
+ * In-flight rendering type detections, plus the pending results of an asynchronous `storeResult`.
82
+ */
83
+ #activeDetections = new Set();
84
+ /**
85
+ * Set once `teardown()` starts, so that requests still in the pool stop opening new detections.
86
+ */
87
+ #shutDown = false;
79
88
  #teardownHooks = [];
80
89
  constructor(options = {}) {
81
90
  const { requestHandler, renderingTypeDetectionRatio = 0.1, renderingTypePredictor, resultChecker, shouldPropagateError, resultComparator, statistics, requestHandlerTimeoutSecs = 60, errorHandler, failedRequestHandler, preNavigationHooks = [], postNavigationHooks = [], extendContext, contextPipelineBuilder, transactionalStorage, launchContext, headless, browserPool, remoteBrowser, ...rest } = options;
@@ -180,6 +189,8 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
180
189
  });
181
190
  }
182
191
  async init() {
192
+ // A crawler can be run again after a teardown.
193
+ this.#shutDown = false;
183
194
  // Only the predictor we built ourselves is ours to initialize - an injected one is borrowed, so its
184
195
  // lifecycle (including restoring persisted state) stays with whoever created it.
185
196
  await this.#renderingTypePredictor.ifOwned((predictor) => predictor.initialize());
@@ -314,8 +325,8 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
314
325
  }
315
326
  }
316
327
  async runRequestHandler(crawlingContext) {
317
- const renderingTypePrediction = this.#renderingTypePredictor.value.predict(crawlingContext.request);
318
- const shouldDetectRenderingType = Math.random() < renderingTypePrediction.detectionProbabilityRecommendation;
328
+ const renderingTypePrediction = await this.#renderingTypePredictor.value.predict(crawlingContext.request);
329
+ const shouldDetectRenderingType = !this.#shutDown && Math.random() < renderingTypePrediction.detectionProbabilityRecommendation;
319
330
  if (!shouldDetectRenderingType) {
320
331
  crawlingContext.log.debug(`Predicted rendering type ${renderingTypePrediction.renderingType} for ${crawlingContext.request.url}`);
321
332
  }
@@ -378,27 +389,36 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
378
389
  browserRun.logs?.forEach(([log, method, ...args]) => log[method](...args));
379
390
  await browserRun.result.commit();
380
391
  if (shouldDetectRenderingType) {
381
- crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
382
- // The detection attempt's transaction is never committed - its writes exist only for the
383
- // result comparison.
384
- const plainHTTPRun = await this.crawlOne('static', crawlingContext, stateTracker.getStateCopy.bind(stateTracker), transactions);
385
- const detectionResult = (() => {
386
- if (!plainHTTPRun.ok) {
387
- return 'clientOnly';
388
- }
389
- const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result);
390
- if (comparisonResult === true || comparisonResult === 'equal') {
391
- return 'static';
392
- }
393
- if (comparisonResult === false || comparisonResult === 'different') {
394
- return 'clientOnly';
392
+ const detectionPromise = (async () => {
393
+ crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
394
+ // The detection attempt's transaction is never committed - its writes exist only for the
395
+ // result comparison.
396
+ const plainHTTPRun = await this.crawlOne('static', crawlingContext, stateTracker.getStateCopy.bind(stateTracker), transactions);
397
+ const detectionResult = (() => {
398
+ if (!plainHTTPRun.ok) {
399
+ return 'clientOnly';
400
+ }
401
+ const comparisonResult = this.#resultComparator(plainHTTPRun.result, browserRun.result);
402
+ if (comparisonResult === true || comparisonResult === 'equal') {
403
+ return 'static';
404
+ }
405
+ if (comparisonResult === false || comparisonResult === 'different') {
406
+ return 'clientOnly';
407
+ }
408
+ return undefined;
409
+ })();
410
+ crawlingContext.log.debug(`Detected rendering type ${detectionResult} for ${crawlingContext.request.url}`);
411
+ if (detectionResult !== undefined) {
412
+ // Deliberately not awaited: a predictor that persists asynchronously gets to keep
413
+ // batching its writes, and the drain below catches whatever is still pending.
414
+ const stored = this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
415
+ if (stored !== undefined) {
416
+ // Nothing downstream awaits this, so a failed write would otherwise be silent.
417
+ void this.#trackDetection(Promise.resolve(stored).catch((error) => this.log.exception(error, `Failed to store the rendering type detection result for ${crawlingContext.request.url}`)));
418
+ }
395
419
  }
396
- return undefined;
397
420
  })();
398
- crawlingContext.log.debug(`Detected rendering type ${detectionResult} for ${crawlingContext.request.url}`);
399
- if (detectionResult !== undefined) {
400
- this.#renderingTypePredictor.value.storeResult(crawlingContext.request, detectionResult);
401
- }
421
+ await this.#trackDetection(detectionPromise);
402
422
  }
403
423
  }
404
424
  finally {
@@ -443,7 +463,66 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
443
463
  },
444
464
  });
445
465
  }
466
+ #trackDetection(promise) {
467
+ this.#activeDetections.add(promise);
468
+ // Not `finally()`: the promise it derives would reject on its own and go unhandled. A rejection here
469
+ // belongs to whoever awaits the original, or to `allSettled` in the drain.
470
+ void promise.catch(() => { }).then(() => this.#activeDetections.delete(promise));
471
+ return promise;
472
+ }
473
+ /**
474
+ * Number of rendering type detections that have not settled yet, including results the predictor is
475
+ * still persisting.
476
+ */
477
+ get inFlightRenderingTypeDetectionCount() {
478
+ return this.#activeDetections.size;
479
+ }
480
+ /**
481
+ * Waits for in-flight rendering type detections to settle, bounded by `timeoutMillis` (defaults to the
482
+ * internal timeout).
483
+ */
484
+ async drainRenderingDetections({ timeoutMillis } = {}) {
485
+ if (this.#activeDetections.size === 0) {
486
+ return;
487
+ }
488
+ const drained = (async () => {
489
+ while (this.#activeDetections.size > 0) {
490
+ await Promise.allSettled(Array.from(this.#activeDetections));
491
+ }
492
+ })();
493
+ const millis = timeoutMillis ?? this.internalTimeoutMillis;
494
+ // A caller opting out of the bound would otherwise get an immediate spurious timeout - `setTimeout`
495
+ // clamps a non-finite delay to 1ms.
496
+ if (!Number.isFinite(millis)) {
497
+ await drained;
498
+ return;
499
+ }
500
+ const abortTimer = new AbortController();
501
+ try {
502
+ const outcome = await Promise.race([
503
+ drained.then(() => 'drained'),
504
+ delay(millis, 'timedOut', { signal: abortTimer.signal }).catch(() => 'aborted'),
505
+ ]);
506
+ if (outcome === 'timedOut') {
507
+ this.log.warning(`Timed out after ${millis / 1e3} seconds waiting for ${this.#activeDetections.size} rendering type detection(s) to settle - their results may be lost.`);
508
+ }
509
+ }
510
+ finally {
511
+ abortTimer.abort();
512
+ }
513
+ }
514
+ /**
515
+ * Stops the crawler immediately, but not before rendering type detections already under way (and results
516
+ * the predictor is still persisting) have settled - see
517
+ * {@link AdaptivePlaywrightCrawler.drainRenderingDetections|`drainRenderingDetections()`}. Requests
518
+ * that are still running are not waited for, unlike {@link BasicCrawler.stop|`stop()`}.
519
+ */
446
520
  async teardown() {
521
+ // Called from outside `run()` - under `keepAlive`, say - the pool keeps dispatching until
522
+ // `super.teardown()` aborts it, and a request starting during the drain would open a detection the
523
+ // drain has already passed. Closing that first makes the drain a fence.
524
+ this.#shutDown = true;
525
+ await this.drainRenderingDetections();
447
526
  await super.teardown();
448
527
  // Mirrors the owned-only `initialize()` in `init()` - without this, the predictor we built keeps its
449
528
  // PERSIST_STATE listener registered after the crawl and never gets a final write.
@@ -1,4 +1,5 @@
1
1
  import type { RecoverableStatePersistenceOptions, Request } from '@crawlee/core';
2
+ import type { Awaitable } from '@crawlee/types';
2
3
  export type RenderingType = 'clientOnly' | 'static';
3
4
  export interface RenderingTypePredictorOptions {
4
5
  /** A number between 0 and 1 that determines the desired ratio of rendering type detections */
@@ -12,13 +13,24 @@ export interface RenderingTypePredictorOptions {
12
13
  * @experimental
13
14
  */
14
15
  export interface IRenderingTypePredictor {
15
- /** Predict the rendering type for a request, and how likely the crawler should be to verify it. */
16
- predict(request: Request): {
16
+ /**
17
+ * Predict the rendering type for a request, and how likely the crawler should be to verify it.
18
+ *
19
+ * Called once per request, before navigation - the crawler awaits it, so prefer loading whatever the
20
+ * prediction needs up front over per-request I/O.
21
+ */
22
+ predict(request: Request): Awaitable<{
17
23
  renderingType: RenderingType;
18
24
  detectionProbabilityRecommendation: number;
19
- };
20
- /** Report a detected rendering type, so that future predictions can take it into account. */
21
- storeResult(requests: Request | Request[], renderingType: RenderingType): void;
25
+ }>;
26
+ /**
27
+ * Report a detected rendering type, so that future predictions can take it into account.
28
+ *
29
+ * The crawler does not await this per detection - it collects the returned promises and drains them in
30
+ * `teardown()`, so an implementation that persists results asynchronously can keep batching its writes
31
+ * and still be sure they land before the crawl ends.
32
+ */
33
+ storeResult(requests: Request | Request[], renderingType: RenderingType): Awaitable<void>;
22
34
  }
23
35
  /**
24
36
  * Stores rendering type information for previously crawled URLs and predicts the rendering type for URLs that have yet to be crawled and recommends when rendering type detection should be performed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/playwright",
3
- "version": "4.0.0-beta.162",
3
+ "version": "4.0.0-beta.164",
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"
@@ -49,13 +49,13 @@
49
49
  "dependencies": {
50
50
  "@apify/datastructures": "^2.0.3",
51
51
  "@apify/timeout": "^0.4.4",
52
- "@crawlee/basic": "4.0.0-beta.162",
53
- "@crawlee/browser": "4.0.0-beta.162",
54
- "@crawlee/browser-pool": "4.0.0-beta.162",
55
- "@crawlee/cheerio": "4.0.0-beta.162",
56
- "@crawlee/core": "4.0.0-beta.162",
57
- "@crawlee/types": "4.0.0-beta.162",
58
- "@crawlee/utils": "4.0.0-beta.162",
52
+ "@crawlee/basic": "4.0.0-beta.164",
53
+ "@crawlee/browser": "4.0.0-beta.164",
54
+ "@crawlee/browser-pool": "4.0.0-beta.164",
55
+ "@crawlee/cheerio": "4.0.0-beta.164",
56
+ "@crawlee/core": "4.0.0-beta.164",
57
+ "@crawlee/types": "4.0.0-beta.164",
58
+ "@crawlee/utils": "4.0.0-beta.164",
59
59
  "cheerio": "^1.0.0",
60
60
  "jquery": "^3.7.1",
61
61
  "ml-logistic-regression": "^2.0.0",
@@ -80,5 +80,5 @@
80
80
  }
81
81
  }
82
82
  },
83
- "gitHead": "0896509b1e3df39639d2224b2d565fc592939fd7"
83
+ "gitHead": "a2e4e2a3aa3a364c8a44fd2e49ca57f500d2c01f"
84
84
  }