@allwright.dev/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/package.json +53 -0
- package/proto/core/v1/browser.proto +55 -0
- package/proto/core/v1/common.proto +15 -0
- package/proto/core/v1/tab.proto +72 -0
- package/proto/engine/v1/engine.proto +14 -0
- package/proto/surfaces/web/v1/web.proto +150 -0
- package/typescript/dist/index.d.ts +408 -0
- package/typescript/dist/index.js +690 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import grpc from "@grpc/grpc-js";
|
|
4
|
+
import protoLoader from "@grpc/proto-loader";
|
|
5
|
+
const DEFAULT_SERVER_ADDR = "127.0.0.1:50051";
|
|
6
|
+
const SERVER_ADDR_ENV_VAR = "ALLWRIGHT_SERVER_ADDR";
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const PROTO_ROOT = path.join(REPO_ROOT, "proto");
|
|
11
|
+
const ENGINE_PROTO_PATH = path.join(REPO_ROOT, "proto", "engine", "v1", "engine.proto");
|
|
12
|
+
let runtimePromise = null;
|
|
13
|
+
let serverAddrOverride = null;
|
|
14
|
+
class EventQueue {
|
|
15
|
+
#items = [];
|
|
16
|
+
#waiters = [];
|
|
17
|
+
#endedError = null;
|
|
18
|
+
push(item) {
|
|
19
|
+
const waiter = this.#waiters.shift();
|
|
20
|
+
if (waiter) {
|
|
21
|
+
waiter.resolve(item);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.#items.push(item);
|
|
25
|
+
}
|
|
26
|
+
fail(error) {
|
|
27
|
+
if (this.#endedError) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.#endedError = error;
|
|
31
|
+
while (this.#waiters.length > 0) {
|
|
32
|
+
this.#waiters.shift().reject(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async next() {
|
|
36
|
+
if (this.#items.length > 0) {
|
|
37
|
+
return this.#items.shift();
|
|
38
|
+
}
|
|
39
|
+
if (this.#endedError) {
|
|
40
|
+
throw this.#endedError;
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
this.#waiters.push({ resolve, reject });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class BrowserType {
|
|
48
|
+
async launch(options = {}) {
|
|
49
|
+
return launchChrome(options);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class Browser {
|
|
53
|
+
#closed = false;
|
|
54
|
+
#runtime;
|
|
55
|
+
#stream;
|
|
56
|
+
#queue;
|
|
57
|
+
#pages = new Map();
|
|
58
|
+
#initialPage;
|
|
59
|
+
constructor(state) {
|
|
60
|
+
const browserInfo = {
|
|
61
|
+
sessionId: state.sessionId,
|
|
62
|
+
browserName: state.chromeLaunched.browser ?? "",
|
|
63
|
+
launchNote: state.chromeLaunched.note ?? "",
|
|
64
|
+
cdpWebSocketURL: state.chromeLaunched.cdpWebsocketUrl ?? "",
|
|
65
|
+
userDataDir: state.chromeLaunched.userDataDir ?? "",
|
|
66
|
+
};
|
|
67
|
+
this.#runtime = state.runtime;
|
|
68
|
+
this.#stream = state.stream;
|
|
69
|
+
this.#queue = state.queue;
|
|
70
|
+
this.sessionId = browserInfo.sessionId;
|
|
71
|
+
this.browserName = browserInfo.browserName;
|
|
72
|
+
this.launchNote = browserInfo.launchNote;
|
|
73
|
+
this.cdpWebSocketURL = browserInfo.cdpWebSocketURL;
|
|
74
|
+
this.userDataDir = browserInfo.userDataDir;
|
|
75
|
+
this.#initialPage = this.#createPage(state.chromeLaunched.initialTabSessionId ?? "");
|
|
76
|
+
}
|
|
77
|
+
sessionId;
|
|
78
|
+
browserName;
|
|
79
|
+
launchNote;
|
|
80
|
+
cdpWebSocketURL;
|
|
81
|
+
userDataDir;
|
|
82
|
+
page() {
|
|
83
|
+
return this.#initialPage;
|
|
84
|
+
}
|
|
85
|
+
initialPage() {
|
|
86
|
+
return this.#initialPage;
|
|
87
|
+
}
|
|
88
|
+
pages() {
|
|
89
|
+
return [...this.#pages.values()];
|
|
90
|
+
}
|
|
91
|
+
async newPage(options = {}) {
|
|
92
|
+
this.#ensureOpen();
|
|
93
|
+
this.#stream.write({
|
|
94
|
+
openTab: {
|
|
95
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
while (true) {
|
|
99
|
+
const event = await this.#queue.next();
|
|
100
|
+
if (event.tabOpened?.tabSessionId) {
|
|
101
|
+
return this.#createPage(event.tabOpened.tabSessionId);
|
|
102
|
+
}
|
|
103
|
+
if (event.error?.message) {
|
|
104
|
+
throw new Error(`browser session error while opening tab: ${event.error.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async close() {
|
|
109
|
+
if (this.#closed) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.#stream.write({
|
|
113
|
+
close: {},
|
|
114
|
+
});
|
|
115
|
+
while (true) {
|
|
116
|
+
const event = await this.#queue.next();
|
|
117
|
+
if (event.closed) {
|
|
118
|
+
this.#closed = true;
|
|
119
|
+
this.#stream.end();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (event.error?.message) {
|
|
123
|
+
throw new Error(`browser session error while closing: ${event.error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async ping(message = "ping") {
|
|
128
|
+
this.#ensureOpen();
|
|
129
|
+
this.#stream.write({
|
|
130
|
+
ping: {
|
|
131
|
+
message,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
while (true) {
|
|
135
|
+
const event = await this.#queue.next();
|
|
136
|
+
if (event.pong?.message) {
|
|
137
|
+
return event.pong.message;
|
|
138
|
+
}
|
|
139
|
+
if (event.error?.message) {
|
|
140
|
+
throw new Error(`browser session error while pinging: ${event.error.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
browserInfo() {
|
|
145
|
+
return {
|
|
146
|
+
sessionId: this.sessionId,
|
|
147
|
+
browserName: this.browserName,
|
|
148
|
+
launchNote: this.launchNote,
|
|
149
|
+
cdpWebSocketURL: this.cdpWebSocketURL,
|
|
150
|
+
userDataDir: this.userDataDir,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
initialTab() {
|
|
154
|
+
return this.initialPage();
|
|
155
|
+
}
|
|
156
|
+
async newTab(options = {}) {
|
|
157
|
+
return this.newPage(options);
|
|
158
|
+
}
|
|
159
|
+
#createPage(sessionId) {
|
|
160
|
+
const existing = this.#pages.get(sessionId);
|
|
161
|
+
if (existing) {
|
|
162
|
+
return existing;
|
|
163
|
+
}
|
|
164
|
+
const page = new Page({
|
|
165
|
+
runtime: this.#runtime,
|
|
166
|
+
browserSessionId: this.sessionId,
|
|
167
|
+
sessionId,
|
|
168
|
+
});
|
|
169
|
+
this.#pages.set(sessionId, page);
|
|
170
|
+
return page;
|
|
171
|
+
}
|
|
172
|
+
#ensureOpen() {
|
|
173
|
+
if (this.#closed) {
|
|
174
|
+
throw new Error(`browser session ${this.sessionId} is closed`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export class Page {
|
|
179
|
+
#runtime;
|
|
180
|
+
#handlePromise = null;
|
|
181
|
+
constructor(input) {
|
|
182
|
+
this.#runtime = input.runtime;
|
|
183
|
+
this.sessionId = input.sessionId;
|
|
184
|
+
this.browserSessionId = input.browserSessionId;
|
|
185
|
+
}
|
|
186
|
+
sessionId;
|
|
187
|
+
browserSessionId;
|
|
188
|
+
async goto(url, options = {}) {
|
|
189
|
+
const handle = await this.#getHandle();
|
|
190
|
+
this.#ensureOpen(handle);
|
|
191
|
+
handle.stream.write({
|
|
192
|
+
browserSessionId: this.browserSessionId,
|
|
193
|
+
tabSessionId: this.sessionId,
|
|
194
|
+
navigate: {
|
|
195
|
+
url,
|
|
196
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
let navigated = null;
|
|
200
|
+
let injection = null;
|
|
201
|
+
while (true) {
|
|
202
|
+
const event = await handle.queue.next();
|
|
203
|
+
if (event.navigated) {
|
|
204
|
+
navigated = event.navigated;
|
|
205
|
+
}
|
|
206
|
+
if (event.chromiumBidiInjection) {
|
|
207
|
+
injection = event.chromiumBidiInjection;
|
|
208
|
+
}
|
|
209
|
+
if (event.error?.message) {
|
|
210
|
+
throw new Error(`page session error while navigating: ${event.error.message}`);
|
|
211
|
+
}
|
|
212
|
+
if (event.closed) {
|
|
213
|
+
handle.closed = true;
|
|
214
|
+
throw new Error(`page session ${this.sessionId} closed while navigating`);
|
|
215
|
+
}
|
|
216
|
+
if (navigated && injection) {
|
|
217
|
+
return {
|
|
218
|
+
url: navigated.url ?? "",
|
|
219
|
+
note: navigated.note ?? "",
|
|
220
|
+
bidiSessionId: injection.bidiSessionId ?? "",
|
|
221
|
+
mapperTargetId: injection.mapperTargetId ?? "",
|
|
222
|
+
mapperSessionId: injection.mapperSessionId ?? "",
|
|
223
|
+
packageVersion: injection.packageVersion ?? "",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async click(selector, options = {}) {
|
|
229
|
+
const handle = await this.#getHandle();
|
|
230
|
+
this.#ensureOpen(handle);
|
|
231
|
+
handle.stream.write({
|
|
232
|
+
browserSessionId: this.browserSessionId,
|
|
233
|
+
tabSessionId: this.sessionId,
|
|
234
|
+
clickElement: {
|
|
235
|
+
cssSelector: selector,
|
|
236
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
while (true) {
|
|
240
|
+
const event = await handle.queue.next();
|
|
241
|
+
if (event.elementClicked) {
|
|
242
|
+
return {
|
|
243
|
+
selector: event.elementClicked.cssSelector ?? "",
|
|
244
|
+
note: event.elementClicked.note ?? "",
|
|
245
|
+
bidiSessionId: event.elementClicked.bidiSessionId ?? "",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (event.error?.message) {
|
|
249
|
+
throw new Error(`page session error while clicking: ${event.error.message}`);
|
|
250
|
+
}
|
|
251
|
+
if (event.closed) {
|
|
252
|
+
handle.closed = true;
|
|
253
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for click result`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async count(selector, options = {}) {
|
|
258
|
+
const handle = await this.#getHandle();
|
|
259
|
+
this.#ensureOpen(handle);
|
|
260
|
+
handle.stream.write({
|
|
261
|
+
browserSessionId: this.browserSessionId,
|
|
262
|
+
tabSessionId: this.sessionId,
|
|
263
|
+
countElements: {
|
|
264
|
+
cssSelector: selector,
|
|
265
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
while (true) {
|
|
269
|
+
const event = await handle.queue.next();
|
|
270
|
+
if (event.elementCounted) {
|
|
271
|
+
return {
|
|
272
|
+
selector: event.elementCounted.cssSelector ?? "",
|
|
273
|
+
count: event.elementCounted.count ?? 0,
|
|
274
|
+
note: event.elementCounted.note ?? "",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (event.error?.message) {
|
|
278
|
+
throw new Error(`page session error while counting elements: ${event.error.message}`);
|
|
279
|
+
}
|
|
280
|
+
if (event.closed) {
|
|
281
|
+
handle.closed = true;
|
|
282
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for count result`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async highlight(selector, options = {}) {
|
|
287
|
+
const handle = await this.#getHandle();
|
|
288
|
+
this.#ensureOpen(handle);
|
|
289
|
+
handle.stream.write({
|
|
290
|
+
browserSessionId: this.browserSessionId,
|
|
291
|
+
tabSessionId: this.sessionId,
|
|
292
|
+
highlightElements: {
|
|
293
|
+
cssSelector: selector,
|
|
294
|
+
durationMs: options.durationMs,
|
|
295
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
while (true) {
|
|
299
|
+
const event = await handle.queue.next();
|
|
300
|
+
if (event.elementsHighlighted) {
|
|
301
|
+
return {
|
|
302
|
+
selector: event.elementsHighlighted.cssSelector ?? "",
|
|
303
|
+
count: event.elementsHighlighted.count ?? 0,
|
|
304
|
+
note: event.elementsHighlighted.note ?? "",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (event.error?.message) {
|
|
308
|
+
throw new Error(`page session error while highlighting elements: ${event.error.message}`);
|
|
309
|
+
}
|
|
310
|
+
if (event.closed) {
|
|
311
|
+
handle.closed = true;
|
|
312
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for highlight result`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async focus(selector, options = {}) {
|
|
317
|
+
const handle = await this.#getHandle();
|
|
318
|
+
this.#ensureOpen(handle);
|
|
319
|
+
handle.stream.write({
|
|
320
|
+
browserSessionId: this.browserSessionId,
|
|
321
|
+
tabSessionId: this.sessionId,
|
|
322
|
+
focusElement: {
|
|
323
|
+
cssSelector: selector,
|
|
324
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
while (true) {
|
|
328
|
+
const event = await handle.queue.next();
|
|
329
|
+
if (event.elementFocused) {
|
|
330
|
+
return {
|
|
331
|
+
selector: event.elementFocused.cssSelector ?? "",
|
|
332
|
+
note: event.elementFocused.note ?? "",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (event.error?.message) {
|
|
336
|
+
throw new Error(`page session error while focusing: ${event.error.message}`);
|
|
337
|
+
}
|
|
338
|
+
if (event.closed) {
|
|
339
|
+
handle.closed = true;
|
|
340
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for focus result`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async fill(selector, value, options = {}) {
|
|
345
|
+
const handle = await this.#getHandle();
|
|
346
|
+
this.#ensureOpen(handle);
|
|
347
|
+
handle.stream.write({
|
|
348
|
+
browserSessionId: this.browserSessionId,
|
|
349
|
+
tabSessionId: this.sessionId,
|
|
350
|
+
fillElement: {
|
|
351
|
+
cssSelector: selector,
|
|
352
|
+
value,
|
|
353
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
while (true) {
|
|
357
|
+
const event = await handle.queue.next();
|
|
358
|
+
if (event.elementFilled) {
|
|
359
|
+
return {
|
|
360
|
+
selector: event.elementFilled.cssSelector ?? "",
|
|
361
|
+
value: event.elementFilled.value ?? "",
|
|
362
|
+
note: event.elementFilled.note ?? "",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (event.error?.message) {
|
|
366
|
+
throw new Error(`page session error while filling: ${event.error.message}`);
|
|
367
|
+
}
|
|
368
|
+
if (event.closed) {
|
|
369
|
+
handle.closed = true;
|
|
370
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for fill result`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async hover(selector, options = {}) {
|
|
375
|
+
const handle = await this.#getHandle();
|
|
376
|
+
this.#ensureOpen(handle);
|
|
377
|
+
handle.stream.write({
|
|
378
|
+
browserSessionId: this.browserSessionId,
|
|
379
|
+
tabSessionId: this.sessionId,
|
|
380
|
+
hoverElement: {
|
|
381
|
+
cssSelector: selector,
|
|
382
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
while (true) {
|
|
386
|
+
const event = await handle.queue.next();
|
|
387
|
+
if (event.elementHovered) {
|
|
388
|
+
return {
|
|
389
|
+
selector: event.elementHovered.cssSelector ?? "",
|
|
390
|
+
note: event.elementHovered.note ?? "",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (event.error?.message) {
|
|
394
|
+
throw new Error(`page session error while hovering: ${event.error.message}`);
|
|
395
|
+
}
|
|
396
|
+
if (event.closed) {
|
|
397
|
+
handle.closed = true;
|
|
398
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for hover result`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async press(selector, key, options = {}) {
|
|
403
|
+
const handle = await this.#getHandle();
|
|
404
|
+
this.#ensureOpen(handle);
|
|
405
|
+
handle.stream.write({
|
|
406
|
+
browserSessionId: this.browserSessionId,
|
|
407
|
+
tabSessionId: this.sessionId,
|
|
408
|
+
pressKey: {
|
|
409
|
+
cssSelector: selector,
|
|
410
|
+
key,
|
|
411
|
+
text: options.text,
|
|
412
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
while (true) {
|
|
416
|
+
const event = await handle.queue.next();
|
|
417
|
+
if (event.keyPressed) {
|
|
418
|
+
return {
|
|
419
|
+
selector: event.keyPressed.cssSelector ?? "",
|
|
420
|
+
key: event.keyPressed.key ?? "",
|
|
421
|
+
note: event.keyPressed.note ?? "",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (event.error?.message) {
|
|
425
|
+
throw new Error(`page session error while pressing key: ${event.error.message}`);
|
|
426
|
+
}
|
|
427
|
+
if (event.closed) {
|
|
428
|
+
handle.closed = true;
|
|
429
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for press result`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async textContent(selector, options = {}) {
|
|
434
|
+
return this.#readText(selector, options, true);
|
|
435
|
+
}
|
|
436
|
+
async innerText(selector, options = {}) {
|
|
437
|
+
return this.#readText(selector, options, false);
|
|
438
|
+
}
|
|
439
|
+
async waitForSelector(selector, options = {}) {
|
|
440
|
+
const handle = await this.#getHandle();
|
|
441
|
+
this.#ensureOpen(handle);
|
|
442
|
+
handle.stream.write({
|
|
443
|
+
browserSessionId: this.browserSessionId,
|
|
444
|
+
tabSessionId: this.sessionId,
|
|
445
|
+
waitForSelector: {
|
|
446
|
+
cssSelector: selector,
|
|
447
|
+
visible: options.visible,
|
|
448
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
while (true) {
|
|
452
|
+
const event = await handle.queue.next();
|
|
453
|
+
if (event.selectorWaitSatisfied) {
|
|
454
|
+
return {
|
|
455
|
+
selector: event.selectorWaitSatisfied.cssSelector ?? "",
|
|
456
|
+
visible: event.selectorWaitSatisfied.visible ?? false,
|
|
457
|
+
note: event.selectorWaitSatisfied.note ?? "",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (event.error?.message) {
|
|
461
|
+
throw new Error(`page session error while waiting for selector: ${event.error.message}`);
|
|
462
|
+
}
|
|
463
|
+
if (event.closed) {
|
|
464
|
+
handle.closed = true;
|
|
465
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for selector result`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async close() {
|
|
470
|
+
const handle = await this.#getHandle();
|
|
471
|
+
if (handle.closed) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
handle.stream.write({
|
|
475
|
+
browserSessionId: this.browserSessionId,
|
|
476
|
+
tabSessionId: this.sessionId,
|
|
477
|
+
close: {},
|
|
478
|
+
});
|
|
479
|
+
while (true) {
|
|
480
|
+
const event = await handle.queue.next();
|
|
481
|
+
if (event.closed) {
|
|
482
|
+
handle.closed = true;
|
|
483
|
+
handle.stream.end();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (event.error?.message) {
|
|
487
|
+
throw new Error(`page session error while closing: ${event.error.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async ping(message = "ping") {
|
|
492
|
+
const handle = await this.#getHandle();
|
|
493
|
+
this.#ensureOpen(handle);
|
|
494
|
+
handle.stream.write({
|
|
495
|
+
browserSessionId: this.browserSessionId,
|
|
496
|
+
tabSessionId: this.sessionId,
|
|
497
|
+
ping: {
|
|
498
|
+
message,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
while (true) {
|
|
502
|
+
const event = await handle.queue.next();
|
|
503
|
+
if (event.pong?.message) {
|
|
504
|
+
return event.pong.message;
|
|
505
|
+
}
|
|
506
|
+
if (event.error?.message) {
|
|
507
|
+
throw new Error(`page session error while pinging: ${event.error.message}`);
|
|
508
|
+
}
|
|
509
|
+
if (event.closed) {
|
|
510
|
+
handle.closed = true;
|
|
511
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for pong`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
pageInfo() {
|
|
516
|
+
return {
|
|
517
|
+
sessionId: this.sessionId,
|
|
518
|
+
browserSessionId: this.browserSessionId,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
async navigate(url, options = {}) {
|
|
522
|
+
return this.goto(url, options);
|
|
523
|
+
}
|
|
524
|
+
async #readText(selector, options, textContent) {
|
|
525
|
+
const handle = await this.#getHandle();
|
|
526
|
+
this.#ensureOpen(handle);
|
|
527
|
+
handle.stream.write(textContent
|
|
528
|
+
? {
|
|
529
|
+
browserSessionId: this.browserSessionId,
|
|
530
|
+
tabSessionId: this.sessionId,
|
|
531
|
+
getTextContent: {
|
|
532
|
+
cssSelector: selector,
|
|
533
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
: {
|
|
537
|
+
browserSessionId: this.browserSessionId,
|
|
538
|
+
tabSessionId: this.sessionId,
|
|
539
|
+
getInnerText: {
|
|
540
|
+
cssSelector: selector,
|
|
541
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
while (true) {
|
|
545
|
+
const event = await handle.queue.next();
|
|
546
|
+
if (event.textContentResolved) {
|
|
547
|
+
return {
|
|
548
|
+
selector: event.textContentResolved.cssSelector ?? "",
|
|
549
|
+
text: event.textContentResolved.text ?? "",
|
|
550
|
+
note: event.textContentResolved.note ?? "",
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (event.innerTextResolved) {
|
|
554
|
+
return {
|
|
555
|
+
selector: event.innerTextResolved.cssSelector ?? "",
|
|
556
|
+
text: event.innerTextResolved.text ?? "",
|
|
557
|
+
note: event.innerTextResolved.note ?? "",
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (event.error?.message) {
|
|
561
|
+
throw new Error(`page session error while reading text: ${event.error.message}`);
|
|
562
|
+
}
|
|
563
|
+
if (event.closed) {
|
|
564
|
+
handle.closed = true;
|
|
565
|
+
throw new Error(`page session ${this.sessionId} closed while waiting for text result`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
#ensureOpen(handle) {
|
|
570
|
+
if (handle.closed) {
|
|
571
|
+
throw new Error(`page session ${this.sessionId} is closed`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async #getHandle() {
|
|
575
|
+
if (!this.#handlePromise) {
|
|
576
|
+
this.#handlePromise = createPageHandle(this.#runtime);
|
|
577
|
+
}
|
|
578
|
+
return this.#handlePromise;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
export const chromium = new BrowserType();
|
|
582
|
+
export async function ping() {
|
|
583
|
+
const runtime = await getRuntime();
|
|
584
|
+
return new Promise((resolve, reject) => {
|
|
585
|
+
runtime.client.Ping({}, (error, response) => {
|
|
586
|
+
if (error) {
|
|
587
|
+
reject(new Error(`ping engine server: ${error.message}`));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
resolve(response.message ?? "");
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
export async function launchChrome(options = {}) {
|
|
595
|
+
const runtime = await getRuntime();
|
|
596
|
+
const stream = runtime.client.BrowserSession();
|
|
597
|
+
const queue = bindStreamQueue(stream);
|
|
598
|
+
stream.write({
|
|
599
|
+
launchChrome: {
|
|
600
|
+
chromeBinary: options.chromeBinary,
|
|
601
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
while (true) {
|
|
605
|
+
const event = await queue.next();
|
|
606
|
+
if (event.chromeLaunched) {
|
|
607
|
+
return new Browser({
|
|
608
|
+
runtime,
|
|
609
|
+
stream,
|
|
610
|
+
queue,
|
|
611
|
+
sessionId: event.sessionId ?? "",
|
|
612
|
+
chromeLaunched: event.chromeLaunched,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (event.error?.message) {
|
|
616
|
+
throw new Error(`browser session error during launch: ${event.error.message}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
export function setServerAddr(serverAddr) {
|
|
621
|
+
serverAddrOverride = normalizeServerAddr(serverAddr);
|
|
622
|
+
runtimePromise = null;
|
|
623
|
+
}
|
|
624
|
+
export async function shutdown() {
|
|
625
|
+
if (!runtimePromise) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const runtime = await runtimePromise;
|
|
629
|
+
runtime.client.close();
|
|
630
|
+
runtimePromise = null;
|
|
631
|
+
}
|
|
632
|
+
async function getRuntime() {
|
|
633
|
+
if (!runtimePromise) {
|
|
634
|
+
runtimePromise = Promise.resolve(createRuntime());
|
|
635
|
+
}
|
|
636
|
+
return runtimePromise;
|
|
637
|
+
}
|
|
638
|
+
function createRuntime() {
|
|
639
|
+
const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
|
|
640
|
+
includeDirs: [PROTO_ROOT],
|
|
641
|
+
keepCase: false,
|
|
642
|
+
longs: String,
|
|
643
|
+
enums: String,
|
|
644
|
+
defaults: true,
|
|
645
|
+
oneofs: true,
|
|
646
|
+
});
|
|
647
|
+
const proto = grpc.loadPackageDefinition(loaded);
|
|
648
|
+
const ClientCtor = proto.allwright.engine.v1.EngineService;
|
|
649
|
+
const client = new ClientCtor(configuredServerAddr(), grpc.credentials.createInsecure());
|
|
650
|
+
return { client };
|
|
651
|
+
}
|
|
652
|
+
function configuredServerAddr() {
|
|
653
|
+
if (serverAddrOverride) {
|
|
654
|
+
return serverAddrOverride;
|
|
655
|
+
}
|
|
656
|
+
return normalizeServerAddr(process.env[SERVER_ADDR_ENV_VAR] ?? DEFAULT_SERVER_ADDR);
|
|
657
|
+
}
|
|
658
|
+
function normalizeServerAddr(raw) {
|
|
659
|
+
const trimmed = raw.trim();
|
|
660
|
+
if (trimmed.startsWith("dns:") || trimmed.startsWith("unix:")) {
|
|
661
|
+
return trimmed;
|
|
662
|
+
}
|
|
663
|
+
if (trimmed.includes("://")) {
|
|
664
|
+
const parsed = new URL(trimmed);
|
|
665
|
+
return parsed.host;
|
|
666
|
+
}
|
|
667
|
+
return trimmed;
|
|
668
|
+
}
|
|
669
|
+
function bindStreamQueue(stream) {
|
|
670
|
+
const queue = new EventQueue();
|
|
671
|
+
stream.on("data", (event) => {
|
|
672
|
+
queue.push(event);
|
|
673
|
+
});
|
|
674
|
+
stream.on("error", (error) => {
|
|
675
|
+
queue.fail(new Error(`grpc stream error: ${error.message}`));
|
|
676
|
+
});
|
|
677
|
+
stream.on("end", () => {
|
|
678
|
+
queue.fail(new Error("grpc stream ended"));
|
|
679
|
+
});
|
|
680
|
+
return queue;
|
|
681
|
+
}
|
|
682
|
+
async function createPageHandle(runtime) {
|
|
683
|
+
const stream = runtime.client.TabSession();
|
|
684
|
+
const queue = bindStreamQueue(stream);
|
|
685
|
+
return {
|
|
686
|
+
stream,
|
|
687
|
+
queue,
|
|
688
|
+
closed: false,
|
|
689
|
+
};
|
|
690
|
+
}
|