@crawlee/browser-pool 4.0.0-beta.223 → 4.0.0-beta.225

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.
@@ -1,206 +0,0 @@
1
- import { serviceLocator } from '@crawlee/core';
2
- import { BrowserPool } from './browser-pool.js';
3
- import { BROWSER_CONTROLLER_EVENTS, BROWSER_POOL_EVENTS } from './events.js';
4
- import { RemoteBrowserProvider } from './remote-browser-provider.js';
5
- /**
6
- * Owns the lifecycle of remote browser sessions for a single {@link RemoteBrowserPool}: endpoint
7
- * resolution, the user's `release()` callback, and a release-at-most-once guarantee. Implements
8
- * {@link RemoteConnection} so it can be injected into a plugin.
9
- */
10
- class RemoteSessionRegistry {
11
- #sessions = new Map();
12
- #nextToken = 0;
13
- #endpoint;
14
- #onRelease;
15
- #log;
16
- constructor(endpoint, onRelease, log) {
17
- this.#endpoint = endpoint;
18
- this.#onRelease = onRelease;
19
- this.#log = log;
20
- }
21
- async resolve(options) {
22
- const resolved = typeof this.#endpoint === 'function' ? await this.#endpoint(options) : this.#endpoint;
23
- let result;
24
- if (typeof resolved === 'string') {
25
- if (!resolved)
26
- throw new Error('Remote browser endpoint resolved to an empty string.');
27
- result = { url: resolved };
28
- }
29
- else if (!resolved?.url) {
30
- throw new Error("Remote browser endpoint() must return a URL string or an object with a non-empty 'url'.");
31
- }
32
- else {
33
- result = resolved;
34
- }
35
- const token = this.#nextToken++;
36
- this.#sessions.set(token, { url: result.url, context: result.context, released: false });
37
- return { url: result.url, token };
38
- }
39
- async release(token) {
40
- const session = this.#sessions.get(token);
41
- // Release at most once per session — guards a close()/teardown race (the `released` flag is set
42
- // synchronously before the awaited onRelease, so releaseAll() can't double-fire an in-flight release).
43
- if (!session || session.released)
44
- return;
45
- session.released = true;
46
- try {
47
- await this.#onRelease?.({ endpoint: session.url, context: session.context });
48
- }
49
- catch (err) {
50
- this.#log.warning('Remote browser release() failed.', { error: err?.message });
51
- }
52
- finally {
53
- this.#sessions.delete(token);
54
- }
55
- }
56
- /** Releases every session that is still open. Called on pool teardown so no remote session leaks. */
57
- async releaseAll() {
58
- await Promise.all([...this.#sessions.keys()].map(async (token) => this.release(token)));
59
- }
60
- }
61
- /**
62
- * An {@link IBrowserPool} implementation for remote browser services.
63
- *
64
- * Unlike configuring a remote browser through a crawler's `launchContext`, this pool is the single owner
65
- * of all remote-session concerns:
66
- * - **endpoint resolution** — static URL, per-launch function, or {@link RemoteBrowserProvider};
67
- * - **release lifecycle** — `release()` fires exactly once per session on close/crash/teardown (no leaks,
68
- * no double-release);
69
- * - **concurrency** — {@link RemoteBrowserPoolOptions.maxOpenBrowsers|maxOpenBrowsers} is enforced inside
70
- * {@link RemoteBrowserPool.newPage|newPage}, which waits for a free slot rather than overshooting.
71
- *
72
- * The wrapped {@link BrowserPool} and its plugin only perform the library-specific `connect()` call.
73
- *
74
- * Pass an instance as the crawler's `browserPool` option:
75
- *
76
- * ```typescript
77
- * import { PlaywrightPlugin, RemoteBrowserPool } from '@crawlee/browser-pool';
78
- * import { PlaywrightCrawler } from 'crawlee';
79
- * import playwright from 'playwright';
80
- *
81
- * const browserPool = new RemoteBrowserPool({
82
- * browserPlugins: [new PlaywrightPlugin(playwright.chromium)],
83
- * endpoint: 'wss://production-sfo.browserless.io?token=xxx',
84
- * maxOpenBrowsers: 2,
85
- * });
86
- *
87
- * const crawler = new PlaywrightCrawler({ browserPool });
88
- * ```
89
- *
90
- * @category Browser management
91
- */
92
- export class RemoteBrowserPool {
93
- /**
94
- * The wrapped pool that performs the remote connections and serves pages.
95
- *
96
- * @internal
97
- */
98
- browserPool;
99
- /** The wrapped pool viewed through the {@link IBrowserPool} contract (the bare type widens pages to `never`). */
100
- #pool;
101
- #registry;
102
- #slotPollIntervalMillis;
103
- #log;
104
- /** Shared by all `newPage` callers waiting for a free slot, so they don't each register their own listeners. */
105
- #capacityChange;
106
- constructor(options) {
107
- const { browserPlugins, endpoint, release, maxOpenBrowsers, connection = {}, browserPoolOptions = {}, slotPollIntervalMillis = 500, } = options;
108
- this.#log = serviceLocator.getLogger().child({ prefix: 'RemoteBrowserPool' });
109
- this.#slotPollIntervalMillis = slotPollIntervalMillis;
110
- // A RemoteBrowserProvider carries its own endpoint, release, and maxOpenBrowsers.
111
- const provider = endpoint instanceof RemoteBrowserProvider ? endpoint : undefined;
112
- const resolvedEndpoint = provider
113
- ? (opts) => provider.connect(opts)
114
- : endpoint;
115
- const resolvedRelease = provider
116
- ? ({ context }) => provider.release(context)
117
- : release;
118
- const resolvedMax = maxOpenBrowsers ?? provider?.maxOpenBrowsers;
119
- this.#registry = new RemoteSessionRegistry(resolvedEndpoint, resolvedRelease, this.#log);
120
- // Wire every plugin for remote connection.
121
- for (const plugin of browserPlugins) {
122
- plugin.useRemoteConnection(this.#registry, connection);
123
- }
124
- this.browserPool = new BrowserPool({ ...browserPoolOptions, browserPlugins });
125
- this.#pool = this.browserPool;
126
- // Release a browser's remote session once it closes. The registry dedupes (close() schedules a delayed
127
- // kill(), so BROWSER_CLOSED can fire twice), and destroy()'s releaseAll() backstops any that never close.
128
- this.browserPool.on(BROWSER_POOL_EVENTS.BROWSER_LAUNCHED, (controller) => {
129
- controller.once(BROWSER_CONTROLLER_EVENTS.BROWSER_CLOSED, () => {
130
- const token = controller.launchContext.remoteToken;
131
- if (token !== undefined)
132
- void this.#registry.release(token);
133
- });
134
- });
135
- if (resolvedMax !== undefined) {
136
- this.browserPool.maxOpenBrowsers = resolvedMax;
137
- }
138
- }
139
- /** Maximum number of remote browsers that may be open at the same time. */
140
- get maxOpenBrowsers() {
141
- return this.browserPool.maxOpenBrowsers;
142
- }
143
- set maxOpenBrowsers(value) {
144
- this.browserPool.maxOpenBrowsers = value;
145
- }
146
- /**
147
- * Opens a new page, waiting first until {@link RemoteBrowserPoolOptions.maxOpenBrowsers|maxOpenBrowsers}
148
- * allows it (either a new browser slot is free, or an active browser still has page capacity).
149
- */
150
- async newPage(options) {
151
- await this.#waitForFreeSlot();
152
- return this.#pool.newPage(options);
153
- }
154
- async closePage(page, options) {
155
- return this.#pool.closePage(page, options);
156
- }
157
- async extractPageState(page) {
158
- return this.#pool.extractPageState(page);
159
- }
160
- async injectPageState(page, state) {
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();
172
- }
173
- /** Closes all browsers, releases any still-open remote sessions, and tears down the wrapped pool. */
174
- async destroy() {
175
- await this.browserPool.destroy();
176
- // Backstop: release any sessions whose browser never emitted a close (e.g. dropped on teardown).
177
- await this.#registry.releaseAll();
178
- }
179
- /** Resolves once the wrapped pool can serve another page without exceeding `maxOpenBrowsers`. */
180
- async #waitForFreeSlot() {
181
- while (!this.browserPool.hasFreeBrowserSlot() && !this.browserPool.hasActiveBrowserWithFreeCapacity()) {
182
- await this.#nextCapacityChange();
183
- }
184
- }
185
- /**
186
- * Resolves on the next browser-retired / page-closed event, or after `slotPollIntervalMillis`. All
187
- * concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners)
188
- * per tick, so a fleet of saturated callers doesn't fan out into N listener pairs on the pool.
189
- */
190
- #nextCapacityChange() {
191
- this.#capacityChange ??= new Promise((resolve) => {
192
- const done = () => {
193
- clearTimeout(timer);
194
- this.browserPool.off(BROWSER_POOL_EVENTS.BROWSER_RETIRED, done);
195
- this.browserPool.off(BROWSER_POOL_EVENTS.PAGE_CLOSED, done);
196
- this.#capacityChange = undefined;
197
- resolve();
198
- };
199
- const timer = setTimeout(done, this.#slotPollIntervalMillis);
200
- timer.unref?.();
201
- this.browserPool.once(BROWSER_POOL_EVENTS.BROWSER_RETIRED, done);
202
- this.browserPool.once(BROWSER_POOL_EVENTS.PAGE_CLOSED, done);
203
- });
204
- return this.#capacityChange;
205
- }
206
- }