@crawlee/playwright 4.0.0-beta.241 → 4.0.0-beta.242
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { BrowserHook, LoadedRequest,
|
|
1
|
+
import type { BrowserHook, LoadedRequest, RouterHandler, RouteSchemas, RoutesFromSchemas } from '@crawlee/browser';
|
|
2
2
|
import type { BasicCrawlerOptions, CrawlingContext, EnqueueLinksOptions, GetUserDataFromRequest, RouterRoutes } from '@crawlee/basic';
|
|
3
3
|
import { BasicCrawler } from '@crawlee/basic';
|
|
4
|
+
import { CrawlingRequest } from '@crawlee/browser';
|
|
4
5
|
import type { StorageTransactionView } from '@crawlee/core';
|
|
5
6
|
import type { Dictionary, Awaitable } from '@crawlee/types';
|
|
6
7
|
import { type Cheerio, type CheerioAPI } from 'cheerio';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
3
|
import { BasicCrawler, ContextPipeline, ContextPipelineInitializationError, ContextPipelineInterruptedError, navigationDeadlineKey, RequestHandlerError, resolveBaseUrlForEnqueueLinksFiltering, Router, Statistics, } from '@crawlee/basic';
|
|
4
|
-
import { extractUrlsFromPage } from '@crawlee/browser';
|
|
4
|
+
import { CrawlingRequest, extractUrlsFromPage } from '@crawlee/browser';
|
|
5
5
|
import { CheerioCrawler } from '@crawlee/cheerio';
|
|
6
6
|
import { ResponseWithUrl } from '@crawlee/http-client';
|
|
7
7
|
import { createStorageTransaction, EnqueueStrategy, OwnedOrInjected } from '@crawlee/core';
|
|
8
|
-
import { extractUrlsFromCheerio, parseArgument } from '@crawlee/utils/internal';
|
|
8
|
+
import { extractUrlsFromCheerio, MutationJournal, parseArgument } from '@crawlee/utils/internal';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
11
11
|
import { PlaywrightCrawler } from './playwright-crawler.js';
|
|
@@ -267,10 +267,13 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
267
267
|
/**
|
|
268
268
|
* Runs one request handler attempt inside its own {@link StorageTransaction}, wrapping the inner
|
|
269
269
|
* (static or browser) context pipeline. The transaction is pushed to `transactions` *at creation
|
|
270
|
-
* time, before the `try
|
|
271
|
-
*
|
|
270
|
+
* time, before the `try`*, as failed attempts are routine here. The caller owns the outcome and
|
|
271
|
+
* disposal, including closing or reverting the {@link ContextModifications}.
|
|
272
|
+
*
|
|
273
|
+
* `request` defaults to the context's; a detection rerun passes a copy so it sees the request as it
|
|
274
|
+
* was before the browser attempt.
|
|
272
275
|
*/
|
|
273
|
-
async #crawlOne(renderingType, context, useStateFunction, transactions) {
|
|
276
|
+
async #crawlOne(renderingType, context, useStateFunction, transactions, request = context.request) {
|
|
274
277
|
const transaction = createStorageTransaction({
|
|
275
278
|
policy: this.#attemptWritePolicy,
|
|
276
279
|
commitTimeoutMillis: this.internalTimeoutMillis,
|
|
@@ -278,9 +281,14 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
278
281
|
transactions.push(transaction);
|
|
279
282
|
const logs = [];
|
|
280
283
|
const deferredCleanup = [];
|
|
284
|
+
const contextModifications = {
|
|
285
|
+
request: new MutationJournal(),
|
|
286
|
+
state: new MutationJournal(),
|
|
287
|
+
};
|
|
281
288
|
const subCrawlerContext = Object.defineProperties({}, Object.getOwnPropertyDescriptors(context));
|
|
282
289
|
const attemptBoundContextHelpers = {
|
|
283
|
-
|
|
290
|
+
request: contextModifications.request.track(request),
|
|
291
|
+
useState: async (defaultValue) => contextModifications.state.track(await useStateFunction(defaultValue)),
|
|
284
292
|
log: this.#createLogProxy(context.log, logs),
|
|
285
293
|
registerDeferredCleanup: (cleanup) => deferredCleanup.push(cleanup),
|
|
286
294
|
// The sub-crawler pipeline keeps this attempt's navigation deadline on `subCrawlerContext`, but the
|
|
@@ -315,10 +323,10 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
315
323
|
const routeTimeoutSecs = this.requestHandler.getTimeoutSecs?.(context.request.label);
|
|
316
324
|
const timeoutMillis = routeTimeoutSecs === undefined ? this.#individualRequestHandlerTimeoutMillis : routeTimeoutSecs * 1000;
|
|
317
325
|
await addTimeoutToPromise(async () => transaction.run(callAdaptiveRequestHandler), timeoutMillis, 'Request handler timed out');
|
|
318
|
-
return {
|
|
326
|
+
return { ok: true, transaction, contextModifications, logs };
|
|
319
327
|
}
|
|
320
328
|
catch (error) {
|
|
321
|
-
return {
|
|
329
|
+
return { ok: false, error, transaction, contextModifications, logs };
|
|
322
330
|
}
|
|
323
331
|
finally {
|
|
324
332
|
await Promise.all(deferredCleanup.map((cleanup) => cleanup()));
|
|
@@ -342,10 +350,12 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
342
350
|
crawlingContext.log.debug(`Running HTTP-only request handler for ${crawlingContext.request.url}`);
|
|
343
351
|
this.statistics.state.httpOnlyRequestHandlerRuns++;
|
|
344
352
|
const plainHTTPRun = await this.#crawlOne('static', crawlingContext, crawlingContext.useState, transactions);
|
|
345
|
-
if (plainHTTPRun.ok &&
|
|
353
|
+
if (plainHTTPRun.ok &&
|
|
354
|
+
this.#resultChecker(plainHTTPRun.transaction, { request: crawlingContext.request })) {
|
|
346
355
|
crawlingContext.log.debug(`HTTP-only request handler succeeded for ${crawlingContext.request.url}`);
|
|
347
|
-
plainHTTPRun.logs
|
|
348
|
-
|
|
356
|
+
plainHTTPRun.logs.forEach(([log, method, ...args]) => log[method](...args));
|
|
357
|
+
this.#keepContextModifications(plainHTTPRun.contextModifications);
|
|
358
|
+
await plainHTTPRun.transaction.commit();
|
|
349
359
|
return;
|
|
350
360
|
}
|
|
351
361
|
// Execution will "fall through" and try running the request handler in a browser
|
|
@@ -356,6 +366,7 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
356
366
|
: plainHTTPRun.error;
|
|
357
367
|
if (actualError instanceof ContextPipelineInterruptedError ||
|
|
358
368
|
(await this.#shouldPropagateError(actualError, crawlingContext))) {
|
|
369
|
+
this.#keepContextModifications(plainHTTPRun.contextModifications);
|
|
359
370
|
throw actualError;
|
|
360
371
|
}
|
|
361
372
|
crawlingContext.log.exception(actualError, `HTTP-only request handler failed for ${crawlingContext.request.url}`);
|
|
@@ -365,35 +376,20 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
365
376
|
this.statistics.state.renderingTypeMispredictions++;
|
|
366
377
|
httpResultRejected = true;
|
|
367
378
|
}
|
|
379
|
+
// The browser attempt starts from the request and state as they were before the rejected one.
|
|
380
|
+
this.#discardContextModifications(plainHTTPRun.contextModifications, crawlingContext);
|
|
368
381
|
}
|
|
369
382
|
crawlingContext.log.debug(`Running browser request handler for ${crawlingContext.request.url}`);
|
|
370
383
|
this.statistics.state.browserRequestHandlerRuns++;
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
const stateTracker = {
|
|
376
|
-
stateCopy: null,
|
|
377
|
-
async getLiveState(defaultValue = {}) {
|
|
378
|
-
const state = await crawlingContext.useState(defaultValue);
|
|
379
|
-
if (this.stateCopy === null) {
|
|
380
|
-
this.stateCopy = JSON.parse(JSON.stringify(state));
|
|
381
|
-
}
|
|
382
|
-
return state;
|
|
383
|
-
},
|
|
384
|
-
async getStateCopy(defaultValue = {}) {
|
|
385
|
-
if (this.stateCopy === null) {
|
|
386
|
-
return defaultValue;
|
|
387
|
-
}
|
|
388
|
-
return this.stateCopy;
|
|
389
|
-
},
|
|
390
|
-
};
|
|
391
|
-
const browserRun = await this.#crawlOne('clientOnly', crawlingContext, stateTracker.getLiveState.bind(stateTracker), transactions);
|
|
384
|
+
const browserRun = await this.#crawlOne('clientOnly', crawlingContext, crawlingContext.useState, transactions);
|
|
385
|
+
// A failed browser attempt is handed to the retry logic with its writes - `userData` stashed for the
|
|
386
|
+
// next attempt must survive.
|
|
387
|
+
this.#keepContextModifications(browserRun.contextModifications);
|
|
392
388
|
if (!browserRun.ok) {
|
|
393
389
|
throw browserRun.error;
|
|
394
390
|
}
|
|
395
|
-
browserRun.logs
|
|
396
|
-
await browserRun.
|
|
391
|
+
browserRun.logs.forEach(([log, method, ...args]) => log[method](...args));
|
|
392
|
+
await browserRun.transaction.commit();
|
|
397
393
|
// Past the shutdown fence, `teardown()` has already drained - and possibly torn down the predictor.
|
|
398
394
|
if (httpResultRejected && !this.#shutDown) {
|
|
399
395
|
this.#storeRenderingType(crawlingContext.request, 'clientOnly');
|
|
@@ -402,13 +398,17 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
402
398
|
const detectionPromise = (async () => {
|
|
403
399
|
crawlingContext.log.debug(`Detecting rendering type for ${crawlingContext.request.url}`);
|
|
404
400
|
// The detection attempt's transaction is never committed - its writes exist only for the
|
|
405
|
-
// result comparison.
|
|
406
|
-
|
|
401
|
+
// result comparison. It runs on copies of the request and the crawler state with the browser
|
|
402
|
+
// attempt's writes undone, so both attempts start from the same conditions, and the copies go
|
|
403
|
+
// away with it.
|
|
404
|
+
const { request: requestJournal, state: stateJournal } = browserRun.contextModifications;
|
|
405
|
+
let stateCopy;
|
|
406
|
+
const plainHTTPRun = await this.#crawlOne('static', crawlingContext, async (defaultValue) => (stateCopy ??= stateJournal.undoOnto(JSON.parse(JSON.stringify(await crawlingContext.useState(defaultValue))))), transactions, requestJournal.undoOnto(CrawlingRequest.fromSchema(JSON.parse(JSON.stringify(crawlingContext.request)))));
|
|
407
407
|
const detectionResult = (() => {
|
|
408
408
|
if (!plainHTTPRun.ok) {
|
|
409
409
|
return 'clientOnly';
|
|
410
410
|
}
|
|
411
|
-
const comparisonResult = this.#resultComparator(plainHTTPRun.
|
|
411
|
+
const comparisonResult = this.#resultComparator(plainHTTPRun.transaction, browserRun.transaction, {
|
|
412
412
|
request: crawlingContext.request,
|
|
413
413
|
});
|
|
414
414
|
if (comparisonResult === true || comparisonResult === 'equal') {
|
|
@@ -435,6 +435,22 @@ export class AdaptivePlaywrightCrawler extends BasicCrawler {
|
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
|
+
#keepContextModifications(modifications) {
|
|
439
|
+
modifications.request.close();
|
|
440
|
+
modifications.state.close();
|
|
441
|
+
}
|
|
442
|
+
#discardContextModifications(modifications, { request, log }) {
|
|
443
|
+
// Nothing else writes to a request while its handler runs, so a conflict means a write that bypassed
|
|
444
|
+
// the context (e.g. through a stashed reference) - it stays, but it is not what the retry logic expects.
|
|
445
|
+
modifications.request.revert({
|
|
446
|
+
onConflict: (key) => log.warning(`Request ${request.url}: '${String(key)}' was modified outside the request handler`),
|
|
447
|
+
});
|
|
448
|
+
// The state is shared with concurrent handlers; a conflict is one of them having written the same
|
|
449
|
+
// property meanwhile. Their value wins, and this attempt's write stays with it.
|
|
450
|
+
modifications.state.revert({
|
|
451
|
+
onConflict: (key) => log.debug(`Crawler state '${String(key)}' was changed concurrently; keeping the newer value`),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
438
454
|
#storeRenderingType(request, renderingType) {
|
|
439
455
|
// Deliberately not awaited: a predictor that persists asynchronously gets to keep batching its writes,
|
|
440
456
|
// and `drainRenderingDetections()` catches whatever is still pending.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/playwright",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.242",
|
|
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.13.0"
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@apify/datastructures": "^3.0.1",
|
|
51
51
|
"@apify/timeout": "^1.0.1",
|
|
52
|
-
"@crawlee/basic": "4.0.0-beta.
|
|
53
|
-
"@crawlee/browser": "4.0.0-beta.
|
|
54
|
-
"@crawlee/browser-pool": "4.0.0-beta.
|
|
55
|
-
"@crawlee/cheerio": "4.0.0-beta.
|
|
56
|
-
"@crawlee/core": "4.0.0-beta.
|
|
57
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
58
|
-
"@crawlee/types": "4.0.0-beta.
|
|
59
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
52
|
+
"@crawlee/basic": "4.0.0-beta.242",
|
|
53
|
+
"@crawlee/browser": "4.0.0-beta.242",
|
|
54
|
+
"@crawlee/browser-pool": "4.0.0-beta.242",
|
|
55
|
+
"@crawlee/cheerio": "4.0.0-beta.242",
|
|
56
|
+
"@crawlee/core": "4.0.0-beta.242",
|
|
57
|
+
"@crawlee/http-client": "4.0.0-beta.242",
|
|
58
|
+
"@crawlee/types": "4.0.0-beta.242",
|
|
59
|
+
"@crawlee/utils": "4.0.0-beta.242",
|
|
60
60
|
"cheerio": "^1.0.0",
|
|
61
61
|
"domhandler": "^6.0.0",
|
|
62
62
|
"jquery": "^3.7.1",
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
|
-
"gitHead": "
|
|
84
|
+
"gitHead": "9bd381d862366b6b874938fb0717f21fd430b9de"
|
|
85
85
|
}
|