@crawlee/browser-pool 4.0.0-beta.211 → 4.0.0-beta.212

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.
@@ -81,6 +81,7 @@ export interface CreateLaunchContextOptions<Library extends CommonLibrary, Libra
81
81
  * feed them to {@link BrowserPool} for use.
82
82
  */
83
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;
84
85
  readonly name: string;
85
86
  /**
86
87
  * Kept `protected` rather than `#private` because the concrete plugins in this package are separate
@@ -136,7 +137,6 @@ export declare abstract class BrowserPlugin<Library extends CommonLibrary = Comm
136
137
  * Launches the browser using provided launch context.
137
138
  */
138
139
  launch(launchContext?: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): Promise<LaunchResult>;
139
- private mergeArgsToHideWebdriver;
140
140
  protected throwAugmentedLaunchError(cause: unknown, executablePath: string | undefined, dockerImage: string, moduleInstallCommand: string): never;
141
141
  protected abstract addProxyToLaunchOptions(launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): Promise<void>;
142
142
  protected abstract isChromiumBasedBrowser(launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): boolean;
@@ -125,7 +125,7 @@ export class BrowserPlugin {
125
125
  }
126
126
  if (!launchContext.isRemote && this.isChromiumBasedBrowser(launchContext)) {
127
127
  // This will set the args for chromium based browsers to hide the webdriver.
128
- launchOptions.args = this.mergeArgsToHideWebdriver(launchOptions.args);
128
+ launchOptions.args = this.#mergeArgsToHideWebdriver(launchOptions.args);
129
129
  // When User-Agent is not set, and we're using Chromium in headless mode,
130
130
  // it is better to use DEFAULT_USER_AGENT to reduce chance of detection,
131
131
  // as otherwise 'HeadlessChrome' is present in User-Agent string.
@@ -139,7 +139,7 @@ export class BrowserPlugin {
139
139
  }
140
140
  return this._launch(launchContext);
141
141
  }
142
- mergeArgsToHideWebdriver(originalArgs) {
142
+ #mergeArgsToHideWebdriver(originalArgs) {
143
143
  if (!originalArgs?.length) {
144
144
  return ['--disable-blink-features=AutomationControlled'];
145
145
  }
package/browser-pool.d.ts CHANGED
@@ -337,7 +337,6 @@ export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoo
337
337
  * until it's closed.
338
338
  */
339
339
  getPageId(page: PageReturn): string | undefined;
340
- private createPageForBrowser;
341
340
  /**
342
341
  * Removes a browser controller from the pool. The underlying
343
342
  * browser will be closed after all its pages are closed.
@@ -408,17 +407,7 @@ export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoo
408
407
  * re-armed and its listeners are dropped, so it cannot be used again.
409
408
  */
410
409
  destroy(): Promise<void>;
411
- private getAllBrowserControllers;
412
- private launchBrowser;
413
- /**
414
- * Picks plugins round robin.
415
- */
416
- private pickBrowserPlugin;
417
- private pickBrowserWithFreeCapacity;
418
410
  private closeInactiveRetiredBrowsers;
419
- private overridePageClose;
420
- private executeHooks;
421
- private closeRetiredBrowserWithNoPages;
422
411
  /**
423
412
  * Returns `true` if the pool can accept a new browser launch without exceeding `maxOpenBrowsers`.
424
413
  * Counts starting, active, and retired browsers.
@@ -437,7 +426,6 @@ export declare class BrowserPool<Options extends BrowserPoolOptions = BrowserPoo
437
426
  * @internal
438
427
  */
439
428
  hasActiveBrowserWithFreeCapacity(): boolean;
440
- private initializeFingerprinting;
441
429
  }
442
430
  export interface BrowserPoolNewPageOptions<PageOptions, BP extends BrowserPlugin> extends NewPageOptions {
443
431
  /**
package/browser-pool.js CHANGED
@@ -99,14 +99,16 @@ export class BrowserPool extends TypedEmitter {
99
99
  #prePageCloseHooks;
100
100
  #postPageCloseHooks;
101
101
  #pageCounter = 0;
102
- // TS-private rather than `#`: tests observe page tracking and controller retirement directly
102
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- tests observe page tracking and controller retirement directly
103
103
  pages = new Map();
104
104
  #pageIds = new WeakMap();
105
105
  #startingBrowserControllers = new Set();
106
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
106
107
  activeBrowserControllers = new Set();
108
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
107
109
  retiredBrowserControllers = new Set();
108
110
  #pageToBrowserController = new WeakMap();
109
- // kept as TS-private: tests replace this interval through bracket access
111
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- tests replace this interval through bracket access
110
112
  browserKillerInterval;
111
113
  #browserRetireInterval;
112
114
  #limiter = pLimit(1);
@@ -151,7 +153,7 @@ export class BrowserPool extends TypedEmitter {
151
153
  this.#postPageCloseHooks = postPageCloseHooks;
152
154
  // fingerprinting
153
155
  if (useFingerprints) {
154
- this.initializeFingerprinting();
156
+ this.#initializeFingerprinting();
155
157
  // The fingerprint pre-launch hook goes last because of the fingerprint cache.
156
158
  // It is usual to generate proxy per browser and we want to know the proxyUrl for the caching.
157
159
  this.#preLaunchHooks = [...this.#preLaunchHooks, createFingerprintPreLaunchHook(this)];
@@ -180,7 +182,7 @@ export class BrowserPool extends TypedEmitter {
180
182
  * crawler's responsibility.
181
183
  */
182
184
  async newPage(options = {}) {
183
- const { id = nanoid(), pageOptions, browserPlugin = this.pickBrowserPlugin(), session, proxyUrl = session?.proxyInfo?.url, ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors, } = options;
185
+ const { id = nanoid(), pageOptions, browserPlugin = this.#pickBrowserPlugin(), session, proxyUrl = session?.proxyInfo?.url, ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors, } = options;
184
186
  if (this.pages.has(id)) {
185
187
  throw new Error(`Page with ID: ${id} already exists.`);
186
188
  }
@@ -199,15 +201,15 @@ export class BrowserPool extends TypedEmitter {
199
201
  // TODO(crawlee@v4): bump p-limit to v5 and drop this AsyncResource.bind wrapper.
200
202
  // Limiter is necessary - https://github.com/apify/crawlee/issues/1126
201
203
  return this.#limiter(AsyncResource.bind(async () => {
202
- let browserController = this.pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
204
+ let browserController = this.#pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
203
205
  if (!browserController)
204
- browserController = await this.launchBrowser(id, {
206
+ browserController = await this.#launchBrowser(id, {
205
207
  browserPlugin,
206
208
  proxyUrl,
207
209
  ignoreTlsErrors,
208
210
  });
209
211
  tryCancel();
210
- return await this.createPageForBrowser(id, browserController, pageOptions, proxyUrl, ignoreTlsErrors);
212
+ return await this.#createPageForBrowser(id, browserController, pageOptions, proxyUrl, ignoreTlsErrors);
211
213
  }));
212
214
  }
213
215
  /**
@@ -216,13 +218,13 @@ export class BrowserPool extends TypedEmitter {
216
218
  * configure the new browser.
217
219
  */
218
220
  async newPageInNewBrowser(options = {}) {
219
- const { id = nanoid(), pageOptions, launchOptions, browserPlugin = this.pickBrowserPlugin() } = options;
221
+ const { id = nanoid(), pageOptions, launchOptions, browserPlugin = this.#pickBrowserPlugin() } = options;
220
222
  if (this.pages.has(id)) {
221
223
  throw new Error(`Page with ID: ${id} already exists.`);
222
224
  }
223
- const browserController = await this.launchBrowser(id, { launchOptions, browserPlugin });
225
+ const browserController = await this.#launchBrowser(id, { launchOptions, browserPlugin });
224
226
  tryCancel();
225
- return await this.createPageForBrowser(id, browserController, pageOptions);
227
+ return await this.#createPageForBrowser(id, browserController, pageOptions);
226
228
  }
227
229
  /**
228
230
  * Opens new pages with all available plugins and returns an array
@@ -288,7 +290,7 @@ export class BrowserPool extends TypedEmitter {
288
290
  getPageId(page) {
289
291
  return this.#pageIds.get(page);
290
292
  }
291
- async createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl, ignoreTlsErrors) {
293
+ async #createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl, ignoreTlsErrors) {
292
294
  // This is needed for concurrent newPage calls to wait for the browser launch.
293
295
  // It's not ideal though, we need to come up with a better API.
294
296
  await browserController.waitForActive();
@@ -303,7 +305,7 @@ export class BrowserPool extends TypedEmitter {
303
305
  });
304
306
  }
305
307
  }
306
- await this.executeHooks(this.#prePageCreateHooks, pageId, browserController, finalPageOptions);
308
+ await this.#executeHooks(this.#prePageCreateHooks, pageId, browserController, finalPageOptions);
307
309
  tryCancel();
308
310
  let page;
309
311
  try {
@@ -316,13 +318,13 @@ export class BrowserPool extends TypedEmitter {
316
318
  if (browserController.totalPages >= this.#retireBrowserAfterPageCount) {
317
319
  this.retireBrowserController(browserController);
318
320
  }
319
- this.overridePageClose(page);
321
+ this.#overridePageClose(page);
320
322
  }
321
323
  catch (err) {
322
324
  this.retireBrowserController(browserController);
323
325
  throw new Error(`browserController.newPage() failed: ${browserController.id}\nCause:${err.message}.`);
324
326
  }
325
- await this.executeHooks(this.#postPageCreateHooks, page, browserController);
327
+ await this.#executeHooks(this.#postPageCreateHooks, page, browserController);
326
328
  tryCancel();
327
329
  this.emit(BROWSER_POOL_EVENTS.PAGE_CREATED, page);
328
330
  return page;
@@ -422,7 +424,7 @@ export class BrowserPool extends TypedEmitter {
422
424
  * @return {Promise<void>}
423
425
  */
424
426
  async closeAllBrowsers() {
425
- const controllers = this.getAllBrowserControllers();
427
+ const controllers = this.#getAllBrowserControllers();
426
428
  const promises = [...controllers]
427
429
  .filter((controller) => controller.isActive)
428
430
  .map(async (controller) => controller.close());
@@ -453,14 +455,14 @@ export class BrowserPool extends TypedEmitter {
453
455
  await this.releaseAllBrowsers();
454
456
  this.removeAllListeners();
455
457
  }
456
- getAllBrowserControllers() {
458
+ #getAllBrowserControllers() {
457
459
  return new Set([
458
460
  ...this.#startingBrowserControllers,
459
461
  ...this.activeBrowserControllers,
460
462
  ...this.retiredBrowserControllers,
461
463
  ]);
462
464
  }
463
- async launchBrowser(pageId, options) {
465
+ async #launchBrowser(pageId, options) {
464
466
  const { browserPlugin, launchOptions, proxyUrl, ignoreTlsErrors } = options;
465
467
  const browserController = browserPlugin.createController();
466
468
  this.#startingBrowserControllers.add(browserController);
@@ -481,7 +483,7 @@ export class BrowserPool extends TypedEmitter {
481
483
  try {
482
484
  // If the hooks or the launch fails, we need to delete the controller,
483
485
  // because otherwise it would be stuck in limbo without a browser.
484
- await this.executeHooks(this.#preLaunchHooks, pageId, launchContext);
486
+ await this.#executeHooks(this.#preLaunchHooks, pageId, launchContext);
485
487
  tryCancel();
486
488
  const browser = await browserPlugin.launch(launchContext);
487
489
  tryCancel();
@@ -496,7 +498,7 @@ export class BrowserPool extends TypedEmitter {
496
498
  try {
497
499
  // If the launch fails on the post-launch hooks, we need to clean up
498
500
  // both the controller and the browser before throwing.
499
- await this.executeHooks(this.#postLaunchHooks, pageId, browserController);
501
+ await this.#executeHooks(this.#postLaunchHooks, pageId, browserController);
500
502
  }
501
503
  catch (err) {
502
504
  this.#startingBrowserControllers.delete(browserController);
@@ -517,12 +519,12 @@ export class BrowserPool extends TypedEmitter {
517
519
  /**
518
520
  * Picks plugins round robin.
519
521
  */
520
- pickBrowserPlugin() {
522
+ #pickBrowserPlugin() {
521
523
  const pluginIndex = this.#pageCounter % this.browserPlugins.length;
522
524
  this.#pageCounter++;
523
525
  return this.browserPlugins[pluginIndex];
524
526
  }
525
- pickBrowserWithFreeCapacity(browserPlugin, options) {
527
+ #pickBrowserWithFreeCapacity(browserPlugin, options) {
526
528
  return [...this.activeBrowserControllers].find((controller) => {
527
529
  const hasCapacity = controller.activePages < this.#maxOpenPagesPerBrowser;
528
530
  const isCorrectPlugin = controller.browserPlugin === browserPlugin;
@@ -534,6 +536,7 @@ export class BrowserPool extends TypedEmitter {
534
536
  (!options?.proxyUrl && !controller.proxyUrl)));
535
537
  });
536
538
  }
539
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
537
540
  async closeInactiveRetiredBrowsers() {
538
541
  const closedBrowserIds = [];
539
542
  for (const controller of this.retiredBrowserControllers) {
@@ -555,7 +558,7 @@ export class BrowserPool extends TypedEmitter {
555
558
  });
556
559
  }
557
560
  }
558
- overridePageClose(page) {
561
+ #overridePageClose(page) {
559
562
  const originalPageClose = page.close;
560
563
  const browserController = this.#pageToBrowserController.get(page);
561
564
  const pageId = this.getPageId(page);
@@ -569,14 +572,14 @@ export class BrowserPool extends TypedEmitter {
569
572
  // so a timeout here would cancel the task of whoever awaited `page.close()`.
570
573
  let pageClosed = false;
571
574
  const closing = (async () => {
572
- await this.executeHooks(this.#prePageCloseHooks, page, browserController);
575
+ await this.#executeHooks(this.#prePageCloseHooks, page, browserController);
573
576
  await originalPageClose.apply(page, args).catch((err) => {
574
577
  this.#log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
575
578
  });
576
579
  // Whether the page itself is gone. Tracked separately from the sequence finishing,
577
580
  // so that a slow hook does not get the browser retired.
578
581
  pageClosed = true;
579
- await this.executeHooks(this.#postPageCloseHooks, pageId, browserController);
582
+ await this.#executeHooks(this.#postPageCloseHooks, pageId, browserController);
580
583
  })();
581
584
  let timeout;
582
585
  const finished = await Promise.race([
@@ -600,16 +603,16 @@ export class BrowserPool extends TypedEmitter {
600
603
  }
601
604
  browserController.registerPageClosed(page);
602
605
  this.pages.delete(pageId);
603
- this.closeRetiredBrowserWithNoPages(browserController);
606
+ this.#closeRetiredBrowserWithNoPages(browserController);
604
607
  this.emit(BROWSER_POOL_EVENTS.PAGE_CLOSED, page);
605
608
  };
606
609
  }
607
- async executeHooks(hooks, ...args) {
610
+ async #executeHooks(hooks, ...args) {
608
611
  for (const hook of hooks) {
609
612
  await hook(...args);
610
613
  }
611
614
  }
612
- closeRetiredBrowserWithNoPages(browserController) {
615
+ #closeRetiredBrowserWithNoPages(browserController) {
613
616
  if (browserController.activePages === 0 && this.retiredBrowserControllers.has(browserController)) {
614
617
  // Run this with a delay, otherwise page.close()
615
618
  // might fail with "Protocol error (Target.closeTarget): Target closed."
@@ -650,7 +653,7 @@ export class BrowserPool extends TypedEmitter {
650
653
  }
651
654
  return false;
652
655
  }
653
- initializeFingerprinting() {
656
+ #initializeFingerprinting() {
654
657
  const { useFingerprintCache = true, fingerprintCacheSize = 10_000 } = this.fingerprintOptions;
655
658
  this.fingerprintGenerator = new FingerprintGenerator(this.fingerprintOptions.fingerprintGeneratorOptions);
656
659
  this.fingerprintInjector = new FingerprintInjector();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/browser-pool",
3
- "version": "4.0.0-beta.211",
3
+ "version": "4.0.0-beta.212",
4
4
  "description": "Rotate multiple browsers using popular automation libraries such as Playwright or Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -31,9 +31,9 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@apify/timeout": "^1.0.1",
34
- "@crawlee/core": "4.0.0-beta.211",
35
- "@crawlee/types": "4.0.0-beta.211",
36
- "@crawlee/utils": "4.0.0-beta.211",
34
+ "@crawlee/core": "4.0.0-beta.212",
35
+ "@crawlee/types": "4.0.0-beta.212",
36
+ "@crawlee/utils": "4.0.0-beta.212",
37
37
  "fingerprint-generator": "^2.1.68",
38
38
  "fingerprint-injector": "^2.1.68",
39
39
  "lodash.merge": "^4.6.2",
@@ -64,5 +64,5 @@
64
64
  }
65
65
  }
66
66
  },
67
- "gitHead": "17bab8ab5b98554ca2db1d09bc5f61c59194a760"
67
+ "gitHead": "171a63a5d7f2a6d74cbce58479204ac20d30b87e"
68
68
  }
@@ -15,7 +15,6 @@ export declare class PlaywrightPlugin extends BrowserPlugin<BrowserType, SafePar
15
15
  */
16
16
  useRemoteConnection(connection: RemoteConnection, parameters?: RemoteConnectionParameters): void;
17
17
  protected _launch(launchContext: LaunchContext<BrowserType>): Promise<PlaywrightBrowser>;
18
- private throwOnFailedLaunch;
19
18
  createController(): PlaywrightController;
20
19
  protected addProxyToLaunchOptions(launchContext: LaunchContext<BrowserType>): Promise<void>;
21
20
  protected isChromiumBasedBrowser(): boolean;
@@ -55,7 +55,7 @@ export class PlaywrightPlugin extends BrowserPlugin {
55
55
  try {
56
56
  if (useIncognitoPages) {
57
57
  browser = await this.library.launch(launchOptions).catch((error) => {
58
- return this.throwOnFailedLaunch(launchContext, error);
58
+ return this.#throwOnFailedLaunch(launchContext, error);
59
59
  });
60
60
  if (anonymizedProxyUrl) {
61
61
  browser.on('disconnected', async () => {
@@ -67,7 +67,7 @@ export class PlaywrightPlugin extends BrowserPlugin {
67
67
  const browserContext = await this.library
68
68
  .launchPersistentContext(userDataDir, launchOptions)
69
69
  .catch((error) => {
70
- return this.throwOnFailedLaunch(launchContext, error);
70
+ return this.#throwOnFailedLaunch(launchContext, error);
71
71
  });
72
72
  browserContext.once('close', () => {
73
73
  if (userDataDir.includes('apify-playwright-firefox-taac-')) {
@@ -104,7 +104,7 @@ export class PlaywrightPlugin extends BrowserPlugin {
104
104
  }
105
105
  return browser;
106
106
  }
107
- throwOnFailedLaunch(launchContext, cause) {
107
+ #throwOnFailedLaunch(launchContext, cause) {
108
108
  this.throwAugmentedLaunchError(cause, launchContext.launchOptions?.executablePath, '`apify/actor-node-playwright-*` (with a correct browser name)', 'Try installing the required dependencies by running `npx playwright install --with-deps` (https://playwright.dev/docs/browsers).');
109
109
  }
110
110
  createController() {
@@ -167,12 +167,4 @@ export declare class RemoteBrowserPool<Page = unknown> implements IBrowserPool<P
167
167
  [Symbol.asyncDispose](): Promise<void>;
168
168
  /** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
169
169
  destroy(): Promise<void>;
170
- /** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
171
- private waitForFreeSlot;
172
- /**
173
- * Resolves on the next browser-retired / page-closed event, or after `slotPollIntervalMillis`. All
174
- * concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners)
175
- * per tick, so a fleet of saturated callers doesn't fan out into N listener pairs on the pool.
176
- */
177
- private nextCapacityChange;
178
170
  }
@@ -148,7 +148,7 @@ export class RemoteBrowserPool {
148
148
  * allows it (either a new browser slot is free, or an active browser still has page capacity).
149
149
  */
150
150
  async newPage(options) {
151
- await this.waitForFreeSlot();
151
+ await this.#waitForFreeSlot();
152
152
  return this.#pool.newPage(options);
153
153
  }
154
154
  async closePage(page, options) {
@@ -177,9 +177,9 @@ export class RemoteBrowserPool {
177
177
  await this.#registry.releaseAll();
178
178
  }
179
179
  /** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
180
- async waitForFreeSlot() {
180
+ async #waitForFreeSlot() {
181
181
  while (!this.browserPool.hasFreeBrowserSlot() && !this.browserPool.hasActiveBrowserWithFreeCapacity()) {
182
- await this.nextCapacityChange();
182
+ await this.#nextCapacityChange();
183
183
  }
184
184
  }
185
185
  /**
@@ -187,7 +187,7 @@ export class RemoteBrowserPool {
187
187
  * concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners)
188
188
  * per tick, so a fleet of saturated callers doesn't fan out into N listener pairs on the pool.
189
189
  */
190
- nextCapacityChange() {
190
+ #nextCapacityChange() {
191
191
  this.#capacityChange ??= new Promise((resolve) => {
192
192
  const done = () => {
193
193
  clearTimeout(timer);