@crawlee/browser-pool 4.0.0-beta.26 → 4.0.0-beta.260
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 +15 -15
- package/abstract-classes/browser-controller.d.ts +100 -29
- package/abstract-classes/browser-controller.js +70 -24
- package/abstract-classes/browser-plugin.d.ts +25 -22
- package/abstract-classes/browser-plugin.js +21 -13
- package/anonymize-proxy.d.ts +6 -2
- package/anonymize-proxy.js +9 -5
- package/browser-pool.d.ts +110 -51
- package/browser-pool.js +318 -147
- package/events.d.ts +2 -4
- package/events.js +0 -2
- package/fingerprinting/hooks.d.ts +0 -1
- package/fingerprinting/hooks.js +26 -10
- package/fingerprinting/types.d.ts +3 -34
- package/fingerprinting/types.js +0 -2
- package/fingerprinting/utils.d.ts +0 -1
- package/fingerprinting/utils.js +5 -6
- package/index.d.ts +15 -7
- package/index.js +6 -3
- package/launch-context.d.ts +15 -5
- package/launch-context.js +15 -10
- package/package.json +15 -15
- package/playwright/playwright-browser.d.ts +3 -6
- package/playwright/playwright-browser.js +17 -17
- package/playwright/playwright-controller.d.ts +1 -1
- package/playwright/playwright-controller.js +10 -6
- package/playwright/playwright-plugin.d.ts +3 -7
- package/playwright/playwright-plugin.js +18 -17
- package/playwright/remote-playwright-plugin.d.ts +29 -0
- package/playwright/remote-playwright-plugin.js +38 -0
- package/proxy-server.d.ts +0 -1
- package/proxy-server.js +0 -1
- package/puppeteer/puppeteer-controller.d.ts +1 -1
- package/puppeteer/puppeteer-controller.js +29 -20
- package/puppeteer/puppeteer-plugin.d.ts +12 -4
- package/puppeteer/puppeteer-plugin.js +75 -48
- package/puppeteer/remote-puppeteer-plugin.d.ts +22 -0
- package/puppeteer/remote-puppeteer-plugin.js +31 -0
- package/remote-browser-plugin.d.ts +49 -0
- package/remote-browser-plugin.js +63 -0
- package/remote-browser-provider.d.ts +81 -0
- package/remote-browser-provider.js +65 -0
- package/utils.d.ts +7 -1
- package/utils.js +19 -1
- package/abstract-classes/browser-controller.d.ts.map +0 -1
- package/abstract-classes/browser-controller.js.map +0 -1
- package/abstract-classes/browser-plugin.d.ts.map +0 -1
- package/abstract-classes/browser-plugin.js.map +0 -1
- package/anonymize-proxy.d.ts.map +0 -1
- package/anonymize-proxy.js.map +0 -1
- package/browser-pool.d.ts.map +0 -1
- package/browser-pool.js.map +0 -1
- package/container-proxy-server.d.ts +0 -12
- package/container-proxy-server.d.ts.map +0 -1
- package/container-proxy-server.js +0 -43
- package/container-proxy-server.js.map +0 -1
- package/events.d.ts.map +0 -1
- package/events.js.map +0 -1
- package/fingerprinting/hooks.d.ts.map +0 -1
- package/fingerprinting/hooks.js.map +0 -1
- package/fingerprinting/types.d.ts.map +0 -1
- package/fingerprinting/types.js.map +0 -1
- package/fingerprinting/utils.d.ts.map +0 -1
- package/fingerprinting/utils.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/launch-context.d.ts.map +0 -1
- package/launch-context.js.map +0 -1
- package/logger.d.ts +0 -3
- package/logger.d.ts.map +0 -1
- package/logger.js +0 -5
- package/logger.js.map +0 -1
- package/playwright/playwright-browser.d.ts.map +0 -1
- package/playwright/playwright-browser.js.map +0 -1
- package/playwright/playwright-controller.d.ts.map +0 -1
- package/playwright/playwright-controller.js.map +0 -1
- package/playwright/playwright-plugin.d.ts.map +0 -1
- package/playwright/playwright-plugin.js.map +0 -1
- package/proxy-server.d.ts.map +0 -1
- package/proxy-server.js.map +0 -1
- package/puppeteer/puppeteer-controller.d.ts.map +0 -1
- package/puppeteer/puppeteer-controller.js.map +0 -1
- package/puppeteer/puppeteer-plugin.d.ts.map +0 -1
- package/puppeteer/puppeteer-plugin.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/browser-pool.js
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
+
import { SessionError, serviceLocator } from '@crawlee/core';
|
|
2
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
1
3
|
import { FingerprintGenerator } from 'fingerprint-generator';
|
|
2
4
|
import { FingerprintInjector } from 'fingerprint-injector';
|
|
3
5
|
import { nanoid } from 'nanoid';
|
|
4
|
-
import ow from 'ow';
|
|
5
6
|
import pLimit from 'p-limit';
|
|
6
7
|
import QuickLRU from 'quick-lru';
|
|
7
8
|
import { TypedEmitter } from 'tiny-typed-emitter';
|
|
9
|
+
import { z } from 'zod';
|
|
8
10
|
import { addTimeoutToPromise, tryCancel } from '@apify/timeout';
|
|
11
|
+
import { BROWSER_POOL_EVENTS } from './events.js';
|
|
9
12
|
import { createFingerprintPreLaunchHook, createPostPageCreateHook, createPrePageCreateHook, } from './fingerprinting/hooks.js';
|
|
10
|
-
|
|
13
|
+
const PAGE_CLOSE_TIMEOUT_MILLIS = 5000;
|
|
11
14
|
const PAGE_CLOSE_KILL_TIMEOUT_MILLIS = 1000;
|
|
12
15
|
const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
|
|
16
|
+
const SLOT_POLL_INTERVAL_MILLIS = 500;
|
|
17
|
+
const browserPoolOptionsSchema = z.strictObject({
|
|
18
|
+
browserPlugins: schemas.anyArray.refine((value) => value.length >= 1, 'Expected a non-empty array'),
|
|
19
|
+
maxOpenPagesPerBrowser: schemas.anyNumber.default(20),
|
|
20
|
+
maxOpenBrowsers: schemas.anyNumber.default(Infinity),
|
|
21
|
+
retireBrowserAfterPageCount: schemas.anyNumber.default(100),
|
|
22
|
+
operationTimeoutSecs: schemas.anyNumber.default(15),
|
|
23
|
+
closeInactiveBrowserAfterSecs: schemas.anyNumber.default(300),
|
|
24
|
+
retireInactiveBrowserAfterSecs: schemas.anyNumber.default(10),
|
|
25
|
+
preLaunchHooks: schemas.anyArray.default(() => []),
|
|
26
|
+
postLaunchHooks: schemas.anyArray.default(() => []),
|
|
27
|
+
prePageCreateHooks: schemas.anyArray.default(() => []),
|
|
28
|
+
postPageCreateHooks: schemas.anyArray.default(() => []),
|
|
29
|
+
prePageCloseHooks: schemas.anyArray.default(() => []),
|
|
30
|
+
postPageCloseHooks: schemas.anyArray.default(() => []),
|
|
31
|
+
useFingerprints: z.boolean().default(true),
|
|
32
|
+
fingerprintOptions: schemas.anyObject.default(() => ({})),
|
|
33
|
+
});
|
|
13
34
|
/**
|
|
14
35
|
* The `BrowserPool` class is the most important class of the `browser-pool` module.
|
|
15
36
|
* It manages opening and closing of browsers and their pages and its constructor
|
|
@@ -59,51 +80,45 @@ const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
|
|
|
59
80
|
*/
|
|
60
81
|
export class BrowserPool extends TypedEmitter {
|
|
61
82
|
browserPlugins;
|
|
62
|
-
|
|
63
|
-
retireBrowserAfterPageCount;
|
|
64
|
-
operationTimeoutMillis;
|
|
65
|
-
closeInactiveBrowserAfterMillis;
|
|
66
|
-
useFingerprints;
|
|
83
|
+
/** @internal */
|
|
67
84
|
fingerprintOptions;
|
|
68
|
-
|
|
69
|
-
postLaunchHooks;
|
|
70
|
-
prePageCreateHooks;
|
|
71
|
-
postPageCreateHooks;
|
|
72
|
-
prePageCloseHooks;
|
|
73
|
-
postPageCloseHooks;
|
|
74
|
-
pageCounter = 0;
|
|
75
|
-
pages = new Map();
|
|
76
|
-
pageIds = new WeakMap();
|
|
77
|
-
startingBrowserControllers = new Set();
|
|
78
|
-
activeBrowserControllers = new Set();
|
|
79
|
-
retiredBrowserControllers = new Set();
|
|
80
|
-
pageToBrowserController = new WeakMap();
|
|
85
|
+
/** @internal */
|
|
81
86
|
fingerprintInjector;
|
|
82
87
|
fingerprintGenerator;
|
|
88
|
+
/** @internal */
|
|
83
89
|
fingerprintCache;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
#maxOpenPagesPerBrowser;
|
|
91
|
+
#maxOpenBrowsers;
|
|
92
|
+
#retireBrowserAfterPageCount;
|
|
93
|
+
#operationTimeoutMillis;
|
|
94
|
+
#closeInactiveBrowserAfterMillis;
|
|
95
|
+
#preLaunchHooks;
|
|
96
|
+
#postLaunchHooks;
|
|
97
|
+
#prePageCreateHooks;
|
|
98
|
+
#postPageCreateHooks;
|
|
99
|
+
#prePageCloseHooks;
|
|
100
|
+
#postPageCloseHooks;
|
|
101
|
+
#pageCounter = 0;
|
|
102
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- tests observe page tracking and controller retirement directly
|
|
103
|
+
pages = new Map();
|
|
104
|
+
#pageIds = new WeakMap();
|
|
105
|
+
#startingBrowserControllers = new Set();
|
|
106
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
|
|
107
|
+
activeBrowserControllers = new Set();
|
|
108
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
|
|
109
|
+
retiredBrowserControllers = new Set();
|
|
110
|
+
#pageToBrowserController = new WeakMap();
|
|
111
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- tests replace this interval through bracket access
|
|
112
|
+
browserKillerInterval;
|
|
113
|
+
#browserRetireInterval;
|
|
114
|
+
#limiter = pLimit(1);
|
|
115
|
+
#log;
|
|
116
|
+
/** Shared by all `newPage` callers waiting for a free slot, so they don't each register their own listeners. */
|
|
117
|
+
#capacityChange;
|
|
87
118
|
constructor(options) {
|
|
88
119
|
super();
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
browserPlugins: ow.array.minLength(1),
|
|
92
|
-
maxOpenPagesPerBrowser: ow.optional.number,
|
|
93
|
-
retireBrowserAfterPageCount: ow.optional.number,
|
|
94
|
-
operationTimeoutSecs: ow.optional.number,
|
|
95
|
-
closeInactiveBrowserAfterSecs: ow.optional.number,
|
|
96
|
-
retireInactiveBrowserAfterSecs: ow.optional.number,
|
|
97
|
-
preLaunchHooks: ow.optional.array,
|
|
98
|
-
postLaunchHooks: ow.optional.array,
|
|
99
|
-
prePageCreateHooks: ow.optional.array,
|
|
100
|
-
postPageCreateHooks: ow.optional.array,
|
|
101
|
-
prePageCloseHooks: ow.optional.array,
|
|
102
|
-
postPageCloseHooks: ow.optional.array,
|
|
103
|
-
useFingerprints: ow.optional.boolean,
|
|
104
|
-
fingerprintOptions: ow.optional.object,
|
|
105
|
-
}));
|
|
106
|
-
const { browserPlugins, maxOpenPagesPerBrowser = 20, retireBrowserAfterPageCount = 100, operationTimeoutSecs = 15, closeInactiveBrowserAfterSecs = 300, retireInactiveBrowserAfterSecs = 10, preLaunchHooks = [], postLaunchHooks = [], prePageCreateHooks = [], postPageCreateHooks = [], prePageCloseHooks = [], postPageCloseHooks = [], useFingerprints = true, fingerprintOptions = {}, } = options;
|
|
120
|
+
this.#log = serviceLocator.getLogger().child({ prefix: 'BrowserPool' });
|
|
121
|
+
const { browserPlugins, maxOpenPagesPerBrowser, maxOpenBrowsers, retireBrowserAfterPageCount, operationTimeoutSecs, closeInactiveBrowserAfterSecs, retireInactiveBrowserAfterSecs, preLaunchHooks, postLaunchHooks, prePageCreateHooks, postPageCreateHooks, prePageCloseHooks, postPageCloseHooks, useFingerprints, fingerprintOptions, } = parseArgument(options, browserPoolOptionsSchema);
|
|
107
122
|
const firstPluginConstructor = browserPlugins[0].constructor;
|
|
108
123
|
for (let i = 1; i < browserPlugins.length; i++) {
|
|
109
124
|
const providedPlugin = browserPlugins[i];
|
|
@@ -114,38 +129,62 @@ export class BrowserPool extends TypedEmitter {
|
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
131
|
this.browserPlugins = browserPlugins;
|
|
117
|
-
this
|
|
118
|
-
this.retireBrowserAfterPageCount = retireBrowserAfterPageCount;
|
|
119
|
-
this.operationTimeoutMillis = operationTimeoutSecs * 1000;
|
|
120
|
-
this.closeInactiveBrowserAfterMillis = closeInactiveBrowserAfterSecs * 1000;
|
|
121
|
-
this.useFingerprints = useFingerprints;
|
|
132
|
+
this.#maxOpenBrowsers = maxOpenBrowsers;
|
|
122
133
|
this.fingerprintOptions = fingerprintOptions;
|
|
123
|
-
this
|
|
134
|
+
this.#maxOpenPagesPerBrowser = maxOpenPagesPerBrowser;
|
|
135
|
+
this.#retireBrowserAfterPageCount = retireBrowserAfterPageCount;
|
|
136
|
+
this.#operationTimeoutMillis = operationTimeoutSecs * 1000;
|
|
137
|
+
this.#closeInactiveBrowserAfterMillis = closeInactiveBrowserAfterSecs * 1000;
|
|
138
|
+
// Sweeping slower than the window it enforces would round any sub-10s
|
|
139
|
+
// `closeInactiveBrowserAfterSecs` up to the sweep period.
|
|
140
|
+
this.browserKillerInterval = setInterval(async () => this.closeInactiveRetiredBrowsers(), Math.min(BROWSER_KILLER_INTERVAL_MILLIS, this.#closeInactiveBrowserAfterMillis));
|
|
141
|
+
this.browserKillerInterval.unref();
|
|
142
|
+
this.#browserRetireInterval = setInterval(async () => this.activeBrowserControllers.forEach((controller) => {
|
|
124
143
|
if (controller.activePages === 0 &&
|
|
125
144
|
controller.lastPageOpenedAt < Date.now() - retireInactiveBrowserAfterSecs * 1000) {
|
|
126
145
|
this.retireBrowserController(controller);
|
|
127
146
|
}
|
|
128
147
|
}), retireInactiveBrowserAfterSecs * 1000);
|
|
129
|
-
this
|
|
148
|
+
this.#browserRetireInterval.unref();
|
|
130
149
|
// hooks
|
|
131
|
-
this
|
|
132
|
-
this
|
|
133
|
-
this
|
|
134
|
-
this
|
|
135
|
-
this
|
|
136
|
-
this
|
|
150
|
+
this.#preLaunchHooks = preLaunchHooks;
|
|
151
|
+
this.#postLaunchHooks = postLaunchHooks;
|
|
152
|
+
this.#prePageCreateHooks = prePageCreateHooks;
|
|
153
|
+
this.#postPageCreateHooks = postPageCreateHooks;
|
|
154
|
+
this.#prePageCloseHooks = prePageCloseHooks;
|
|
155
|
+
this.#postPageCloseHooks = postPageCloseHooks;
|
|
137
156
|
// fingerprinting
|
|
138
|
-
if (
|
|
139
|
-
this
|
|
157
|
+
if (useFingerprints) {
|
|
158
|
+
this.#initializeFingerprinting();
|
|
159
|
+
// The fingerprint pre-launch hook goes last because of the fingerprint cache.
|
|
160
|
+
// It is usual to generate proxy per browser and we want to know the proxyUrl for the caching.
|
|
161
|
+
this.#preLaunchHooks = [...this.#preLaunchHooks, createFingerprintPreLaunchHook(this)];
|
|
162
|
+
this.#prePageCreateHooks = [createPrePageCreateHook(), ...this.#prePageCreateHooks];
|
|
163
|
+
this.#postPageCreateHooks = [
|
|
164
|
+
createPostPageCreateHook(this.fingerprintInjector),
|
|
165
|
+
...this.#postPageCreateHooks,
|
|
166
|
+
];
|
|
140
167
|
}
|
|
141
168
|
}
|
|
142
169
|
/**
|
|
143
170
|
* Opens a new page in one of the running browsers or launches
|
|
144
171
|
* a new browser and opens a page there, if no browsers are active,
|
|
145
172
|
* or their page limits have been exceeded.
|
|
173
|
+
*
|
|
174
|
+
* **Session injection (best-effort):** When a {@link NewPageOptions.session|session} is
|
|
175
|
+
* provided, this implementation uses it as a cache key for browser fingerprints (when
|
|
176
|
+
* fingerprinting is enabled) and reads
|
|
177
|
+
* {@link ProxyInfo.url|session.proxyInfo.url} /
|
|
178
|
+
* {@link ProxyInfo.ignoreTlsErrors|session.proxyInfo.ignoreTlsErrors} as defaults
|
|
179
|
+
* for `proxyUrl` and `ignoreTlsErrors` respectively. Explicit `proxyUrl` /
|
|
180
|
+
* `ignoreTlsErrors` values in the options take precedence.
|
|
181
|
+
*
|
|
182
|
+
* Beyond fingerprint caching and proxy configuration, no other session
|
|
183
|
+
* properties are consumed — cookie and header injection remain the
|
|
184
|
+
* crawler's responsibility.
|
|
146
185
|
*/
|
|
147
186
|
async newPage(options = {}) {
|
|
148
|
-
const { id = nanoid(), pageOptions, browserPlugin = this
|
|
187
|
+
const { id = nanoid(), pageOptions, browserPlugin = this.#pickBrowserPlugin(), session, proxyUrl = session?.proxyInfo?.url, ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors, } = options;
|
|
149
188
|
if (this.pages.has(id)) {
|
|
150
189
|
throw new Error(`Page with ID: ${id} already exists.`);
|
|
151
190
|
}
|
|
@@ -153,12 +192,16 @@ export class BrowserPool extends TypedEmitter {
|
|
|
153
192
|
throw new Error('Provided browserPlugin is not one of the plugins used by BrowserPool.');
|
|
154
193
|
}
|
|
155
194
|
// Limiter is necessary - https://github.com/apify/crawlee/issues/1126
|
|
156
|
-
return this
|
|
157
|
-
let browserController = this
|
|
158
|
-
|
|
159
|
-
|
|
195
|
+
return this.#limiter(async () => {
|
|
196
|
+
let browserController = this.#pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
|
|
197
|
+
// Checked under the limiter so concurrent waiters can't all pass on the same freed slot.
|
|
198
|
+
while (!browserController && !this.hasFreeBrowserSlot()) {
|
|
199
|
+
await this.#nextCapacityChange();
|
|
200
|
+
browserController = this.#pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
|
|
201
|
+
}
|
|
202
|
+
browserController ??= await this.#launchBrowser(id, { browserPlugin, proxyUrl, ignoreTlsErrors });
|
|
160
203
|
tryCancel();
|
|
161
|
-
return await this
|
|
204
|
+
return await this.#createPageForBrowser(id, browserController, pageOptions, proxyUrl, ignoreTlsErrors);
|
|
162
205
|
});
|
|
163
206
|
}
|
|
164
207
|
/**
|
|
@@ -167,13 +210,13 @@ export class BrowserPool extends TypedEmitter {
|
|
|
167
210
|
* configure the new browser.
|
|
168
211
|
*/
|
|
169
212
|
async newPageInNewBrowser(options = {}) {
|
|
170
|
-
const { id = nanoid(), pageOptions, launchOptions, browserPlugin = this
|
|
213
|
+
const { id = nanoid(), pageOptions, launchOptions, browserPlugin = this.#pickBrowserPlugin() } = options;
|
|
171
214
|
if (this.pages.has(id)) {
|
|
172
215
|
throw new Error(`Page with ID: ${id} already exists.`);
|
|
173
216
|
}
|
|
174
|
-
const browserController = await this
|
|
217
|
+
const browserController = await this.#launchBrowser(id, { launchOptions, browserPlugin });
|
|
175
218
|
tryCancel();
|
|
176
|
-
return await this
|
|
219
|
+
return await this.#createPageForBrowser(id, browserController, pageOptions);
|
|
177
220
|
}
|
|
178
221
|
/**
|
|
179
222
|
* Opens new pages with all available plugins and returns an array
|
|
@@ -219,7 +262,7 @@ export class BrowserPool extends TypedEmitter {
|
|
|
219
262
|
* @param page - Browser plugin page
|
|
220
263
|
*/
|
|
221
264
|
getBrowserControllerByPage(page) {
|
|
222
|
-
return this
|
|
265
|
+
return this.#pageToBrowserController.get(page);
|
|
223
266
|
}
|
|
224
267
|
/**
|
|
225
268
|
* If you provided a custom ID to one of your pages or saved the
|
|
@@ -237,40 +280,45 @@ export class BrowserPool extends TypedEmitter {
|
|
|
237
280
|
* until it's closed.
|
|
238
281
|
*/
|
|
239
282
|
getPageId(page) {
|
|
240
|
-
return this
|
|
283
|
+
return this.#pageIds.get(page);
|
|
241
284
|
}
|
|
242
|
-
async
|
|
285
|
+
async #createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl, ignoreTlsErrors) {
|
|
243
286
|
// This is needed for concurrent newPage calls to wait for the browser launch.
|
|
244
287
|
// It's not ideal though, we need to come up with a better API.
|
|
245
|
-
|
|
246
|
-
await browserController['isActivePromise'];
|
|
288
|
+
await browserController.waitForActive();
|
|
247
289
|
tryCancel();
|
|
248
290
|
const finalPageOptions = browserController.launchContext.useIncognitoPages ? pageOptions : undefined;
|
|
249
291
|
if (finalPageOptions) {
|
|
250
292
|
Object.assign(finalPageOptions, browserController.normalizeProxyOptions(proxyUrl, pageOptions));
|
|
293
|
+
if (ignoreTlsErrors) {
|
|
294
|
+
Object.assign(finalPageOptions, {
|
|
295
|
+
ignoreHTTPSErrors: true,
|
|
296
|
+
acceptInsecureCerts: true,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
251
299
|
}
|
|
252
|
-
await this
|
|
300
|
+
await this.#executeHooks(this.#prePageCreateHooks, pageId, browserController, finalPageOptions);
|
|
253
301
|
tryCancel();
|
|
254
302
|
let page;
|
|
255
303
|
try {
|
|
256
|
-
page = (await addTimeoutToPromise(async () => browserController.newPage(finalPageOptions), this
|
|
304
|
+
page = (await addTimeoutToPromise(async () => browserController.newPage(finalPageOptions), this.#operationTimeoutMillis, 'browserController.newPage() timed out.'));
|
|
257
305
|
tryCancel();
|
|
258
306
|
this.pages.set(pageId, page);
|
|
259
|
-
this
|
|
260
|
-
this
|
|
307
|
+
this.#pageIds.set(page, pageId);
|
|
308
|
+
this.#pageToBrowserController.set(page, browserController);
|
|
261
309
|
// if you synchronously trigger a lot of page launches, browser will not get retired soon enough. Not sure if it's a problem, let's monitor it.
|
|
262
|
-
if (browserController.totalPages >= this
|
|
310
|
+
if (browserController.totalPages >= this.#retireBrowserAfterPageCount) {
|
|
263
311
|
this.retireBrowserController(browserController);
|
|
264
312
|
}
|
|
265
|
-
this
|
|
313
|
+
this.#overridePageClose(page);
|
|
266
314
|
}
|
|
267
315
|
catch (err) {
|
|
268
316
|
this.retireBrowserController(browserController);
|
|
269
317
|
throw new Error(`browserController.newPage() failed: ${browserController.id}\nCause:${err.message}.`);
|
|
270
318
|
}
|
|
271
|
-
await this
|
|
319
|
+
await this.#executeHooks(this.#postPageCreateHooks, page, browserController);
|
|
272
320
|
tryCancel();
|
|
273
|
-
this.emit(
|
|
321
|
+
this.emit(BROWSER_POOL_EVENTS.PAGE_CREATED, page);
|
|
274
322
|
return page;
|
|
275
323
|
}
|
|
276
324
|
/**
|
|
@@ -279,14 +327,14 @@ export class BrowserPool extends TypedEmitter {
|
|
|
279
327
|
*
|
|
280
328
|
*/
|
|
281
329
|
retireBrowserController(browserController) {
|
|
282
|
-
const isStarting = this
|
|
330
|
+
const isStarting = this.#startingBrowserControllers.has(browserController);
|
|
283
331
|
const isActive = this.activeBrowserControllers.has(browserController);
|
|
284
332
|
const hasBeenRetiredOrKilled = !isStarting && !isActive;
|
|
285
333
|
if (hasBeenRetiredOrKilled)
|
|
286
334
|
return;
|
|
287
335
|
this.retiredBrowserControllers.add(browserController);
|
|
288
|
-
this.emit(
|
|
289
|
-
this
|
|
336
|
+
this.emit(BROWSER_POOL_EVENTS.BROWSER_RETIRED, browserController);
|
|
337
|
+
this.#startingBrowserControllers.delete(browserController);
|
|
290
338
|
this.activeBrowserControllers.delete(browserController);
|
|
291
339
|
}
|
|
292
340
|
/**
|
|
@@ -298,12 +346,68 @@ export class BrowserPool extends TypedEmitter {
|
|
|
298
346
|
if (browserController)
|
|
299
347
|
this.retireBrowserController(browserController);
|
|
300
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Releases a page back to the pool. The page is closed and, if the
|
|
351
|
+
* optional `error` is a {@link SessionError} (including
|
|
352
|
+
* {@link SessionRetiredError}), the browser controller that served
|
|
353
|
+
* the page is retired so that the finished session's state (cookies,
|
|
354
|
+
* storage, etc.) cannot leak into future sessions.
|
|
355
|
+
*
|
|
356
|
+
* This is the primary way the crawler should return pages to the pool.
|
|
357
|
+
*
|
|
358
|
+
* @param page The page to release.
|
|
359
|
+
* @param options.error The error that caused the page to be released, if any.
|
|
360
|
+
*/
|
|
361
|
+
async closePage(page, options) {
|
|
362
|
+
if (options?.error instanceof SessionError) {
|
|
363
|
+
this.retireBrowserByPage(page);
|
|
364
|
+
}
|
|
365
|
+
// The bound on this lives in `overridePageClose`, where it covers the pool's own
|
|
366
|
+
// bookkeeping as well. Wrapping it here instead would time out the override and abandon
|
|
367
|
+
// that bookkeeping, leaving the browser holding a slot it can never get back.
|
|
368
|
+
await page.close();
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Extracts the relevant state (currently just cookies) from a page via its
|
|
372
|
+
* owning {@link BrowserController}. Returns empty state when the page is
|
|
373
|
+
* no longer associated with a controller.
|
|
374
|
+
*
|
|
375
|
+
* As with {@link BrowserPool.injectPageState}, cookies are isolated per
|
|
376
|
+
* page only when the pool is configured with `useIncognitoPages: true`.
|
|
377
|
+
* With the default `useIncognitoPages: false`, the extracted cookies
|
|
378
|
+
* include those set by any sibling page sharing the same browser.
|
|
379
|
+
*/
|
|
380
|
+
async extractPageState(page) {
|
|
381
|
+
const controller = this.getBrowserControllerByPage(page);
|
|
382
|
+
if (!controller) {
|
|
383
|
+
return { cookies: [] };
|
|
384
|
+
}
|
|
385
|
+
return { cookies: await controller.getCookies(page) };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Injects state into a page via its owning {@link BrowserController}.
|
|
389
|
+
*
|
|
390
|
+
* No-op when the page is no longer associated with a controller.
|
|
391
|
+
*
|
|
392
|
+
* Note that cookies are isolated per page only when the pool is configured
|
|
393
|
+
* with `useIncognitoPages: true` — each page then gets its own browser
|
|
394
|
+
* context. With the default `useIncognitoPages: false`, all pages in a
|
|
395
|
+
* browser share a single context, so injected cookies are visible to every
|
|
396
|
+
* page served by that browser.
|
|
397
|
+
*/
|
|
398
|
+
async injectPageState(page, state) {
|
|
399
|
+
const controller = this.getBrowserControllerByPage(page);
|
|
400
|
+
if (!controller) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await controller.setCookies(page, state.cookies);
|
|
404
|
+
}
|
|
301
405
|
/**
|
|
302
406
|
* Removes all active browsers from the pool. The browsers will be
|
|
303
407
|
* closed after all their pages are closed.
|
|
304
408
|
*/
|
|
305
409
|
retireAllBrowsers() {
|
|
306
|
-
[...this
|
|
410
|
+
[...this.#startingBrowserControllers, ...this.activeBrowserControllers].forEach((controller) => {
|
|
307
411
|
this.retireBrowserController(controller);
|
|
308
412
|
});
|
|
309
413
|
}
|
|
@@ -312,71 +416,86 @@ export class BrowserPool extends TypedEmitter {
|
|
|
312
416
|
* @return {Promise<void>}
|
|
313
417
|
*/
|
|
314
418
|
async closeAllBrowsers() {
|
|
315
|
-
const controllers = this
|
|
419
|
+
const controllers = this.#getAllBrowserControllers();
|
|
316
420
|
const promises = [...controllers]
|
|
317
421
|
.filter((controller) => controller.isActive)
|
|
318
422
|
.map(async (controller) => controller.close());
|
|
319
423
|
await Promise.all(promises);
|
|
320
424
|
}
|
|
425
|
+
async [Symbol.asyncDispose]() {
|
|
426
|
+
await this.destroy();
|
|
427
|
+
}
|
|
321
428
|
/**
|
|
322
|
-
* Closes
|
|
429
|
+
* Closes every managed browser and empties the pool, which stays usable afterwards — a crawler releases its
|
|
430
|
+
* browsers when a run ends and may start another run on the same pool.
|
|
323
431
|
*/
|
|
324
|
-
async
|
|
325
|
-
clearInterval(this.browserKillerInterval);
|
|
326
|
-
clearInterval(this.browserRetireInterval);
|
|
327
|
-
this.browserKillerInterval = undefined;
|
|
328
|
-
this.browserRetireInterval = undefined;
|
|
432
|
+
async releaseAllBrowsers() {
|
|
329
433
|
await this.closeAllBrowsers();
|
|
330
|
-
this.
|
|
331
|
-
}
|
|
332
|
-
_teardown() {
|
|
333
|
-
this.startingBrowserControllers.clear();
|
|
434
|
+
this.#startingBrowserControllers.clear();
|
|
334
435
|
this.activeBrowserControllers.clear();
|
|
335
436
|
this.retiredBrowserControllers.clear();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Closes all managed browsers and tears the pool down for good: its intervals are cleared without being
|
|
440
|
+
* re-armed and its listeners are dropped, so it cannot be used again.
|
|
441
|
+
*/
|
|
442
|
+
async destroy() {
|
|
443
|
+
clearInterval(this.browserKillerInterval);
|
|
444
|
+
clearInterval(this.#browserRetireInterval);
|
|
445
|
+
this.browserKillerInterval = undefined;
|
|
446
|
+
this.#browserRetireInterval = undefined;
|
|
447
|
+
await this.releaseAllBrowsers();
|
|
336
448
|
this.removeAllListeners();
|
|
337
449
|
}
|
|
338
|
-
|
|
450
|
+
#getAllBrowserControllers() {
|
|
339
451
|
return new Set([
|
|
340
|
-
...this
|
|
452
|
+
...this.#startingBrowserControllers,
|
|
341
453
|
...this.activeBrowserControllers,
|
|
342
454
|
...this.retiredBrowserControllers,
|
|
343
455
|
]);
|
|
344
456
|
}
|
|
345
|
-
async
|
|
346
|
-
const { browserPlugin, launchOptions,
|
|
457
|
+
async #launchBrowser(pageId, options) {
|
|
458
|
+
const { browserPlugin, launchOptions, proxyUrl, ignoreTlsErrors } = options;
|
|
347
459
|
const browserController = browserPlugin.createController();
|
|
348
|
-
this
|
|
460
|
+
this.#startingBrowserControllers.add(browserController);
|
|
349
461
|
const launchContext = browserPlugin.createLaunchContext({
|
|
350
462
|
id: pageId,
|
|
351
463
|
launchOptions,
|
|
352
|
-
proxyTier,
|
|
353
464
|
proxyUrl,
|
|
354
465
|
});
|
|
466
|
+
// Disable SSL verification for MITM proxies
|
|
467
|
+
if (ignoreTlsErrors) {
|
|
468
|
+
/**
|
|
469
|
+
* @see https://playwright.dev/docs/api/class-browser/#browser-new-context
|
|
470
|
+
* @see https://github.com/puppeteer/puppeteer/blob/main/docs/api.md
|
|
471
|
+
*/
|
|
472
|
+
launchContext.launchOptions.ignoreHTTPSErrors = true;
|
|
473
|
+
launchContext.launchOptions.acceptInsecureCerts = true;
|
|
474
|
+
}
|
|
355
475
|
try {
|
|
356
476
|
// If the hooks or the launch fails, we need to delete the controller,
|
|
357
477
|
// because otherwise it would be stuck in limbo without a browser.
|
|
358
|
-
await this
|
|
478
|
+
await this.#executeHooks(this.#preLaunchHooks, pageId, launchContext);
|
|
359
479
|
tryCancel();
|
|
360
480
|
const browser = await browserPlugin.launch(launchContext);
|
|
361
481
|
tryCancel();
|
|
362
482
|
browserController.assignBrowser(browser, launchContext);
|
|
363
483
|
}
|
|
364
484
|
catch (err) {
|
|
365
|
-
this
|
|
485
|
+
this.#startingBrowserControllers.delete(browserController);
|
|
366
486
|
throw err;
|
|
367
487
|
}
|
|
368
|
-
log.debug('Launched new browser.', { id: browserController.id });
|
|
369
|
-
browserController.proxyTier = proxyTier;
|
|
488
|
+
this.#log.debug('Launched new browser.', { id: browserController.id });
|
|
370
489
|
browserController.proxyUrl = proxyUrl;
|
|
371
490
|
try {
|
|
372
491
|
// If the launch fails on the post-launch hooks, we need to clean up
|
|
373
492
|
// both the controller and the browser before throwing.
|
|
374
|
-
await this
|
|
493
|
+
await this.#executeHooks(this.#postLaunchHooks, pageId, browserController);
|
|
375
494
|
}
|
|
376
495
|
catch (err) {
|
|
377
|
-
this
|
|
496
|
+
this.#startingBrowserControllers.delete(browserController);
|
|
378
497
|
browserController.close().catch((closeErr) => {
|
|
379
|
-
log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
|
|
498
|
+
this.#log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
|
|
380
499
|
id: browserController.id,
|
|
381
500
|
});
|
|
382
501
|
});
|
|
@@ -384,105 +503,157 @@ export class BrowserPool extends TypedEmitter {
|
|
|
384
503
|
}
|
|
385
504
|
tryCancel();
|
|
386
505
|
browserController.activate();
|
|
387
|
-
this
|
|
506
|
+
this.#startingBrowserControllers.delete(browserController);
|
|
388
507
|
this.activeBrowserControllers.add(browserController);
|
|
389
|
-
this.emit(
|
|
508
|
+
this.emit(BROWSER_POOL_EVENTS.BROWSER_LAUNCHED, browserController);
|
|
390
509
|
return browserController;
|
|
391
510
|
}
|
|
392
511
|
/**
|
|
393
512
|
* Picks plugins round robin.
|
|
394
|
-
* @private
|
|
395
513
|
*/
|
|
396
|
-
|
|
397
|
-
const pluginIndex = this
|
|
398
|
-
this
|
|
514
|
+
#pickBrowserPlugin() {
|
|
515
|
+
const pluginIndex = this.#pageCounter % this.browserPlugins.length;
|
|
516
|
+
this.#pageCounter++;
|
|
399
517
|
return this.browserPlugins[pluginIndex];
|
|
400
518
|
}
|
|
401
|
-
|
|
519
|
+
#pickBrowserWithFreeCapacity(browserPlugin, options) {
|
|
402
520
|
return [...this.activeBrowserControllers].find((controller) => {
|
|
403
|
-
const hasCapacity = controller.activePages < this
|
|
521
|
+
const hasCapacity = controller.activePages < this.#maxOpenPagesPerBrowser;
|
|
404
522
|
const isCorrectPlugin = controller.browserPlugin === browserPlugin;
|
|
405
523
|
const isSameProxyUrl = controller.proxyUrl === options?.proxyUrl;
|
|
406
|
-
const isCorrectProxyTier = controller.proxyTier === options?.proxyTier;
|
|
407
524
|
return (isCorrectPlugin &&
|
|
408
525
|
hasCapacity &&
|
|
409
|
-
(
|
|
410
|
-
(options?.proxyTier && isCorrectProxyTier) ||
|
|
526
|
+
(!controller.launchContext.browserPerProxy ||
|
|
411
527
|
(options?.proxyUrl && isSameProxyUrl) ||
|
|
412
|
-
(!options?.proxyUrl && !
|
|
528
|
+
(!options?.proxyUrl && !controller.proxyUrl)));
|
|
413
529
|
});
|
|
414
530
|
}
|
|
415
|
-
|
|
531
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
|
|
532
|
+
async closeInactiveRetiredBrowsers() {
|
|
416
533
|
const closedBrowserIds = [];
|
|
417
534
|
for (const controller of this.retiredBrowserControllers) {
|
|
418
535
|
const millisSinceLastPageOpened = Date.now() - controller.lastPageOpenedAt;
|
|
419
|
-
const isBrowserIdle = millisSinceLastPageOpened >= this
|
|
536
|
+
const isBrowserIdle = millisSinceLastPageOpened >= this.#closeInactiveBrowserAfterMillis;
|
|
420
537
|
const isBrowserEmpty = controller.activePages === 0;
|
|
421
538
|
if (isBrowserIdle || isBrowserEmpty) {
|
|
422
539
|
const { id } = controller;
|
|
423
|
-
log.debug('Closing retired browser.', { id });
|
|
540
|
+
this.#log.debug('Closing retired browser.', { id });
|
|
424
541
|
await controller.close();
|
|
425
542
|
this.retiredBrowserControllers.delete(controller);
|
|
426
543
|
closedBrowserIds.push(id);
|
|
427
544
|
}
|
|
428
545
|
}
|
|
429
546
|
if (closedBrowserIds.length) {
|
|
430
|
-
log.debug('Closed retired browsers.', {
|
|
547
|
+
this.#log.debug('Closed retired browsers.', {
|
|
431
548
|
count: closedBrowserIds.length,
|
|
432
549
|
closedBrowserIds,
|
|
433
550
|
});
|
|
434
551
|
}
|
|
435
552
|
}
|
|
436
|
-
|
|
553
|
+
#overridePageClose(page) {
|
|
437
554
|
const originalPageClose = page.close;
|
|
438
|
-
const browserController = this
|
|
555
|
+
const browserController = this.#pageToBrowserController.get(page);
|
|
439
556
|
const pageId = this.getPageId(page);
|
|
440
557
|
page.close = async (...args) => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
558
|
+
// A browser can acknowledge the close and then never destroy the target, in which case
|
|
559
|
+
// this never settles and the page's own `close` event never fires either (Chromium
|
|
560
|
+
// 536385539). Bound the whole sequence so nothing here can hang the caller, then
|
|
561
|
+
// reconcile the pool regardless of the outcome, so a page that refuses to close cannot
|
|
562
|
+
// leave the pool believing it is still open. `Promise.race` rather than
|
|
563
|
+
// `addTimeoutToPromise`: the latter inherits its AbortController from the calling frame,
|
|
564
|
+
// so a timeout here would cancel the task of whoever awaited `page.close()`.
|
|
565
|
+
let pageClosed = false;
|
|
566
|
+
const closing = (async () => {
|
|
567
|
+
await this.#executeHooks(this.#prePageCloseHooks, page, browserController);
|
|
568
|
+
await originalPageClose.apply(page, args).catch((err) => {
|
|
569
|
+
this.#log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
|
|
570
|
+
});
|
|
571
|
+
// Whether the page itself is gone. Tracked separately from the sequence finishing,
|
|
572
|
+
// so that a slow hook does not get the browser retired.
|
|
573
|
+
pageClosed = true;
|
|
574
|
+
await this.#executeHooks(this.#postPageCloseHooks, pageId, browserController);
|
|
575
|
+
})();
|
|
576
|
+
let timeout;
|
|
577
|
+
const finished = await Promise.race([
|
|
578
|
+
closing.then(() => true, (err) => {
|
|
579
|
+
this.#log.warning(`Closing a page failed, releasing it from the pool anyway.\nCause:${err.message}`, { id: browserController.id, pageId });
|
|
580
|
+
return true;
|
|
581
|
+
}),
|
|
582
|
+
new Promise((resolve) => {
|
|
583
|
+
timeout = setTimeout(() => resolve(false), PAGE_CLOSE_TIMEOUT_MILLIS);
|
|
584
|
+
}),
|
|
585
|
+
]);
|
|
586
|
+
clearTimeout(timeout);
|
|
587
|
+
if (!finished) {
|
|
588
|
+
this.#log.warning(`Closing a page did not finish within ${PAGE_CLOSE_TIMEOUT_MILLIS / 1000} seconds, ` +
|
|
589
|
+
'releasing it from the pool anyway.', { id: browserController.id, pageId });
|
|
590
|
+
}
|
|
591
|
+
if (!pageClosed) {
|
|
592
|
+
// The page is still attached, so this browser cannot be trusted with more work.
|
|
593
|
+
// Retiring it lets the reclamation below close the process once its pages drain.
|
|
594
|
+
this.retireBrowserController(browserController);
|
|
595
|
+
}
|
|
596
|
+
browserController.registerPageClosed(page);
|
|
446
597
|
this.pages.delete(pageId);
|
|
447
|
-
this
|
|
448
|
-
this.emit(
|
|
598
|
+
this.#closeRetiredBrowserWithNoPages(browserController);
|
|
599
|
+
this.emit(BROWSER_POOL_EVENTS.PAGE_CLOSED, page);
|
|
449
600
|
};
|
|
450
601
|
}
|
|
451
|
-
async
|
|
602
|
+
async #executeHooks(hooks, ...args) {
|
|
452
603
|
for (const hook of hooks) {
|
|
453
604
|
await hook(...args);
|
|
454
605
|
}
|
|
455
606
|
}
|
|
456
|
-
|
|
607
|
+
#closeRetiredBrowserWithNoPages(browserController) {
|
|
457
608
|
if (browserController.activePages === 0 && this.retiredBrowserControllers.has(browserController)) {
|
|
458
609
|
// Run this with a delay, otherwise page.close()
|
|
459
610
|
// might fail with "Protocol error (Target.closeTarget): Target closed."
|
|
460
611
|
setTimeout(() => {
|
|
461
|
-
log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
|
|
612
|
+
this.#log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
|
|
462
613
|
void browserController.close().finally(() => {
|
|
463
614
|
this.retiredBrowserControllers.delete(browserController);
|
|
464
615
|
});
|
|
465
616
|
}, PAGE_CLOSE_KILL_TIMEOUT_MILLIS);
|
|
466
617
|
}
|
|
467
618
|
}
|
|
468
|
-
|
|
619
|
+
/**
|
|
620
|
+
* Returns `true` if the pool can accept a new browser launch without exceeding `maxOpenBrowsers`.
|
|
621
|
+
* Counts starting, active, and retired browsers.
|
|
622
|
+
*
|
|
623
|
+
* @internal
|
|
624
|
+
*/
|
|
625
|
+
hasFreeBrowserSlot() {
|
|
626
|
+
const total = this.#startingBrowserControllers.size +
|
|
627
|
+
this.activeBrowserControllers.size +
|
|
628
|
+
this.retiredBrowserControllers.size;
|
|
629
|
+
return total < this.#maxOpenBrowsers;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Resolves on the next browser-retired / page-closed event, or after a short fallback interval. All
|
|
633
|
+
* concurrently-waiting `newPage` calls share a single promise (and a single pair of event listeners) per tick.
|
|
634
|
+
*/
|
|
635
|
+
#nextCapacityChange() {
|
|
636
|
+
this.#capacityChange ??= new Promise((resolve) => {
|
|
637
|
+
const done = () => {
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
this.off(BROWSER_POOL_EVENTS.BROWSER_RETIRED, done);
|
|
640
|
+
this.off(BROWSER_POOL_EVENTS.PAGE_CLOSED, done);
|
|
641
|
+
this.#capacityChange = undefined;
|
|
642
|
+
resolve();
|
|
643
|
+
};
|
|
644
|
+
const timer = setTimeout(done, SLOT_POLL_INTERVAL_MILLIS);
|
|
645
|
+
timer.unref?.();
|
|
646
|
+
this.once(BROWSER_POOL_EVENTS.BROWSER_RETIRED, done);
|
|
647
|
+
this.once(BROWSER_POOL_EVENTS.PAGE_CLOSED, done);
|
|
648
|
+
});
|
|
649
|
+
return this.#capacityChange;
|
|
650
|
+
}
|
|
651
|
+
#initializeFingerprinting() {
|
|
469
652
|
const { useFingerprintCache = true, fingerprintCacheSize = 10_000 } = this.fingerprintOptions;
|
|
470
653
|
this.fingerprintGenerator = new FingerprintGenerator(this.fingerprintOptions.fingerprintGeneratorOptions);
|
|
471
654
|
this.fingerprintInjector = new FingerprintInjector();
|
|
472
655
|
if (useFingerprintCache) {
|
|
473
656
|
this.fingerprintCache = new QuickLRU({ maxSize: fingerprintCacheSize });
|
|
474
657
|
}
|
|
475
|
-
this._addFingerprintHooks();
|
|
476
|
-
}
|
|
477
|
-
_addFingerprintHooks() {
|
|
478
|
-
this.preLaunchHooks = [
|
|
479
|
-
...this.preLaunchHooks,
|
|
480
|
-
// This is flipped because of the fingerprint cache.
|
|
481
|
-
// It is usual to generate proxy per browser and we want to know the proxyUrl for the caching.
|
|
482
|
-
createFingerprintPreLaunchHook(this),
|
|
483
|
-
];
|
|
484
|
-
this.prePageCreateHooks = [createPrePageCreateHook(), ...this.prePageCreateHooks];
|
|
485
|
-
this.postPageCreateHooks = [createPostPageCreateHook(this.fingerprintInjector), ...this.postPageCreateHooks];
|
|
486
658
|
}
|
|
487
659
|
}
|
|
488
|
-
//# sourceMappingURL=browser-pool.js.map
|