@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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@crawlee/browser-pool",
3
- "version": "4.0.0-rc.0",
3
+ "version": "4.0.0-rc.1",
4
4
  "description": "Rotate multiple browsers using popular automation libraries such as Playwright or Puppeteer.",
5
5
  "engines": {
6
- "node": ">=22.0.0"
6
+ "node": ">=22.13.0"
7
7
  },
8
8
  "type": "module",
9
9
  "exports": {
@@ -30,9 +30,10 @@
30
30
  "copy": "tsx ../../scripts/copy.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@apify/timeout": "^0.4.4",
34
- "@crawlee/core": "4.0.0-rc.0",
35
- "@crawlee/types": "4.0.0-rc.0",
33
+ "@apify/timeout": "^1.0.1",
34
+ "@crawlee/core": "4.0.0-rc.1",
35
+ "@crawlee/types": "4.0.0-rc.1",
36
+ "@crawlee/utils": "4.0.0-rc.1",
36
37
  "fingerprint-generator": "^2.1.68",
37
38
  "fingerprint-injector": "^2.1.68",
38
39
  "lodash.merge": "^4.6.2",
@@ -42,7 +43,7 @@
42
43
  "quick-lru": "^7.0.1",
43
44
  "tiny-typed-emitter": "^2.1.0",
44
45
  "tslib": "^2.8.1",
45
- "zod": "^4.4.3"
46
+ "zod": "^4.5.4"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "playwright": "*",
@@ -63,5 +64,5 @@
63
64
  }
64
65
  }
65
66
  },
66
- "gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
67
+ "gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
67
68
  }
@@ -10,6 +10,7 @@ export interface BrowserOptions {
10
10
  */
11
11
  export declare class PlaywrightBrowser extends EventEmitter {
12
12
  #private;
13
+ /** @internal */
13
14
  constructor(options: BrowserOptions);
14
15
  [Symbol.asyncDispose](): Promise<void>;
15
16
  close(): Promise<void>;
@@ -7,6 +7,7 @@ export class PlaywrightBrowser extends EventEmitter {
7
7
  #version;
8
8
  #isConnected = true;
9
9
  #browserType;
10
+ /** @internal */
10
11
  constructor(options) {
11
12
  super();
12
13
  const { browserContext, version } = options;
@@ -4,6 +4,7 @@ import type { Browser, BrowserType, Page } from 'playwright';
4
4
  import { BrowserController } from '../abstract-classes/browser-controller.js';
5
5
  import type { SafeParameters } from '../utils.js';
6
6
  export declare class PlaywrightController extends BrowserController<BrowserType, SafeParameters<BrowserType['launch']>[0], Browser> {
7
+ /** @internal */
7
8
  normalizeProxyOptions(proxyUrl: string | undefined, pageOptions: any): Record<string, unknown>;
8
9
  protected _newPage(contextOptions?: SafeParameters<Browser['newPage']>[0]): Promise<Page>;
9
10
  protected _close(): Promise<void>;
@@ -2,6 +2,7 @@ import { tryCancel } from '@apify/timeout';
2
2
  import { BrowserController } from '../abstract-classes/browser-controller.js';
3
3
  import { anonymizeProxySugar } from '../anonymize-proxy.js';
4
4
  export class PlaywrightController extends BrowserController {
5
+ /** @internal */
5
6
  normalizeProxyOptions(proxyUrl, pageOptions) {
6
7
  if (!proxyUrl) {
7
8
  return {};
@@ -46,9 +47,9 @@ export class PlaywrightController extends BrowserController {
46
47
  }
47
48
  try {
48
49
  const page = await this.browser.newPage(contextOptions);
49
- page.once('close', async () => {
50
- this.activePages--;
51
- await close();
50
+ this.registerPageTeardown(page, close);
51
+ page.once('close', () => {
52
+ this.registerPageClosed(page);
52
53
  });
53
54
  tryCancel();
54
55
  return page;
@@ -10,10 +10,11 @@ export declare class PlaywrightPlugin extends BrowserPlugin<BrowserType, SafePar
10
10
  /**
11
11
  * Playwright remote connections only support incognito pages — `connect()` / `connectOverCDP()` don't
12
12
  * accept persistent contexts. Force it on (and inform the user) when wired for a remote connection.
13
+ *
14
+ * @internal
13
15
  */
14
16
  useRemoteConnection(connection: RemoteConnection, parameters?: RemoteConnectionParameters): void;
15
17
  protected _launch(launchContext: LaunchContext<BrowserType>): Promise<PlaywrightBrowser>;
16
- private throwOnFailedLaunch;
17
18
  createController(): PlaywrightController;
18
19
  protected addProxyToLaunchOptions(launchContext: LaunchContext<BrowserType>): Promise<void>;
19
20
  protected isChromiumBasedBrowser(): boolean;
@@ -9,6 +9,8 @@ export class PlaywrightPlugin extends BrowserPlugin {
9
9
  /**
10
10
  * Playwright remote connections only support incognito pages — `connect()` / `connectOverCDP()` don't
11
11
  * accept persistent contexts. Force it on (and inform the user) when wired for a remote connection.
12
+ *
13
+ * @internal
12
14
  */
13
15
  useRemoteConnection(connection, parameters = {}) {
14
16
  super.useRemoteConnection(connection, parameters);
@@ -53,7 +55,7 @@ export class PlaywrightPlugin extends BrowserPlugin {
53
55
  try {
54
56
  if (useIncognitoPages) {
55
57
  browser = await this.library.launch(launchOptions).catch((error) => {
56
- return this.throwOnFailedLaunch(launchContext, error);
58
+ return this.#throwOnFailedLaunch(launchContext, error);
57
59
  });
58
60
  if (anonymizedProxyUrl) {
59
61
  browser.on('disconnected', async () => {
@@ -65,7 +67,7 @@ export class PlaywrightPlugin extends BrowserPlugin {
65
67
  const browserContext = await this.library
66
68
  .launchPersistentContext(userDataDir, launchOptions)
67
69
  .catch((error) => {
68
- return this.throwOnFailedLaunch(launchContext, error);
70
+ return this.#throwOnFailedLaunch(launchContext, error);
69
71
  });
70
72
  browserContext.once('close', () => {
71
73
  if (userDataDir.includes('apify-playwright-firefox-taac-')) {
@@ -102,7 +104,7 @@ export class PlaywrightPlugin extends BrowserPlugin {
102
104
  }
103
105
  return browser;
104
106
  }
105
- throwOnFailedLaunch(launchContext, cause) {
107
+ #throwOnFailedLaunch(launchContext, cause) {
106
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).');
107
109
  }
108
110
  createController() {
@@ -9,6 +9,7 @@ export interface PuppeteerNewPageOptions extends PuppeteerTypes.BrowserContextOp
9
9
  proxyPassword?: string;
10
10
  }
11
11
  export declare class PuppeteerController extends BrowserController<typeof Puppeteer, PuppeteerTypes.LaunchOptions, PuppeteerTypes.Browser, PuppeteerNewPageOptions> {
12
+ /** @internal */
12
13
  normalizeProxyOptions(proxyUrl: string | undefined, pageOptions: any): Record<string, unknown>;
13
14
  protected _newPage(contextOptions?: PuppeteerNewPageOptions): Promise<PuppeteerTypes.Page>;
14
15
  protected _close(): Promise<void>;
@@ -3,6 +3,7 @@ import { BrowserController } from '../abstract-classes/browser-controller.js';
3
3
  import { anonymizeProxySugar } from '../anonymize-proxy.js';
4
4
  const PROCESS_KILL_TIMEOUT_MILLIS = 5000;
5
5
  export class PuppeteerController extends BrowserController {
6
+ /** @internal */
6
7
  normalizeProxyOptions(proxyUrl, pageOptions) {
7
8
  if (!proxyUrl) {
8
9
  return {};
@@ -51,17 +52,18 @@ export class PuppeteerController extends BrowserController {
51
52
  tryCancel();
52
53
  }
53
54
  */
54
- page.once('close', async () => {
55
- this.activePages--;
56
- try {
57
- await context.close();
58
- }
59
- catch (error) {
55
+ this.registerPageTeardown(page, async () => {
56
+ // The proxy server is not chained behind the context close: that can hang on
57
+ // the same target the page hung on, and the proxy runs in this process, so it
58
+ // has to be reclaimed either way.
59
+ const contextClosed = context.close().catch((error) => {
60
60
  this.log.exception(error, 'Failed to close context.');
61
- }
62
- finally {
63
- await close();
64
- }
61
+ });
62
+ await close();
63
+ await contextClosed;
64
+ });
65
+ page.once('close', () => {
66
+ this.registerPageClosed(page);
65
67
  });
66
68
  return page;
67
69
  }
@@ -73,7 +75,7 @@ export class PuppeteerController extends BrowserController {
73
75
  const page = await this.browser.newPage();
74
76
  tryCancel();
75
77
  page.once('close', () => {
76
- this.activePages--;
78
+ this.registerPageClosed(page);
77
79
  });
78
80
  return page;
79
81
  }
@@ -8,7 +8,11 @@ import type { RemoteConnection, RemoteConnectionParameters } from '../remote-bro
8
8
  import type { PuppeteerNewPageOptions } from './puppeteer-controller.js';
9
9
  import { PuppeteerController } from './puppeteer-controller.js';
10
10
  export declare class PuppeteerPlugin extends BrowserPlugin<typeof Puppeteer, PuppeteerTypes.LaunchOptions, PuppeteerTypes.Browser, PuppeteerNewPageOptions> {
11
- /** Pages share cookies/storage on the remote browser (Puppeteer defaults to non-incognito). */
11
+ /**
12
+ * Pages share cookies/storage on the remote browser (Puppeteer defaults to non-incognito).
13
+ *
14
+ * @internal
15
+ */
12
16
  useRemoteConnection(connection: RemoteConnection, parameters?: RemoteConnectionParameters): void;
13
17
  protected _launch(launchContext: LaunchContext<typeof Puppeteer, PuppeteerTypes.LaunchOptions, PuppeteerTypes.Browser, PuppeteerNewPageOptions>): Promise<PuppeteerTypes.Browser>;
14
18
  createController(): PuppeteerController;
@@ -5,7 +5,11 @@ import { noop } from '../utils.js';
5
5
  import { PuppeteerController } from './puppeteer-controller.js';
6
6
  const PROXY_SERVER_ARG = '--proxy-server=';
7
7
  export class PuppeteerPlugin extends BrowserPlugin {
8
- /** Pages share cookies/storage on the remote browser (Puppeteer defaults to non-incognito). */
8
+ /**
9
+ * Pages share cookies/storage on the remote browser (Puppeteer defaults to non-incognito).
10
+ *
11
+ * @internal
12
+ */
9
13
  useRemoteConnection(connection, parameters = {}) {
10
14
  super.useRemoteConnection(connection, parameters);
11
15
  if (!this.useIncognitoPages) {
@@ -91,7 +91,11 @@ export interface RemoteBrowserPoolOptions {
91
91
  connection?: RemoteConnectionParameters;
92
92
  /** Extra {@link BrowserPool} options (lifecycle hooks, page limits, fingerprinting, …). */
93
93
  browserPoolOptions?: Omit<BrowserPoolOptions, 'browserPlugins'> & BrowserPoolHooks<any, any, any>;
94
- /** Fallback poll interval (ms) while waiting for a free browser slot. The wait is event-driven; this only bounds it. @default 500 */
94
+ /**
95
+ * Fallback poll interval (ms) while waiting for a free browser slot. The wait is event-driven; this only bounds it.
96
+ * @default 500
97
+ * @internal
98
+ */
95
99
  slotPollIntervalMillis?: number;
96
100
  }
97
101
  /**
@@ -135,7 +139,11 @@ export type CrawlerRemoteBrowserOptions = Omit<RemoteBrowserPoolOptions, 'browse
135
139
  */
136
140
  export declare class RemoteBrowserPool<Page = unknown> implements IBrowserPool<Page> {
137
141
  #private;
138
- /** The wrapped pool that performs the remote connections and serves pages. */
142
+ /**
143
+ * The wrapped pool that performs the remote connections and serves pages.
144
+ *
145
+ * @internal
146
+ */
139
147
  readonly browserPool: BrowserPool;
140
148
  constructor(options: RemoteBrowserPoolOptions);
141
149
  /** Maximum number of remote browsers that may be open at the same time. */
@@ -151,14 +159,12 @@ export declare class RemoteBrowserPool<Page = unknown> implements IBrowserPool<P
151
159
  }): Promise<void>;
152
160
  extractPageState(page: Page): Promise<PageState>;
153
161
  injectPageState(page: Page, state: PageState): Promise<void>;
154
- /** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
155
- destroy(): Promise<void>;
156
- /** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
157
- private waitForFreeSlot;
158
162
  /**
159
- * Resolves on the next browser-retired / page-closed event, or after `slotPollIntervalMillis`. All
160
- * concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners)
161
- * per tick, so a fleet of saturated callers doesn't fan out into N listener pairs on the pool.
163
+ * Closes all browsers and releases their remote sessions, leaving the pool usable. Closing a browser is what
164
+ * releases its session (see the constructor), so this needs no separate release pass.
162
165
  */
163
- private nextCapacityChange;
166
+ releaseAllBrowsers(): Promise<void>;
167
+ [Symbol.asyncDispose](): Promise<void>;
168
+ /** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
169
+ destroy(): Promise<void>;
164
170
  }
@@ -90,7 +90,11 @@ class RemoteSessionRegistry {
90
90
  * @category Browser management
91
91
  */
92
92
  export class RemoteBrowserPool {
93
- /** The wrapped pool that performs the remote connections and serves pages. */
93
+ /**
94
+ * The wrapped pool that performs the remote connections and serves pages.
95
+ *
96
+ * @internal
97
+ */
94
98
  browserPool;
95
99
  /** The wrapped pool viewed through the {@link IBrowserPool} contract (the bare type widens pages to `never`). */
96
100
  #pool;
@@ -144,7 +148,7 @@ export class RemoteBrowserPool {
144
148
  * allows it (either a new browser slot is free, or an active browser still has page capacity).
145
149
  */
146
150
  async newPage(options) {
147
- await this.waitForFreeSlot();
151
+ await this.#waitForFreeSlot();
148
152
  return this.#pool.newPage(options);
149
153
  }
150
154
  async closePage(page, options) {
@@ -156,6 +160,16 @@ export class RemoteBrowserPool {
156
160
  async injectPageState(page, state) {
157
161
  return this.#pool.injectPageState(page, state);
158
162
  }
163
+ /**
164
+ * Closes all browsers and releases their remote sessions, leaving the pool usable. Closing a browser is what
165
+ * releases its session (see the constructor), so this needs no separate release pass.
166
+ */
167
+ async releaseAllBrowsers() {
168
+ await this.browserPool.releaseAllBrowsers();
169
+ }
170
+ async [Symbol.asyncDispose]() {
171
+ await this.destroy();
172
+ }
159
173
  /** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
160
174
  async destroy() {
161
175
  await this.browserPool.destroy();
@@ -163,9 +177,9 @@ export class RemoteBrowserPool {
163
177
  await this.#registry.releaseAll();
164
178
  }
165
179
  /** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
166
- async waitForFreeSlot() {
180
+ async #waitForFreeSlot() {
167
181
  while (!this.browserPool.hasFreeBrowserSlot() && !this.browserPool.hasActiveBrowserWithFreeCapacity()) {
168
- await this.nextCapacityChange();
182
+ await this.#nextCapacityChange();
169
183
  }
170
184
  }
171
185
  /**
@@ -173,7 +187,7 @@ export class RemoteBrowserPool {
173
187
  * concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners)
174
188
  * per tick, so a fleet of saturated callers doesn't fan out into N listener pairs on the pool.
175
189
  */
176
- nextCapacityChange() {
190
+ #nextCapacityChange() {
177
191
  this.#capacityChange ??= new Promise((resolve) => {
178
192
  const done = () => {
179
193
  clearTimeout(timer);
@@ -1,11 +0,0 @@
1
- /**
2
- * Creates a proxy server designed to handle requests from "container" instances.
3
- * Each container instance is assigned to a different (but still localhost) IP address
4
- * in order to work around authorization and to enable upstream.
5
- * @internal
6
- */
7
- export declare function createProxyServerForContainers(fallbackProxyUrl?: string): Promise<{
8
- port: number;
9
- ipToProxy: Map<string, string>;
10
- close(closeConnections: boolean): Promise<void>;
11
- }>;
@@ -1,42 +0,0 @@
1
- import { Server as ProxyChainServer } from 'proxy-chain';
2
- /**
3
- * Creates a proxy server designed to handle requests from "container" instances.
4
- * Each container instance is assigned to a different (but still localhost) IP address
5
- * in order to work around authorization and to enable upstream.
6
- * @internal
7
- */
8
- export async function createProxyServerForContainers(fallbackProxyUrl) {
9
- const ipToProxy = new Map();
10
- const proxyServer = new ProxyChainServer({
11
- prepareRequestFunction({ request }) {
12
- const prefix4to6 = '::ffff:';
13
- const localAddress = request.socket.localAddress.startsWith(prefix4to6)
14
- ? request.socket.localAddress.slice(prefix4to6.length)
15
- : request.socket.localAddress;
16
- const upstreamProxyUrl = ipToProxy.get(localAddress);
17
- if (upstreamProxyUrl === undefined) {
18
- if (fallbackProxyUrl) {
19
- return {
20
- upstreamProxyUrl: fallbackProxyUrl,
21
- requestAuthentication: false,
22
- };
23
- }
24
- console.warn(`Request without proxy ${localAddress} ${request.headers.host}`);
25
- }
26
- return {
27
- upstreamProxyUrl,
28
- requestAuthentication: false,
29
- };
30
- },
31
- port: 0,
32
- });
33
- await proxyServer.listen();
34
- proxyServer.server.unref();
35
- return {
36
- port: proxyServer.port,
37
- ipToProxy,
38
- async close(closeConnections) {
39
- return proxyServer.close(closeConnections);
40
- },
41
- };
42
- }