@crawlee/basic 4.0.0-beta.99 → 4.0.0-rc.1
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.
- package/README.md +1 -1
- package/index.d.ts +11 -1
- package/index.js +11 -0
- package/internals/autoscaling/autoscaled_pool.d.ts +178 -0
- package/internals/autoscaling/autoscaled_pool.js +387 -0
- package/internals/autoscaling/concurrency_system.d.ts +235 -0
- package/internals/autoscaling/concurrency_system.js +357 -0
- package/internals/autoscaling/cpu_load_signal.d.ts +43 -0
- package/internals/autoscaling/cpu_load_signal.js +46 -0
- package/internals/autoscaling/event_loop_load_signal.d.ts +51 -0
- package/internals/autoscaling/event_loop_load_signal.js +60 -0
- package/internals/autoscaling/index.d.ts +9 -0
- package/internals/autoscaling/index.js +9 -0
- package/internals/autoscaling/load_signal.d.ts +98 -0
- package/internals/autoscaling/load_signal.js +103 -0
- package/internals/autoscaling/memory_load_signal.d.ts +45 -0
- package/internals/autoscaling/memory_load_signal.js +104 -0
- package/internals/autoscaling/snapshotter.d.ts +84 -0
- package/internals/autoscaling/snapshotter.js +67 -0
- package/internals/autoscaling/storage_backend_load_signal.d.ts +56 -0
- package/internals/autoscaling/storage_backend_load_signal.js +73 -0
- package/internals/autoscaling/system_status.d.ts +112 -0
- package/internals/autoscaling/system_status.js +139 -0
- package/internals/autoscaling/weighted_avg.d.ts +5 -0
- package/internals/autoscaling/weighted_avg.js +14 -0
- package/internals/basic-crawler.d.ts +185 -389
- package/internals/basic-crawler.js +982 -684
- package/internals/cookie_utils.d.ts +40 -0
- package/internals/cookie_utils.js +122 -0
- package/internals/crawler-run.d.ts +76 -0
- package/internals/crawler-run.js +142 -0
- package/internals/crawlers/context_pipeline.d.ts +75 -0
- package/internals/crawlers/context_pipeline.js +109 -0
- package/internals/crawlers/crawler_commons.d.ts +216 -0
- package/internals/crawlers/crawler_commons.js +1 -0
- package/internals/crawlers/error_snapshotter.d.ts +29 -0
- package/internals/crawlers/error_snapshotter.js +116 -0
- package/internals/crawlers/error_tracker.d.ts +51 -0
- package/internals/crawlers/error_tracker.js +316 -0
- package/internals/crawlers/index.d.ts +4 -0
- package/internals/crawlers/index.js +3 -0
- package/internals/crawlers/internals/types.d.ts +7 -0
- package/internals/crawlers/internals/types.js +1 -0
- package/internals/crawlers/statistics.d.ts +316 -0
- package/internals/crawlers/statistics.js +525 -0
- package/internals/crawling_request.d.ts +86 -0
- package/internals/crawling_request.js +127 -0
- package/internals/enqueue_links/enqueue_links.d.ts +152 -0
- package/internals/enqueue_links/enqueue_links.js +76 -0
- package/internals/enqueue_links/index.d.ts +3 -0
- package/internals/enqueue_links/index.js +3 -0
- package/internals/enqueue_links/shared.d.ts +106 -0
- package/internals/enqueue_links/shared.js +258 -0
- package/internals/errors.d.ts +68 -0
- package/internals/errors.js +83 -0
- package/internals/http.d.ts +9 -0
- package/internals/http.js +28 -0
- package/internals/request-timeout.d.ts +6 -0
- package/internals/request-timeout.js +27 -2
- package/internals/router.d.ts +299 -0
- package/internals/router.js +312 -0
- package/internals/send-request.d.ts +2 -1
- package/internals/send-request.js +1 -0
- package/internals/session_pool/consts.d.ts +1 -0
- package/internals/session_pool/consts.js +1 -0
- package/internals/session_pool/errors.d.ts +7 -0
- package/internals/session_pool/errors.js +11 -0
- package/internals/session_pool/fingerprint.d.ts +9 -0
- package/internals/session_pool/fingerprint.js +30 -0
- package/internals/session_pool/index.d.ts +4 -0
- package/internals/session_pool/index.js +4 -0
- package/internals/session_pool/session.d.ts +151 -0
- package/internals/session_pool/session.js +225 -0
- package/internals/session_pool/session_pool.d.ts +193 -0
- package/internals/session_pool/session_pool.js +453 -0
- package/internals/sitemap_request_loader.d.ts +158 -0
- package/internals/sitemap_request_loader.js +432 -0
- package/internals/throttling_request_manager.d.ts +229 -0
- package/internals/throttling_request_manager.js +843 -0
- package/package.json +17 -14
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Cookie as CookieObject } from '@crawlee/types';
|
|
2
|
+
import { Cookie } from 'tough-cookie';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export declare function getCookiesFromResponse(response: Response): Cookie[];
|
|
7
|
+
/**
|
|
8
|
+
* Calculate cookie expiration date
|
|
9
|
+
* @param maxAgeSecs
|
|
10
|
+
* @returns Calculated date by session max age seconds.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function getDefaultCookieExpirationDate(maxAgeSecs: number): Date;
|
|
14
|
+
/**
|
|
15
|
+
* Transforms tough-cookie to puppeteer cookie.
|
|
16
|
+
* @param toughCookie Cookie from CookieJar
|
|
17
|
+
* @return Cookie compatible with browser pool
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare function toughCookieToBrowserPoolCookie(toughCookie: Cookie): CookieObject;
|
|
21
|
+
/**
|
|
22
|
+
* Transforms browser-pool cookie to tough-cookie.
|
|
23
|
+
* @param cookieObject Cookie object (for instance from the `page.cookies` method).
|
|
24
|
+
* @param maxAgeSecs Fallback expiration in seconds when the cookie itself has no `expires`.
|
|
25
|
+
* When omitted, such a cookie is stored as a session cookie (no automatic expiration).
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function browserPoolCookieToToughCookie(cookieObject: CookieObject, maxAgeSecs?: number): Cookie;
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
* @param cookieString The cookie string to attempt parsing
|
|
32
|
+
* @returns Browser pool compatible cookie, or null if cookie cannot be parsed
|
|
33
|
+
*/
|
|
34
|
+
export declare function cookieStringToToughCookie(cookieString: string): CookieObject | null;
|
|
35
|
+
/**
|
|
36
|
+
* Merges multiple cookie strings. Keys are compared case-sensitively, warning will be logged
|
|
37
|
+
* if we see two cookies with same keys but different casing.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare function mergeCookies(url: string, sourceCookies: string[]): string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { serviceLocator } from '@crawlee/core';
|
|
2
|
+
import { Cookie, CookieJar } from 'tough-cookie';
|
|
3
|
+
import { CookieParseError } from './session_pool/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export function getCookiesFromResponse(response) {
|
|
8
|
+
const headers = response.headers;
|
|
9
|
+
const cookieHeaders = headers.getSetCookie();
|
|
10
|
+
try {
|
|
11
|
+
return cookieHeaders.map((cookie) => Cookie.parse(cookie));
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
throw new CookieParseError(cookieHeaders);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Calculate cookie expiration date
|
|
19
|
+
* @param maxAgeSecs
|
|
20
|
+
* @returns Calculated date by session max age seconds.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function getDefaultCookieExpirationDate(maxAgeSecs) {
|
|
24
|
+
return new Date(Date.now() + maxAgeSecs * 1000);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Transforms tough-cookie to puppeteer cookie.
|
|
28
|
+
* @param toughCookie Cookie from CookieJar
|
|
29
|
+
* @return Cookie compatible with browser pool
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function toughCookieToBrowserPoolCookie(toughCookie) {
|
|
33
|
+
return {
|
|
34
|
+
name: toughCookie.key,
|
|
35
|
+
value: toughCookie.value,
|
|
36
|
+
// Puppeteer and Playwright expect 'expires' to be 'Unix time in seconds', not ms
|
|
37
|
+
// If there is no expires date (so defaults to Infinity), we don't provide it to the browsers
|
|
38
|
+
expires: toughCookie.expires == null || toughCookie.expires === 'Infinity'
|
|
39
|
+
? undefined
|
|
40
|
+
: toughCookie.expires.getTime() / 1000,
|
|
41
|
+
domain: toughCookie.domain ? `${toughCookie.hostOnly ? '' : '.'}${toughCookie.domain}` : undefined,
|
|
42
|
+
path: toughCookie.path ?? undefined,
|
|
43
|
+
secure: toughCookie.secure,
|
|
44
|
+
httpOnly: toughCookie.httpOnly,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Transforms browser-pool cookie to tough-cookie.
|
|
49
|
+
* @param cookieObject Cookie object (for instance from the `page.cookies` method).
|
|
50
|
+
* @param maxAgeSecs Fallback expiration in seconds when the cookie itself has no `expires`.
|
|
51
|
+
* When omitted, such a cookie is stored as a session cookie (no automatic expiration).
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export function browserPoolCookieToToughCookie(cookieObject, maxAgeSecs) {
|
|
55
|
+
const isExpiresValid = cookieObject.expires && typeof cookieObject.expires === 'number' && cookieObject.expires > 0;
|
|
56
|
+
let expires;
|
|
57
|
+
if (isExpiresValid) {
|
|
58
|
+
expires = new Date(cookieObject.expires * 1000);
|
|
59
|
+
}
|
|
60
|
+
else if (maxAgeSecs != null) {
|
|
61
|
+
expires = getDefaultCookieExpirationDate(maxAgeSecs);
|
|
62
|
+
}
|
|
63
|
+
const domainHasLeadingDot = cookieObject.domain?.startsWith?.('.');
|
|
64
|
+
const domain = domainHasLeadingDot ? cookieObject.domain?.slice?.(1) : cookieObject.domain;
|
|
65
|
+
return new Cookie({
|
|
66
|
+
key: cookieObject.name,
|
|
67
|
+
value: cookieObject.value,
|
|
68
|
+
...(expires !== undefined && { expires }),
|
|
69
|
+
domain,
|
|
70
|
+
path: cookieObject.path,
|
|
71
|
+
secure: cookieObject.secure,
|
|
72
|
+
httpOnly: cookieObject.httpOnly,
|
|
73
|
+
hostOnly: !domainHasLeadingDot,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
* @param cookieString The cookie string to attempt parsing
|
|
79
|
+
* @returns Browser pool compatible cookie, or null if cookie cannot be parsed
|
|
80
|
+
*/
|
|
81
|
+
export function cookieStringToToughCookie(cookieString) {
|
|
82
|
+
const parsed = Cookie.parse(cookieString);
|
|
83
|
+
if (parsed) {
|
|
84
|
+
return toughCookieToBrowserPoolCookie(parsed);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Merges multiple cookie strings. Keys are compared case-sensitively, warning will be logged
|
|
90
|
+
* if we see two cookies with same keys but different casing.
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export function mergeCookies(url, sourceCookies) {
|
|
94
|
+
const jar = new CookieJar();
|
|
95
|
+
// ignore empty cookies
|
|
96
|
+
for (const sourceCookieString of sourceCookies) {
|
|
97
|
+
// ignore empty cookies
|
|
98
|
+
if (!sourceCookieString)
|
|
99
|
+
continue;
|
|
100
|
+
const cookies = sourceCookieString.split(/ *; */);
|
|
101
|
+
for (const cookieString of cookies) {
|
|
102
|
+
// ignore extra spaces
|
|
103
|
+
if (!cookieString)
|
|
104
|
+
continue;
|
|
105
|
+
const cookie = Cookie.parse(cookieString);
|
|
106
|
+
if (!cookie) {
|
|
107
|
+
serviceLocator.getLogger().warning(`Skipping malformed cookie fragment: '${cookieString}'`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const similarKeyCookie = jar.getCookiesSync(url).find((c) => {
|
|
111
|
+
return cookie.key !== c.key && cookie.key.toLowerCase() === c.key.toLowerCase();
|
|
112
|
+
});
|
|
113
|
+
if (similarKeyCookie) {
|
|
114
|
+
serviceLocator
|
|
115
|
+
.getLogger()
|
|
116
|
+
.warningOnce(`Found cookies with similar name during cookie merging: '${cookie.key}' and '${similarKeyCookie.key}'`);
|
|
117
|
+
}
|
|
118
|
+
jar.setCookieSync(cookie, url);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return jar.getCookieStringSync(url);
|
|
122
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CrawleeLogger } from '@crawlee/core';
|
|
2
|
+
import type { AutoscaledPoolOptions } from './autoscaling/autoscaled_pool.js';
|
|
3
|
+
import type { IConcurrencySystem } from './autoscaling/concurrency_system.js';
|
|
4
|
+
/** Everything a run needs to exist. Whatever a crawler resolves per run is resolved before the run is built. */
|
|
5
|
+
export interface CrawlerRunSetup {
|
|
6
|
+
log: CrawleeLogger;
|
|
7
|
+
/** The governor this run books its requests against, started and stopped by whoever owns it. */
|
|
8
|
+
concurrencySystem: IConcurrencySystem;
|
|
9
|
+
taskLoopOptions: Omit<AutoscaledPoolOptions, 'concurrencySystem' | 'consumer'>;
|
|
10
|
+
consumer: AutoscaledPoolOptions['consumer'];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Everything that belongs to a single {@link BasicCrawler.run|`crawler.run()`} — the task loop dispatching its
|
|
14
|
+
* requests, the concurrency governor those requests are booked against, and the bookkeeping that must not survive
|
|
15
|
+
* into a later run.
|
|
16
|
+
*
|
|
17
|
+
* The crawler builds a fresh one for every run, which is what keeps per-run state from leaking across runs: there
|
|
18
|
+
* is no reset checklist to keep in sync. A finished run stays readable as the history of the last crawl, so
|
|
19
|
+
* liveness is asked about explicitly (`isLive`) rather than inferred from presence.
|
|
20
|
+
*
|
|
21
|
+
* The lifecycle is `dispatchRequests()` and `finish()`. Everything a run owns exists from construction, so there
|
|
22
|
+
* is no half-built state to guard against in between.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export declare class CrawlerRun {
|
|
27
|
+
#private;
|
|
28
|
+
/** Whether this run initialized the ambient event manager, and is therefore the one that closes it. */
|
|
29
|
+
ownsEventManager: boolean;
|
|
30
|
+
constructor(setup: CrawlerRunSetup);
|
|
31
|
+
/**
|
|
32
|
+
* Runs the task loop until the crawl is over or {@link CrawlerRun.abort|aborted}. The governor has to be
|
|
33
|
+
* running by now — the task loop refuses to book anything against one that is not.
|
|
34
|
+
*/
|
|
35
|
+
dispatchRequests(): Promise<void>;
|
|
36
|
+
/** Whether the `run()` call this belongs to is still executing. */
|
|
37
|
+
get isLive(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the task loop is dispatching requests. Pausing one that has not started would not suspend a crawl,
|
|
40
|
+
* it would keep it from ever starting.
|
|
41
|
+
*/
|
|
42
|
+
get isDispatching(): boolean;
|
|
43
|
+
/** Whether a graceful shutdown of this run has been requested. */
|
|
44
|
+
get stopRequested(): boolean;
|
|
45
|
+
/** Asks the run to stop taking new requests. Only the first call logs `reason`. */
|
|
46
|
+
stop(reason: string): void;
|
|
47
|
+
/** Stops dispatching new requests, resolving once the ones in flight have settled. */
|
|
48
|
+
pause(timeoutSecs?: number): Promise<void>;
|
|
49
|
+
/** Resumes dispatching after a {@link CrawlerRun.pause|`pause()`}. */
|
|
50
|
+
resume(): void;
|
|
51
|
+
/** Ends the crawl without waiting for the requests in flight, so a pending `crawl()` resolves. */
|
|
52
|
+
abort(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Logs `message` the first time this run asks for it under `key`. Keyed rather than deduped by text, so that
|
|
55
|
+
* two messages describing the same shutdown say it once between them.
|
|
56
|
+
*/
|
|
57
|
+
logOnce(key: string, message: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Nudges the task loop at `readyAt`, when the request manager says it will have work again. The loop polls on
|
|
60
|
+
* its own anyway, so this only shortens the wait — hence a single timer that only an earlier wake-up replaces,
|
|
61
|
+
* `unref`'d so it never keeps the process alive. A run that has ended arms nothing.
|
|
62
|
+
*/
|
|
63
|
+
scheduleWake(readyAt: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Ends the run: drops a pending wake-up and aborts the task loop. Idempotent, so aborting a run through
|
|
66
|
+
* `teardown()` and then finishing it does the work once. The governor is left alone — it belongs to whoever
|
|
67
|
+
* started it.
|
|
68
|
+
*/
|
|
69
|
+
end(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Ends the run and marks it as history: the crawler stops reporting itself as running, and what is left is
|
|
72
|
+
* readable but no longer drivable. Ending here too means a subclass `teardown()` that forgets `super` cannot
|
|
73
|
+
* leave the task loop dispatching.
|
|
74
|
+
*/
|
|
75
|
+
finish(): Promise<void>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { AutoscaledPool } from './autoscaling/autoscaled_pool.js';
|
|
2
|
+
/**
|
|
3
|
+
* Everything that belongs to a single {@link BasicCrawler.run|`crawler.run()`} — the task loop dispatching its
|
|
4
|
+
* requests, the concurrency governor those requests are booked against, and the bookkeeping that must not survive
|
|
5
|
+
* into a later run.
|
|
6
|
+
*
|
|
7
|
+
* The crawler builds a fresh one for every run, which is what keeps per-run state from leaking across runs: there
|
|
8
|
+
* is no reset checklist to keep in sync. A finished run stays readable as the history of the last crawl, so
|
|
9
|
+
* liveness is asked about explicitly (`isLive`) rather than inferred from presence.
|
|
10
|
+
*
|
|
11
|
+
* The lifecycle is `dispatchRequests()` and `finish()`. Everything a run owns exists from construction, so there
|
|
12
|
+
* is no half-built state to guard against in between.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export class CrawlerRun {
|
|
17
|
+
#log;
|
|
18
|
+
#autoscaledPool;
|
|
19
|
+
#loggedOnce = new Set();
|
|
20
|
+
#stopRequested = false;
|
|
21
|
+
#dispatching = false;
|
|
22
|
+
#ended = false;
|
|
23
|
+
#finished = false;
|
|
24
|
+
#wakeTimer;
|
|
25
|
+
#wakeAt = 0;
|
|
26
|
+
/** Whether this run initialized the ambient event manager, and is therefore the one that closes it. */
|
|
27
|
+
ownsEventManager = false;
|
|
28
|
+
constructor(setup) {
|
|
29
|
+
this.#log = setup.log;
|
|
30
|
+
this.#autoscaledPool = new AutoscaledPool({
|
|
31
|
+
...setup.taskLoopOptions,
|
|
32
|
+
concurrencySystem: setup.concurrencySystem,
|
|
33
|
+
consumer: setup.consumer,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Runs the task loop until the crawl is over or {@link CrawlerRun.abort|aborted}. The governor has to be
|
|
38
|
+
* running by now — the task loop refuses to book anything against one that is not.
|
|
39
|
+
*/
|
|
40
|
+
async dispatchRequests() {
|
|
41
|
+
this.#dispatching = true;
|
|
42
|
+
try {
|
|
43
|
+
await this.#autoscaledPool.run();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
this.#dispatching = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Whether the `run()` call this belongs to is still executing. */
|
|
50
|
+
get isLive() {
|
|
51
|
+
return !this.#finished;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether the task loop is dispatching requests. Pausing one that has not started would not suspend a crawl,
|
|
55
|
+
* it would keep it from ever starting.
|
|
56
|
+
*/
|
|
57
|
+
get isDispatching() {
|
|
58
|
+
return this.#dispatching;
|
|
59
|
+
}
|
|
60
|
+
/** Whether a graceful shutdown of this run has been requested. */
|
|
61
|
+
get stopRequested() {
|
|
62
|
+
return this.#stopRequested;
|
|
63
|
+
}
|
|
64
|
+
/** Asks the run to stop taking new requests. Only the first call logs `reason`. */
|
|
65
|
+
stop(reason) {
|
|
66
|
+
if (this.#stopRequested) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.#log.info(reason);
|
|
70
|
+
this.#stopRequested = true;
|
|
71
|
+
}
|
|
72
|
+
/** Stops dispatching new requests, resolving once the ones in flight have settled. */
|
|
73
|
+
async pause(timeoutSecs) {
|
|
74
|
+
await this.#autoscaledPool.pause(timeoutSecs);
|
|
75
|
+
}
|
|
76
|
+
/** Resumes dispatching after a {@link CrawlerRun.pause|`pause()`}. */
|
|
77
|
+
resume() {
|
|
78
|
+
this.#autoscaledPool.resume();
|
|
79
|
+
}
|
|
80
|
+
/** Ends the crawl without waiting for the requests in flight, so a pending `crawl()` resolves. */
|
|
81
|
+
async abort() {
|
|
82
|
+
await this.#autoscaledPool.abort();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Logs `message` the first time this run asks for it under `key`. Keyed rather than deduped by text, so that
|
|
86
|
+
* two messages describing the same shutdown say it once between them.
|
|
87
|
+
*/
|
|
88
|
+
logOnce(key, message) {
|
|
89
|
+
if (this.#loggedOnce.has(key)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.#log.info(message);
|
|
93
|
+
this.#loggedOnce.add(key);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Nudges the task loop at `readyAt`, when the request manager says it will have work again. The loop polls on
|
|
97
|
+
* its own anyway, so this only shortens the wait — hence a single timer that only an earlier wake-up replaces,
|
|
98
|
+
* `unref`'d so it never keeps the process alive. A run that has ended arms nothing.
|
|
99
|
+
*/
|
|
100
|
+
scheduleWake(readyAt) {
|
|
101
|
+
if (this.#ended) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.#wakeTimer !== undefined) {
|
|
105
|
+
if (this.#wakeAt <= readyAt) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
clearTimeout(this.#wakeTimer);
|
|
109
|
+
}
|
|
110
|
+
this.#wakeAt = readyAt;
|
|
111
|
+
this.#wakeTimer = setTimeout(() => {
|
|
112
|
+
this.#wakeTimer = undefined;
|
|
113
|
+
void this.#autoscaledPool.notify();
|
|
114
|
+
}, Math.max(0, readyAt - Date.now()));
|
|
115
|
+
this.#wakeTimer.unref();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Ends the run: drops a pending wake-up and aborts the task loop. Idempotent, so aborting a run through
|
|
119
|
+
* `teardown()` and then finishing it does the work once. The governor is left alone — it belongs to whoever
|
|
120
|
+
* started it.
|
|
121
|
+
*/
|
|
122
|
+
async end() {
|
|
123
|
+
if (this.#ended) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.#ended = true;
|
|
127
|
+
if (this.#wakeTimer !== undefined) {
|
|
128
|
+
clearTimeout(this.#wakeTimer);
|
|
129
|
+
this.#wakeTimer = undefined;
|
|
130
|
+
}
|
|
131
|
+
await this.#autoscaledPool.abort();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ends the run and marks it as history: the crawler stops reporting itself as running, and what is left is
|
|
135
|
+
* readable but no longer drivable. Ending here too means a subclass `teardown()` that forgets `super` cannot
|
|
136
|
+
* leave the task loop dispatching.
|
|
137
|
+
*/
|
|
138
|
+
async finish() {
|
|
139
|
+
await this.end();
|
|
140
|
+
this.#finished = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Awaitable } from '@crawlee/types';
|
|
2
|
+
/**
|
|
3
|
+
* Registers a callback that runs after the final context consumer finishes. Cleanups run in reverse registration
|
|
4
|
+
* order and receive the same `error` as the `onError` parameter of {@link ContextPipeline.call}: `undefined` on
|
|
5
|
+
* success, otherwise a {@link ContextPipelineInitializationError} (a middleware failed) or a
|
|
6
|
+
* {@link RequestHandlerError} (the consumer failed), with the original in `cause`. A cleanup that throws fails the
|
|
7
|
+
* whole call with a {@link ContextPipelineCleanupError}, which is critical.
|
|
8
|
+
*/
|
|
9
|
+
export type CleanupRegistrar = (cleanup: (error?: unknown) => Awaitable<void>) => void;
|
|
10
|
+
/**
|
|
11
|
+
* A middleware step in the context pipeline: receives the context built so far and returns an extension that gets
|
|
12
|
+
* merged into it. Anything that needs tearing down is registered via `onCleanup`.
|
|
13
|
+
*
|
|
14
|
+
* @template TCrawlingContext - The input context type for this middleware
|
|
15
|
+
* @template TCrawlingContextExtension - The enhanced output context type
|
|
16
|
+
*/
|
|
17
|
+
export type ContextMiddleware<TCrawlingContext, TCrawlingContextExtension> = (context: TCrawlingContext, onCleanup: CleanupRegistrar) => Awaitable<TCrawlingContextExtension>;
|
|
18
|
+
/**
|
|
19
|
+
* Encapsulates the logic of gradually enhancing the crawling context with additional information and utilities.
|
|
20
|
+
*
|
|
21
|
+
* The enhancement is done by a chain of middlewares that are added to the pipeline after its creation.
|
|
22
|
+
* This class provides a type-safe way to build a pipeline of context transformations where each step
|
|
23
|
+
* can enhance the context with additional properties or utilities.
|
|
24
|
+
*
|
|
25
|
+
* @template TContextBase - The base context type that serves as the starting point
|
|
26
|
+
* @template TCrawlingContext - The final context type after all middleware transformations
|
|
27
|
+
*/
|
|
28
|
+
export declare abstract class ContextPipeline<TContextBase, TCrawlingContext extends TContextBase> {
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new empty context pipeline.
|
|
31
|
+
*
|
|
32
|
+
* @template TContextBase - The base context type for the pipeline
|
|
33
|
+
* @returns A new ContextPipeline instance with no transformations
|
|
34
|
+
*/
|
|
35
|
+
static create<TContextBase>(): ContextPipeline<TContextBase, TContextBase>;
|
|
36
|
+
/**
|
|
37
|
+
* Adds a middleware to the pipeline, creating a new pipeline instance.
|
|
38
|
+
*
|
|
39
|
+
* This method provides a fluent interface for building context transformation pipelines.
|
|
40
|
+
* Each middleware can enhance the context with additional properties or utilities.
|
|
41
|
+
*
|
|
42
|
+
* @template TCrawlingContextExtension - The enhanced context type produced by this middleware
|
|
43
|
+
* @param middleware - The middleware to add to the pipeline
|
|
44
|
+
* @returns A new ContextPipeline instance with the added middleware
|
|
45
|
+
*/
|
|
46
|
+
abstract compose<TCrawlingContextExtension>(middleware: ContextMiddleware<TCrawlingContext, TCrawlingContextExtension>): ContextPipeline<TContextBase, TCrawlingContext & TCrawlingContextExtension>;
|
|
47
|
+
/**
|
|
48
|
+
* Chains another pipeline onto this one. The other pipeline's base context must match
|
|
49
|
+
* this pipeline's output context. Returns a new pipeline that runs this pipeline's
|
|
50
|
+
* middlewares first, then the other pipeline's middlewares.
|
|
51
|
+
*
|
|
52
|
+
* @template TFinalContext - The final context type after the chained pipeline's transformations
|
|
53
|
+
* @param other - The pipeline to append after this one
|
|
54
|
+
* @returns A new ContextPipeline combining both pipelines' middlewares
|
|
55
|
+
*/
|
|
56
|
+
abstract chain<TFinalContext extends TCrawlingContext>(other: ContextPipeline<TCrawlingContext, TFinalContext>): ContextPipeline<TContextBase, TFinalContext>;
|
|
57
|
+
/**
|
|
58
|
+
* Executes the middleware pipeline and passes the final context to a consumer function.
|
|
59
|
+
*
|
|
60
|
+
* This method runs the crawling context through the entire middleware chain, enhancing it
|
|
61
|
+
* at each step, and then passes the final enhanced context to the provided consumer function.
|
|
62
|
+
* Proper cleanup is performed even if exceptions occur during processing.
|
|
63
|
+
*
|
|
64
|
+
* @param crawlingContext - The initial context to process through the pipeline
|
|
65
|
+
* @param finalContextConsumer - The function that will receive the final enhanced context
|
|
66
|
+
* @param onError - Receives a middleware failure wrapped in a {@link ContextPipelineInitializationError}, or a
|
|
67
|
+
* consumer failure wrapped in a {@link RequestHandlerError}, with the original in `cause`. After a middleware
|
|
68
|
+
* failure, the rest of the chain and the consumer are skipped. Runs before the cleanups, so it still sees
|
|
69
|
+
* whatever the middlewares set up.
|
|
70
|
+
*
|
|
71
|
+
* @throws {ContextPipelineCleanupError} When cleanup operations fail
|
|
72
|
+
* @throws Whatever `onError` throws
|
|
73
|
+
*/
|
|
74
|
+
abstract call(crawlingContext: TContextBase, finalContextConsumer: (finalContext: TCrawlingContext) => Awaitable<unknown>, onError: (error: unknown) => Awaitable<void>): Promise<void>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { serviceLocator } from '@crawlee/core';
|
|
2
|
+
import { ContextPipelineCleanupError, ContextPipelineInitializationError, RequestHandlerError } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Encapsulates the logic of gradually enhancing the crawling context with additional information and utilities.
|
|
5
|
+
*
|
|
6
|
+
* The enhancement is done by a chain of middlewares that are added to the pipeline after its creation.
|
|
7
|
+
* This class provides a type-safe way to build a pipeline of context transformations where each step
|
|
8
|
+
* can enhance the context with additional properties or utilities.
|
|
9
|
+
*
|
|
10
|
+
* @template TContextBase - The base context type that serves as the starting point
|
|
11
|
+
* @template TCrawlingContext - The final context type after all middleware transformations
|
|
12
|
+
*/
|
|
13
|
+
export class ContextPipeline {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new empty context pipeline.
|
|
16
|
+
*
|
|
17
|
+
* @template TContextBase - The base context type for the pipeline
|
|
18
|
+
* @returns A new ContextPipeline instance with no transformations
|
|
19
|
+
*/
|
|
20
|
+
static create() {
|
|
21
|
+
return new ContextPipelineImpl(async (context) => context);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Implementation of the `ContextPipeline` logic. This hides implementation details such as the `middleware` and `parent`
|
|
26
|
+
* properties from the `ContextPipeline` interface, making type checking more reliable.
|
|
27
|
+
*/
|
|
28
|
+
class ContextPipelineImpl extends ContextPipeline {
|
|
29
|
+
#middleware;
|
|
30
|
+
#parent;
|
|
31
|
+
constructor(middleware, parent) {
|
|
32
|
+
super();
|
|
33
|
+
this.#middleware = middleware;
|
|
34
|
+
this.#parent = parent;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @inheritdoc
|
|
38
|
+
*/
|
|
39
|
+
compose(middleware) {
|
|
40
|
+
return new ContextPipelineImpl(middleware, this);
|
|
41
|
+
}
|
|
42
|
+
chain(other) {
|
|
43
|
+
const otherMiddlewares = Array.from(other.#middlewareChain()).reverse();
|
|
44
|
+
let result = this;
|
|
45
|
+
for (const middleware of otherMiddlewares) {
|
|
46
|
+
result = result.compose(middleware);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
*#middlewareChain() {
|
|
51
|
+
let step = this;
|
|
52
|
+
while (step !== undefined) {
|
|
53
|
+
yield step.#middleware;
|
|
54
|
+
step = step.#parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* @inheritdoc
|
|
59
|
+
*/
|
|
60
|
+
async call(crawlingContext, finalContextConsumer, onError) {
|
|
61
|
+
const middlewares = Array.from(this.#middlewareChain()).reverse();
|
|
62
|
+
const cleanupStack = [];
|
|
63
|
+
const onCleanup = (cleanup) => {
|
|
64
|
+
cleanupStack.push(cleanup);
|
|
65
|
+
};
|
|
66
|
+
let failure;
|
|
67
|
+
let consumerStarted = false;
|
|
68
|
+
try {
|
|
69
|
+
for (const middleware of middlewares) {
|
|
70
|
+
const contextExtension = await middleware(crawlingContext, onCleanup);
|
|
71
|
+
const extensionNames = [
|
|
72
|
+
...Object.getOwnPropertyNames(contextExtension),
|
|
73
|
+
...Object.getOwnPropertySymbols(contextExtension),
|
|
74
|
+
];
|
|
75
|
+
for (const key of extensionNames) {
|
|
76
|
+
try {
|
|
77
|
+
if (Object.getOwnPropertyDescriptor(crawlingContext, key)?.configurable !== false) {
|
|
78
|
+
Object.defineProperty(crawlingContext, key, Object.getOwnPropertyDescriptor(contextExtension, key));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
serviceLocator
|
|
83
|
+
.getLogger()
|
|
84
|
+
.debug(`Context pipeline failed to define property ${key.toString()}:`, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
consumerStarted = true;
|
|
89
|
+
await finalContextConsumer(crawlingContext);
|
|
90
|
+
}
|
|
91
|
+
catch (exception) {
|
|
92
|
+
failure = consumerStarted
|
|
93
|
+
? new RequestHandlerError(exception)
|
|
94
|
+
: new ContextPipelineInitializationError(exception);
|
|
95
|
+
await onError(failure);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
try {
|
|
99
|
+
for (const cleanup of cleanupStack.reverse()) {
|
|
100
|
+
await cleanup(failure);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (exception) {
|
|
104
|
+
// eslint-disable-next-line no-unsafe-finally
|
|
105
|
+
throw new ContextPipelineCleanupError(exception);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|