@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 +1 -1
- package/abstract-classes/browser-controller.d.ts +30 -16
- package/abstract-classes/browser-controller.js +50 -3
- package/abstract-classes/browser-plugin.d.ts +7 -9
- package/abstract-classes/browser-plugin.js +8 -4
- package/anonymize-proxy.d.ts +1 -0
- package/browser-pool.d.ts +28 -37
- package/browser-pool.js +171 -112
- package/events.d.ts +0 -1
- package/events.js +0 -1
- package/fingerprinting/types.d.ts +1 -31
- package/fingerprinting/types.js +0 -1
- package/index.d.ts +1 -1
- package/launch-context.d.ts +1 -0
- package/package.json +8 -7
- package/playwright/playwright-browser.d.ts +1 -0
- package/playwright/playwright-browser.js +1 -0
- package/playwright/playwright-controller.d.ts +1 -0
- package/playwright/playwright-controller.js +4 -3
- package/playwright/playwright-plugin.d.ts +2 -1
- package/playwright/playwright-plugin.js +5 -3
- package/puppeteer/puppeteer-controller.d.ts +1 -0
- package/puppeteer/puppeteer-controller.js +13 -11
- package/puppeteer/puppeteer-plugin.d.ts +5 -1
- package/puppeteer/puppeteer-plugin.js +5 -1
- package/remote-browser-pool.d.ts +16 -10
- package/remote-browser-pool.js +19 -5
- package/container-proxy-server.d.ts +0 -11
- package/container-proxy-server.js +0 -42
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/browser-pool",
|
|
3
|
-
"version": "4.0.0-rc.
|
|
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.
|
|
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.
|
|
34
|
-
"@crawlee/core": "4.0.0-rc.
|
|
35
|
-
"@crawlee/types": "4.0.0-rc.
|
|
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
|
|
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": "
|
|
67
|
+
"gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
|
|
67
68
|
}
|
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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) {
|
package/remote-browser-pool.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
160
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/remote-browser-pool.js
CHANGED
|
@@ -90,7 +90,11 @@ class RemoteSessionRegistry {
|
|
|
90
90
|
* @category Browser management
|
|
91
91
|
*/
|
|
92
92
|
export class RemoteBrowserPool {
|
|
93
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
}
|