@crawlee/browser-pool 4.0.0-rc.0 → 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 CHANGED
@@ -34,7 +34,7 @@ Crawlee is available as the [`crawlee`](https://www.npmjs.com/package/crawlee) N
34
34
 
35
35
  We recommend visiting the [Introduction tutorial](https://crawlee.dev/js/docs/introduction) in Crawlee documentation for more information.
36
36
 
37
- > Crawlee requires **Node.js 16 or higher**.
37
+ > Crawlee requires **Node.js 22.13 or higher**.
38
38
 
39
39
  ### With Crawlee CLI
40
40
 
@@ -82,6 +82,12 @@ export interface BrowserControllerEvents<Library extends CommonLibrary, LibraryO
82
82
  export declare abstract class BrowserController<Library extends CommonLibrary = CommonLibrary, LibraryOptions extends Dictionary | undefined = Parameters<Library['launch']>[0], LaunchResult extends CommonBrowser = UnwrapPromise<ReturnType<Library['launch']>>, NewPageOptions = Parameters<LaunchResult['newPage']>[0], NewPageResult = UnwrapPromise<ReturnType<LaunchResult['newPage']>>> extends TypedEmitter<BrowserControllerEvents<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>> implements IBrowserController<NewPageResult> {
83
83
  #private;
84
84
  readonly id: string;
85
+ /**
86
+ * Kept `protected` rather than `#private` because the concrete controllers in this package are
87
+ * separate classes — an ES private field would not be visible to them.
88
+ *
89
+ * @internal
90
+ */
85
91
  protected readonly log: CrawleeLogger;
86
92
  /**
87
93
  * The `BrowserPlugin` instance used to launch the browser.
@@ -100,11 +106,17 @@ export declare abstract class BrowserController<Library extends CommonLibrary =
100
106
  * `undefined` if no proxy is used
101
107
  */
102
108
  proxyUrl?: string;
109
+ /** @internal */
103
110
  isActive: boolean;
104
111
  activePages: number;
112
+ /** @internal */
105
113
  totalPages: number;
114
+ /** @internal */
106
115
  lastPageOpenedAt: number;
107
- private isActivePromise;
116
+ /**
117
+ * Returns a promise that resolves once the browser controller has been activated.
118
+ */
119
+ waitForActive(): Promise<void>;
108
120
  constructor(browserPlugin: BrowserPlugin<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>);
109
121
  /**
110
122
  * Activates the BrowserController. If you try to open new pages before
@@ -135,30 +147,32 @@ export declare abstract class BrowserController<Library extends CommonLibrary =
135
147
  * @ignore
136
148
  */
137
149
  newPage(pageOptions?: NewPageOptions): Promise<NewPageResult>;
138
- setCookies(page: NewPageResult, cookies: Cookie[]): Promise<void>;
139
- getCookies(page: NewPageResult): Promise<Cookie[]>;
140
150
  /**
141
- * @private
151
+ * Accounts for a page no longer being open. Callable from either side and safe to call twice:
152
+ * the page's own `close` event, or the pool when it gives up on a close that never settles.
153
+ * Tracked in a `WeakSet` rather than via `isClosed()`, because that reports the same event.
154
+ * @internal
142
155
  */
143
- protected abstract _close(): Promise<void>;
156
+ registerPageClosed(page: NewPageResult): void;
144
157
  /**
145
- * @private
158
+ * Registers cleanup belonging to a single page: an anonymizing proxy server, an incognito
159
+ * context. It runs from `registerPageClosed`, so it happens whether the page closed on its own
160
+ * or the pool gave up on a close that never settled, and it runs at most once.
161
+ *
162
+ * Hanging it off the page's `close` event instead leaks it in exactly the case this controller
163
+ * now handles, because that event does not arrive for a page the browser never destroyed.
164
+ * @ignore
146
165
  */
166
+ protected registerPageTeardown(page: NewPageResult, teardown: () => Promise<void>): void;
167
+ setCookies(page: NewPageResult, cookies: Cookie[]): Promise<void>;
168
+ getCookies(page: NewPageResult): Promise<Cookie[]>;
169
+ protected abstract _close(): Promise<void>;
147
170
  protected abstract _kill(): Promise<void>;
148
- /**
149
- * @private
150
- */
151
171
  protected abstract _newPage(pageOptions?: NewPageOptions): Promise<NewPageResult>;
152
- /**
153
- * @private
154
- */
155
172
  protected abstract _setCookies(page: NewPageResult, cookies: Cookie[]): Promise<void>;
156
- /**
157
- * @private
158
- */
159
173
  protected abstract _getCookies(page: NewPageResult): Promise<Cookie[]>;
160
174
  /**
161
- * @private
175
+ * @internal
162
176
  */
163
177
  abstract normalizeProxyOptions(proxyUrl: string | undefined, pageOptions: any): Record<string, unknown>;
164
178
  }
@@ -14,6 +14,12 @@ const PROCESS_KILL_TIMEOUT_MILLIS = 5000;
14
14
  */
15
15
  export class BrowserController extends TypedEmitter {
16
16
  id = nanoid();
17
+ /**
18
+ * Kept `protected` rather than `#private` because the concrete controllers in this package are
19
+ * separate classes — an ES private field would not be visible to them.
20
+ *
21
+ * @internal
22
+ */
17
23
  log;
18
24
  /**
19
25
  * The `BrowserPlugin` instance used to launch the browser.
@@ -32,15 +38,25 @@ export class BrowserController extends TypedEmitter {
32
38
  * `undefined` if no proxy is used
33
39
  */
34
40
  proxyUrl;
41
+ /** @internal */
35
42
  isActive = false;
36
43
  activePages = 0;
44
+ #closedPages = new WeakSet();
45
+ #pageTeardowns = new WeakMap();
46
+ /** @internal */
37
47
  totalPages = 0;
48
+ /** @internal */
38
49
  lastPageOpenedAt = Date.now();
39
50
  #activate;
40
- // kept as TS-private: `BrowserPool` awaits it through cross-object bracket access
41
- isActivePromise = new Promise((resolve) => {
51
+ #isActivePromise = new Promise((resolve) => {
42
52
  this.#activate = resolve;
43
53
  });
54
+ /**
55
+ * Returns a promise that resolves once the browser controller has been activated.
56
+ */
57
+ async waitForActive() {
58
+ await this.#isActivePromise;
59
+ }
44
60
  #commitBrowser;
45
61
  #hasBrowserPromise = new Promise((resolve) => {
46
62
  this.#commitBrowser = resolve;
@@ -115,12 +131,43 @@ export class BrowserController extends TypedEmitter {
115
131
  async newPage(pageOptions) {
116
132
  this.activePages++;
117
133
  this.totalPages++;
118
- await this.isActivePromise;
134
+ await this.#isActivePromise;
119
135
  const page = await this._newPage(pageOptions);
120
136
  tryCancel();
121
137
  this.lastPageOpenedAt = Date.now();
122
138
  return page;
123
139
  }
140
+ /**
141
+ * Accounts for a page no longer being open. Callable from either side and safe to call twice:
142
+ * the page's own `close` event, or the pool when it gives up on a close that never settles.
143
+ * Tracked in a `WeakSet` rather than via `isClosed()`, because that reports the same event.
144
+ * @internal
145
+ */
146
+ registerPageClosed(page) {
147
+ if (this.#closedPages.has(page))
148
+ return;
149
+ this.#closedPages.add(page);
150
+ this.activePages--;
151
+ const teardown = this.#pageTeardowns.get(page);
152
+ if (!teardown)
153
+ return;
154
+ this.#pageTeardowns.delete(page);
155
+ teardown().catch((error) => {
156
+ this.log.debug(`Could not clean up after a closed page.\nCause:${error.message}`, { id: this.id });
157
+ });
158
+ }
159
+ /**
160
+ * Registers cleanup belonging to a single page: an anonymizing proxy server, an incognito
161
+ * context. It runs from `registerPageClosed`, so it happens whether the page closed on its own
162
+ * or the pool gave up on a close that never settled, and it runs at most once.
163
+ *
164
+ * Hanging it off the page's `close` event instead leaks it in exactly the case this controller
165
+ * now handles, because that event does not arrive for a page the browser never destroyed.
166
+ * @ignore
167
+ */
168
+ registerPageTeardown(page, teardown) {
169
+ this.#pageTeardowns.set(page, teardown);
170
+ }
124
171
  async setCookies(page, cookies) {
125
172
  return this._setCookies(page, cookies);
126
173
  }
@@ -12,8 +12,6 @@ import type { BrowserController } from './browser-controller.js';
12
12
  * - without using a fingerprint,
13
13
  * - without specifying a user agent.
14
14
  * Last updated on 2022-05-05.
15
- *
16
- * After you update it here, please update it also in jsdom-crawler.ts
17
15
  */
18
16
  export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36";
19
17
  /**
@@ -83,7 +81,14 @@ export interface CreateLaunchContextOptions<Library extends CommonLibrary, Libra
83
81
  * feed them to {@link BrowserPool} for use.
84
82
  */
85
83
  export declare abstract class BrowserPlugin<Library extends CommonLibrary = CommonLibrary, LibraryOptions extends Dictionary | undefined = Parameters<Library['launch']>[0], LaunchResult extends CommonBrowser = UnwrapPromise<ReturnType<Library['launch']>>, NewPageOptions = Parameters<LaunchResult['newPage']>[0], NewPageResult = UnwrapPromise<ReturnType<LaunchResult['newPage']>>> {
84
+ #private;
86
85
  readonly name: string;
86
+ /**
87
+ * Kept `protected` rather than `#private` because the concrete plugins in this package are separate
88
+ * classes — an ES private field would not be visible to them.
89
+ *
90
+ * @internal
91
+ */
87
92
  protected readonly log: CrawleeLogger;
88
93
  readonly library: Library;
89
94
  readonly launchOptions: LibraryOptions;
@@ -132,16 +137,9 @@ export declare abstract class BrowserPlugin<Library extends CommonLibrary = Comm
132
137
  * Launches the browser using provided launch context.
133
138
  */
134
139
  launch(launchContext?: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): Promise<LaunchResult>;
135
- private mergeArgsToHideWebdriver;
136
140
  protected throwAugmentedLaunchError(cause: unknown, executablePath: string | undefined, dockerImage: string, moduleInstallCommand: string): never;
137
- /**
138
- * @private
139
- */
140
141
  protected abstract addProxyToLaunchOptions(launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): Promise<void>;
141
142
  protected abstract isChromiumBasedBrowser(launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): boolean;
142
- /**
143
- * @private
144
- */
145
143
  protected abstract _launch(launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): Promise<LaunchResult>;
146
144
  }
147
145
  export declare class BrowserLaunchError extends CriticalError {
@@ -9,8 +9,6 @@ import { sanitizeEndpointForLog } from '../utils.js';
9
9
  * - without using a fingerprint,
10
10
  * - without specifying a user agent.
11
11
  * Last updated on 2022-05-05.
12
- *
13
- * After you update it here, please update it also in jsdom-crawler.ts
14
12
  */
15
13
  export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36';
16
14
  /**
@@ -21,6 +19,12 @@ export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_
21
19
  */
22
20
  export class BrowserPlugin {
23
21
  name = this.constructor.name;
22
+ /**
23
+ * Kept `protected` rather than `#private` because the concrete plugins in this package are separate
24
+ * classes — an ES private field would not be visible to them.
25
+ *
26
+ * @internal
27
+ */
24
28
  log;
25
29
  library;
26
30
  launchOptions;
@@ -121,7 +125,7 @@ export class BrowserPlugin {
121
125
  }
122
126
  if (!launchContext.isRemote && this.isChromiumBasedBrowser(launchContext)) {
123
127
  // This will set the args for chromium based browsers to hide the webdriver.
124
- launchOptions.args = this.mergeArgsToHideWebdriver(launchOptions.args);
128
+ launchOptions.args = this.#mergeArgsToHideWebdriver(launchOptions.args);
125
129
  // When User-Agent is not set, and we're using Chromium in headless mode,
126
130
  // it is better to use DEFAULT_USER_AGENT to reduce chance of detection,
127
131
  // as otherwise 'HeadlessChrome' is present in User-Agent string.
@@ -135,7 +139,7 @@ export class BrowserPlugin {
135
139
  }
136
140
  return this._launch(launchContext);
137
141
  }
138
- mergeArgsToHideWebdriver(originalArgs) {
142
+ #mergeArgsToHideWebdriver(originalArgs) {
139
143
  if (!originalArgs?.length) {
140
144
  return ['--disable-blink-features=AutomationControlled'];
141
145
  }
@@ -1,4 +1,5 @@
1
1
  type PromiseVoid = () => Promise<void>;
2
+ /** @internal */
2
3
  export interface AnonymizeProxySugarOptions {
3
4
  ignoreProxyCertificate?: boolean;
4
5
  }
package/browser-pool.d.ts CHANGED
@@ -250,29 +250,18 @@ export interface BrowserPoolHooks<BC extends BrowserController, LC extends Launc
250
250
  export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoolOptions, BrowserPlugins extends BrowserPlugin[] = InferBrowserPluginArray<Options['browserPlugins']>, BrowserControllerReturn extends BrowserController = ReturnType<BrowserPlugins[number]['createController']>, LaunchContextReturn extends LaunchContext = ReturnType<BrowserPlugins[number]['createLaunchContext']>, PageOptions = Parameters<BrowserControllerReturn['newPage']>[0], PageReturn extends UnwrapPromise<ReturnType<BrowserControllerReturn['newPage']>> = UnwrapPromise<ReturnType<BrowserControllerReturn['newPage']>>> extends TypedEmitter<BrowserPoolEvents<BrowserControllerReturn, PageReturn>> implements IBrowserPool<PageReturn> {
251
251
  #private;
252
252
  browserPlugins: BrowserPlugins;
253
- maxOpenPagesPerBrowser: number;
253
+ /** @internal */
254
254
  maxOpenBrowsers: number;
255
- retireBrowserAfterPageCount: number;
256
- operationTimeoutMillis: number;
257
- closeInactiveBrowserAfterMillis: number;
258
- useFingerprints?: boolean;
255
+ /** @internal */
259
256
  fingerprintOptions: FingerprintOptions;
260
- preLaunchHooks: PreLaunchHook<LaunchContextReturn>[];
261
- postLaunchHooks: PostLaunchHook<BrowserControllerReturn>[];
262
- prePageCreateHooks: PrePageCreateHook<BrowserControllerReturn, PageOptions>[];
263
- postPageCreateHooks: PostPageCreateHook<BrowserControllerReturn, PageReturn>[];
264
- prePageCloseHooks: PrePageCloseHook<BrowserControllerReturn, PageReturn>[];
265
- postPageCloseHooks: PostPageCloseHook<BrowserControllerReturn>[];
266
- pageCounter: number;
267
- pages: Map<string, PageReturn>;
268
- pageIds: WeakMap<PageReturn, string>;
269
- startingBrowserControllers: Set<BrowserControllerReturn>;
270
- activeBrowserControllers: Set<BrowserControllerReturn>;
271
- retiredBrowserControllers: Set<BrowserControllerReturn>;
272
- pageToBrowserController: WeakMap<PageReturn, BrowserControllerReturn>;
257
+ /** @internal */
273
258
  fingerprintInjector?: FingerprintInjector;
274
259
  fingerprintGenerator?: FingerprintGenerator;
260
+ /** @internal */
275
261
  fingerprintCache?: QuickLRU<string, BrowserFingerprintWithHeaders>;
262
+ private pages;
263
+ private activeBrowserControllers;
264
+ private retiredBrowserControllers;
276
265
  private browserKillerInterval?;
277
266
  constructor(options: Options & BrowserPoolHooks<BrowserControllerReturn, LaunchContextReturn, PageReturn>);
278
267
  /**
@@ -348,7 +337,6 @@ export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoo
348
337
  * until it's closed.
349
338
  */
350
339
  getPageId(page: PageReturn): string | undefined;
351
- private createPageForBrowser;
352
340
  /**
353
341
  * Removes a browser controller from the pool. The underlying
354
342
  * browser will be closed after all its pages are closed.
@@ -362,8 +350,9 @@ export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoo
362
350
  retireBrowserByPage(page: PageReturn): void;
363
351
  /**
364
352
  * Releases a page back to the pool. The page is closed and, if the
365
- * optional `error` is a {@link SessionError}, the browser controller
366
- * that served the page is retired so that its tainted state (cookies,
353
+ * optional `error` is a {@link SessionError} (including
354
+ * {@link SessionRetiredError}), the browser controller that served
355
+ * the page is retired so that the finished session's state (cookies,
367
356
  * storage, etc.) cannot leak into future sessions.
368
357
  *
369
358
  * This is the primary way the crawler should return pages to the pool.
@@ -407,34 +396,36 @@ export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoo
407
396
  * @return {Promise<void>}
408
397
  */
409
398
  closeAllBrowsers(): Promise<void>;
399
+ [Symbol.asyncDispose](): Promise<void>;
410
400
  /**
411
- * Closes all managed browsers and tears down the pool.
401
+ * Closes every managed browser and empties the pool, which stays usable afterwards — a crawler releases its
402
+ * browsers when a run ends and may start another run on the same pool.
412
403
  */
413
- destroy(): Promise<void>;
414
- private teardown;
415
- private getAllBrowserControllers;
416
- private launchBrowser;
404
+ releaseAllBrowsers(): Promise<void>;
417
405
  /**
418
- * Picks plugins round robin.
419
- * @private
406
+ * Closes all managed browsers and tears the pool down for good: its intervals are cleared without being
407
+ * re-armed and its listeners are dropped, so it cannot be used again.
420
408
  */
421
- private pickBrowserPlugin;
422
- private pickBrowserWithFreeCapacity;
409
+ destroy(): Promise<void>;
423
410
  private closeInactiveRetiredBrowsers;
424
- private overridePageClose;
425
- private executeHooks;
426
- private closeRetiredBrowserWithNoPages;
427
411
  /**
428
- * Returns `true` if the pool can accept a new browser launch without exceeding
429
- * {@link BrowserPoolOptions.maxOpenBrowsers}. Counts starting, active, and retired browsers.
412
+ * Returns `true` if the pool can accept a new browser launch without exceeding `maxOpenBrowsers`.
413
+ * Counts starting, active, and retired browsers.
414
+ *
415
+ * A plain `BrowserPool` leaves `maxOpenBrowsers` at `Infinity`, so this only returns `false` when something
416
+ * has set a cap — {@link RemoteBrowserPool} does, from its own
417
+ * {@link RemoteBrowserPoolOptions.maxOpenBrowsers|`maxOpenBrowsers`} option. There is no
418
+ * `BrowserPoolOptions` key for it.
419
+ *
420
+ * @internal
430
421
  */
431
422
  hasFreeBrowserSlot(): boolean;
432
423
  /**
433
424
  * Returns `true` if any active browser has room for another page.
425
+ *
426
+ * @internal
434
427
  */
435
428
  hasActiveBrowserWithFreeCapacity(): boolean;
436
- private initializeFingerprinting;
437
- private addFingerprintHooks;
438
429
  }
439
430
  export interface BrowserPoolNewPageOptions<PageOptions, BP extends BrowserPlugin> extends NewPageOptions {
440
431
  /**