/
browser-controller.ts
228 lines (193 loc) · 7.33 KB
/
browser-controller.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import { tryCancel } from '@apify/timeout';
import type { Cookie, Dictionary } from '@crawlee/types';
import { nanoid } from 'nanoid';
import { TypedEmitter } from 'tiny-typed-emitter';
import type { BrowserPlugin, CommonBrowser, CommonLibrary } from './browser-plugin';
import { throwImplementationNeeded } from './utils';
import { BROWSER_CONTROLLER_EVENTS } from '../events';
import type { LaunchContext } from '../launch-context';
import { log } from '../logger';
import type { UnwrapPromise } from '../utils';
const PROCESS_KILL_TIMEOUT_MILLIS = 5000;
export interface BrowserControllerEvents<
Library extends CommonLibrary,
LibraryOptions extends Dictionary | undefined = Parameters<Library['launch']>[0],
LaunchResult extends CommonBrowser = UnwrapPromise<ReturnType<Library['launch']>>,
NewPageOptions = Parameters<LaunchResult['newPage']>[0],
NewPageResult = UnwrapPromise<ReturnType<LaunchResult['newPage']>>,
> {
[BROWSER_CONTROLLER_EVENTS.BROWSER_CLOSED]:
(controller: BrowserController<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>) => void;
}
/**
* The `BrowserController` serves two purposes. First, it is the base class that
* specialized controllers like `PuppeteerController` or `PlaywrightController`
* extend. Second, it defines the public interface of the specialized classes
* which provide only private methods. Therefore, we do not keep documentation
* for the specialized classes, because it's the same for all of them.
* @hideconstructor
*/
export abstract class BrowserController<
Library extends CommonLibrary = CommonLibrary,
LibraryOptions extends Dictionary | undefined = Parameters<Library['launch']>[0],
LaunchResult extends CommonBrowser = UnwrapPromise<ReturnType<Library['launch']>>,
NewPageOptions = Parameters<LaunchResult['newPage']>[0],
NewPageResult = UnwrapPromise<ReturnType<LaunchResult['newPage']>>,
> extends TypedEmitter<BrowserControllerEvents<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>> {
id = nanoid();
/**
* The `BrowserPlugin` instance used to launch the browser.
*/
browserPlugin: BrowserPlugin<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>;
/**
* Browser representation of the underlying automation library.
*/
browser: LaunchResult = undefined!;
/**
* The configuration the browser was launched with.
*/
launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult> = undefined!;
/**
* The proxy tier tied to this browser controller.
* `undefined` if no tiered proxy is used.
*/
proxyTier?: number;
/**
* The proxy URL used by the browser controller. This is set every time the browser controller uses proxy (even the tiered one).
* `undefined` if no proxy is used
*/
proxyUrl?: string;
isActive = false;
activePages = 0;
totalPages = 0;
lastPageOpenedAt = Date.now();
private _activate!: () => void;
private isActivePromise = new Promise<void>((resolve) => {
this._activate = resolve;
});
private commitBrowser!: () => void;
private hasBrowserPromise = new Promise<void>((resolve) => {
this.commitBrowser = resolve;
});
constructor(browserPlugin: BrowserPlugin<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>) {
super();
this.browserPlugin = browserPlugin;
}
/**
* Activates the BrowserController. If you try to open new pages before
* activation, the pages will get queued and will only be opened after
* activate is called.
* @ignore
*/
activate(): void {
if (!this.browser) {
throw new Error('Cannot activate BrowserController without an assigned browser.');
}
this._activate();
this.isActive = true;
}
/**
* @ignore
*/
assignBrowser(browser: LaunchResult, launchContext: LaunchContext<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>): void {
if (this.browser) {
throw new Error('BrowserController already has a browser instance assigned.');
}
this.browser = browser;
this.launchContext = launchContext;
this.commitBrowser();
}
/**
* Gracefully closes the browser and makes sure
* there will be no lingering browser processes.
*
* Emits 'browserClosed' event.
*/
async close(): Promise<void> {
await this.hasBrowserPromise;
try {
await this._close();
// TODO: shouldn't this go in a finally instead?
this.isActive = false;
} catch (error) {
log.debug(`Could not close browser.\nCause: ${(error as Error).message}`, { id: this.id });
}
this.emit(BROWSER_CONTROLLER_EVENTS.BROWSER_CLOSED, this);
setTimeout(() => {
this._kill().catch((err) => {
log.debug(`Could not kill browser.\nCause: ${err.message}`, { id: this.id });
});
}, PROCESS_KILL_TIMEOUT_MILLIS);
}
/**
* Immediately kills the browser process.
*
* Emits 'browserClosed' event.
*/
async kill(): Promise<void> {
await this.hasBrowserPromise;
await this._kill();
this.emit(BROWSER_CONTROLLER_EVENTS.BROWSER_CLOSED, this);
}
/**
* Opens new browser page.
* @ignore
*/
async newPage(pageOptions?: NewPageOptions): Promise<NewPageResult> {
this.activePages++;
this.totalPages++;
await this.isActivePromise;
const page = await this._newPage(pageOptions);
tryCancel();
this.lastPageOpenedAt = Date.now();
return page;
}
async setCookies(page: NewPageResult, cookies: Cookie[]): Promise<void> {
return this._setCookies(page, cookies);
}
async getCookies(page: NewPageResult): Promise<Cookie[]> {
return this._getCookies(page);
}
/**
* @private
*/
// @ts-expect-error Give runtime error as well as compile time
protected abstract async _close(): Promise<void> {
throwImplementationNeeded('_close');
}
/**
* @private
*/
// @ts-expect-error Give runtime error as well as compile time
protected abstract async _kill(): Promise<void> {
throwImplementationNeeded('_kill');
}
/**
* @private
*/
// @ts-expect-error Give runtime error as well as compile time
protected abstract async _newPage(pageOptions?: NewPageOptions): Promise<NewPageResult> {
throwImplementationNeeded('_newPage');
}
/**
* @private
*/
// @ts-expect-error Give runtime error as well as compile time
protected abstract async _setCookies(page: NewPageResult, cookies: Cookie[]): Promise<void> {
throwImplementationNeeded('_setCookies');
}
/**
* @private
*/
// @ts-expect-error Give runtime error as well as compile time
protected abstract async _getCookies(page: NewPageResult): Promise<Cookie[]> {
throwImplementationNeeded('_getCookies');
}
/**
* @private
*/
// @ts-expect-error Give runtime error as well as compile time
abstract normalizeProxyOptions(proxyUrl: string | undefined, pageOptions: any): Record<string, unknown> {
throwImplementationNeeded('_normalizeProxyOptions');
}
}