@crawlee/browser-pool 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/abstract-classes/browser-controller.d.ts +31 -19
- package/abstract-classes/browser-controller.js +62 -13
- package/abstract-classes/browser-plugin.d.ts +11 -13
- package/abstract-classes/browser-plugin.js +13 -9
- package/anonymize-proxy.d.ts +1 -0
- package/browser-pool.d.ts +30 -41
- package/browser-pool.js +211 -147
- package/events.d.ts +2 -3
- package/events.js +0 -1
- package/fingerprinting/hooks.js +2 -1
- package/fingerprinting/types.d.ts +3 -33
- package/fingerprinting/types.js +0 -1
- package/fingerprinting/utils.js +5 -5
- package/index.d.ts +1 -1
- package/launch-context.d.ts +3 -3
- package/launch-context.js +11 -8
- package/package.json +9 -8
- package/playwright/playwright-browser.d.ts +3 -5
- package/playwright/playwright-browser.js +17 -16
- package/playwright/playwright-controller.d.ts +1 -0
- package/playwright/playwright-controller.js +4 -3
- package/playwright/playwright-plugin.d.ts +5 -4
- package/playwright/playwright-plugin.js +16 -12
- package/puppeteer/puppeteer-controller.d.ts +1 -0
- package/puppeteer/puppeteer-controller.js +13 -11
- package/puppeteer/puppeteer-plugin.d.ts +7 -3
- package/puppeteer/puppeteer-plugin.js +9 -5
- package/remote-browser-pool.d.ts +21 -21
- package/remote-browser-pool.js +63 -48
- package/container-proxy-server.d.ts +0 -11
- package/container-proxy-server.js +0 -42
package/remote-browser-pool.d.ts
CHANGED
|
@@ -91,14 +91,18 @@ 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
|
/**
|
|
98
|
-
* The remote-connection configuration a browser crawler accepts on its `remoteBrowser` option
|
|
99
|
-
* {@link RemoteBrowserPoolOptions}
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
+
* The remote-connection configuration a browser crawler accepts on its `remoteBrowser` option: the
|
|
103
|
+
* {@link RemoteBrowserPoolOptions} minus the `browserPlugins` (the crawler builds the correct one for its
|
|
104
|
+
* browser, which is what makes this path mismatch-proof) and minus `browserPoolOptions` — tuning the wrapping
|
|
105
|
+
* pool means building the pool yourself, through the `remote*BrowserPool()` factory for your crawler.
|
|
102
106
|
*/
|
|
103
107
|
export type CrawlerRemoteBrowserOptions = Omit<RemoteBrowserPoolOptions, 'browserPlugins' | 'browserPoolOptions'>;
|
|
104
108
|
/**
|
|
@@ -134,15 +138,13 @@ export type CrawlerRemoteBrowserOptions = Omit<RemoteBrowserPoolOptions, 'browse
|
|
|
134
138
|
* @category Browser management
|
|
135
139
|
*/
|
|
136
140
|
export declare class RemoteBrowserPool<Page = unknown> implements IBrowserPool<Page> {
|
|
137
|
-
|
|
141
|
+
#private;
|
|
142
|
+
/**
|
|
143
|
+
* The wrapped pool that performs the remote connections and serves pages.
|
|
144
|
+
*
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
138
147
|
readonly browserPool: BrowserPool;
|
|
139
|
-
/** The wrapped pool viewed through the {@link IBrowserPool} contract (the bare type widens pages to `never`). */
|
|
140
|
-
private readonly pool;
|
|
141
|
-
private readonly registry;
|
|
142
|
-
private readonly slotPollIntervalMillis;
|
|
143
|
-
private readonly log;
|
|
144
|
-
/** Shared by all `newPage` callers waiting for a free slot, so they don't each register their own listeners. */
|
|
145
|
-
private _capacityChange?;
|
|
146
148
|
constructor(options: RemoteBrowserPoolOptions);
|
|
147
149
|
/** Maximum number of remote browsers that may be open at the same time. */
|
|
148
150
|
get maxOpenBrowsers(): number;
|
|
@@ -157,14 +159,12 @@ export declare class RemoteBrowserPool<Page = unknown> implements IBrowserPool<P
|
|
|
157
159
|
}): Promise<void>;
|
|
158
160
|
extractPageState(page: Page): Promise<PageState>;
|
|
159
161
|
injectPageState(page: Page, state: PageState): Promise<void>;
|
|
160
|
-
/** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
|
|
161
|
-
destroy(): Promise<void>;
|
|
162
|
-
/** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
|
|
163
|
-
private _waitForFreeSlot;
|
|
164
162
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* 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.
|
|
168
165
|
*/
|
|
169
|
-
|
|
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>;
|
|
170
170
|
}
|
package/remote-browser-pool.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serviceLocator } from '@crawlee/core';
|
|
2
2
|
import { BrowserPool } from './browser-pool.js';
|
|
3
|
+
import { BROWSER_CONTROLLER_EVENTS, BROWSER_POOL_EVENTS } from './events.js';
|
|
3
4
|
import { RemoteBrowserProvider } from './remote-browser-provider.js';
|
|
4
5
|
/**
|
|
5
6
|
* Owns the lifecycle of remote browser sessions for a single {@link RemoteBrowserPool}: endpoint
|
|
@@ -7,18 +8,18 @@ import { RemoteBrowserProvider } from './remote-browser-provider.js';
|
|
|
7
8
|
* {@link RemoteConnection} so it can be injected into a plugin.
|
|
8
9
|
*/
|
|
9
10
|
class RemoteSessionRegistry {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
#sessions = new Map();
|
|
12
|
+
#nextToken = 0;
|
|
13
|
+
#endpoint;
|
|
14
|
+
#onRelease;
|
|
15
|
+
#log;
|
|
15
16
|
constructor(endpoint, onRelease, log) {
|
|
16
|
-
this
|
|
17
|
-
this
|
|
18
|
-
this
|
|
17
|
+
this.#endpoint = endpoint;
|
|
18
|
+
this.#onRelease = onRelease;
|
|
19
|
+
this.#log = log;
|
|
19
20
|
}
|
|
20
21
|
async resolve(options) {
|
|
21
|
-
const resolved = typeof this
|
|
22
|
+
const resolved = typeof this.#endpoint === 'function' ? await this.#endpoint(options) : this.#endpoint;
|
|
22
23
|
let result;
|
|
23
24
|
if (typeof resolved === 'string') {
|
|
24
25
|
if (!resolved)
|
|
@@ -31,30 +32,30 @@ class RemoteSessionRegistry {
|
|
|
31
32
|
else {
|
|
32
33
|
result = resolved;
|
|
33
34
|
}
|
|
34
|
-
const token = this
|
|
35
|
-
this
|
|
35
|
+
const token = this.#nextToken++;
|
|
36
|
+
this.#sessions.set(token, { url: result.url, context: result.context, released: false });
|
|
36
37
|
return { url: result.url, token };
|
|
37
38
|
}
|
|
38
39
|
async release(token) {
|
|
39
|
-
const session = this
|
|
40
|
+
const session = this.#sessions.get(token);
|
|
40
41
|
// Release at most once per session — guards a close()/teardown race (the `released` flag is set
|
|
41
42
|
// synchronously before the awaited onRelease, so releaseAll() can't double-fire an in-flight release).
|
|
42
43
|
if (!session || session.released)
|
|
43
44
|
return;
|
|
44
45
|
session.released = true;
|
|
45
46
|
try {
|
|
46
|
-
await this
|
|
47
|
+
await this.#onRelease?.({ endpoint: session.url, context: session.context });
|
|
47
48
|
}
|
|
48
49
|
catch (err) {
|
|
49
|
-
this
|
|
50
|
+
this.#log.warning('Remote browser release() failed.', { error: err?.message });
|
|
50
51
|
}
|
|
51
52
|
finally {
|
|
52
|
-
this
|
|
53
|
+
this.#sessions.delete(token);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
/** Releases every session that is still open. Called on pool teardown so no remote session leaks. */
|
|
56
57
|
async releaseAll() {
|
|
57
|
-
await Promise.all([...this
|
|
58
|
+
await Promise.all([...this.#sessions.keys()].map(async (token) => this.release(token)));
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
@@ -89,19 +90,23 @@ class RemoteSessionRegistry {
|
|
|
89
90
|
* @category Browser management
|
|
90
91
|
*/
|
|
91
92
|
export class RemoteBrowserPool {
|
|
92
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* The wrapped pool that performs the remote connections and serves pages.
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
93
98
|
browserPool;
|
|
94
99
|
/** The wrapped pool viewed through the {@link IBrowserPool} contract (the bare type widens pages to `never`). */
|
|
95
|
-
pool;
|
|
96
|
-
registry;
|
|
97
|
-
slotPollIntervalMillis;
|
|
98
|
-
log;
|
|
100
|
+
#pool;
|
|
101
|
+
#registry;
|
|
102
|
+
#slotPollIntervalMillis;
|
|
103
|
+
#log;
|
|
99
104
|
/** Shared by all `newPage` callers waiting for a free slot, so they don't each register their own listeners. */
|
|
100
|
-
|
|
105
|
+
#capacityChange;
|
|
101
106
|
constructor(options) {
|
|
102
107
|
const { browserPlugins, endpoint, release, maxOpenBrowsers, connection = {}, browserPoolOptions = {}, slotPollIntervalMillis = 500, } = options;
|
|
103
|
-
this
|
|
104
|
-
this
|
|
108
|
+
this.#log = serviceLocator.getLogger().child({ prefix: 'RemoteBrowserPool' });
|
|
109
|
+
this.#slotPollIntervalMillis = slotPollIntervalMillis;
|
|
105
110
|
// A RemoteBrowserProvider carries its own endpoint, release, and maxOpenBrowsers.
|
|
106
111
|
const provider = endpoint instanceof RemoteBrowserProvider ? endpoint : undefined;
|
|
107
112
|
const resolvedEndpoint = provider
|
|
@@ -111,20 +116,20 @@ export class RemoteBrowserPool {
|
|
|
111
116
|
? ({ context }) => provider.release(context)
|
|
112
117
|
: release;
|
|
113
118
|
const resolvedMax = maxOpenBrowsers ?? provider?.maxOpenBrowsers;
|
|
114
|
-
this
|
|
119
|
+
this.#registry = new RemoteSessionRegistry(resolvedEndpoint, resolvedRelease, this.#log);
|
|
115
120
|
// Wire every plugin for remote connection.
|
|
116
121
|
for (const plugin of browserPlugins) {
|
|
117
|
-
plugin.useRemoteConnection(this
|
|
122
|
+
plugin.useRemoteConnection(this.#registry, connection);
|
|
118
123
|
}
|
|
119
124
|
this.browserPool = new BrowserPool({ ...browserPoolOptions, browserPlugins });
|
|
120
|
-
this
|
|
125
|
+
this.#pool = this.browserPool;
|
|
121
126
|
// Release a browser's remote session once it closes. The registry dedupes (close() schedules a delayed
|
|
122
127
|
// kill(), so BROWSER_CLOSED can fire twice), and destroy()'s releaseAll() backstops any that never close.
|
|
123
|
-
this.browserPool.on(
|
|
124
|
-
controller.once(
|
|
125
|
-
const token = controller.launchContext.
|
|
128
|
+
this.browserPool.on(BROWSER_POOL_EVENTS.BROWSER_LAUNCHED, (controller) => {
|
|
129
|
+
controller.once(BROWSER_CONTROLLER_EVENTS.BROWSER_CLOSED, () => {
|
|
130
|
+
const token = controller.launchContext.remoteToken;
|
|
126
131
|
if (token !== undefined)
|
|
127
|
-
void this
|
|
132
|
+
void this.#registry.release(token);
|
|
128
133
|
});
|
|
129
134
|
});
|
|
130
135
|
if (resolvedMax !== undefined) {
|
|
@@ -143,28 +148,38 @@ export class RemoteBrowserPool {
|
|
|
143
148
|
* allows it (either a new browser slot is free, or an active browser still has page capacity).
|
|
144
149
|
*/
|
|
145
150
|
async newPage(options) {
|
|
146
|
-
await this
|
|
147
|
-
return this
|
|
151
|
+
await this.#waitForFreeSlot();
|
|
152
|
+
return this.#pool.newPage(options);
|
|
148
153
|
}
|
|
149
154
|
async closePage(page, options) {
|
|
150
|
-
return this
|
|
155
|
+
return this.#pool.closePage(page, options);
|
|
151
156
|
}
|
|
152
157
|
async extractPageState(page) {
|
|
153
|
-
return this
|
|
158
|
+
return this.#pool.extractPageState(page);
|
|
154
159
|
}
|
|
155
160
|
async injectPageState(page, state) {
|
|
156
|
-
return this
|
|
161
|
+
return this.#pool.injectPageState(page, state);
|
|
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();
|
|
157
172
|
}
|
|
158
173
|
/** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
|
|
159
174
|
async destroy() {
|
|
160
175
|
await this.browserPool.destroy();
|
|
161
176
|
// Backstop: release any sessions whose browser never emitted a close (e.g. dropped on teardown).
|
|
162
|
-
await this
|
|
177
|
+
await this.#registry.releaseAll();
|
|
163
178
|
}
|
|
164
179
|
/** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
|
|
165
|
-
async
|
|
180
|
+
async #waitForFreeSlot() {
|
|
166
181
|
while (!this.browserPool.hasFreeBrowserSlot() && !this.browserPool.hasActiveBrowserWithFreeCapacity()) {
|
|
167
|
-
await this
|
|
182
|
+
await this.#nextCapacityChange();
|
|
168
183
|
}
|
|
169
184
|
}
|
|
170
185
|
/**
|
|
@@ -172,20 +187,20 @@ export class RemoteBrowserPool {
|
|
|
172
187
|
* concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners)
|
|
173
188
|
* per tick, so a fleet of saturated callers doesn't fan out into N listener pairs on the pool.
|
|
174
189
|
*/
|
|
175
|
-
|
|
176
|
-
this
|
|
190
|
+
#nextCapacityChange() {
|
|
191
|
+
this.#capacityChange ??= new Promise((resolve) => {
|
|
177
192
|
const done = () => {
|
|
178
193
|
clearTimeout(timer);
|
|
179
|
-
this.browserPool.off(
|
|
180
|
-
this.browserPool.off(
|
|
181
|
-
this
|
|
194
|
+
this.browserPool.off(BROWSER_POOL_EVENTS.BROWSER_RETIRED, done);
|
|
195
|
+
this.browserPool.off(BROWSER_POOL_EVENTS.PAGE_CLOSED, done);
|
|
196
|
+
this.#capacityChange = undefined;
|
|
182
197
|
resolve();
|
|
183
198
|
};
|
|
184
|
-
const timer = setTimeout(done, this
|
|
199
|
+
const timer = setTimeout(done, this.#slotPollIntervalMillis);
|
|
185
200
|
timer.unref?.();
|
|
186
|
-
this.browserPool.once(
|
|
187
|
-
this.browserPool.once(
|
|
201
|
+
this.browserPool.once(BROWSER_POOL_EVENTS.BROWSER_RETIRED, done);
|
|
202
|
+
this.browserPool.once(BROWSER_POOL_EVENTS.PAGE_CLOSED, done);
|
|
188
203
|
});
|
|
189
|
-
return this
|
|
204
|
+
return this.#capacityChange;
|
|
190
205
|
}
|
|
191
206
|
}
|
|
@@ -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
|
-
}
|