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