@narumitw/pi-chrome-devtools 0.53.2 → 0.53.4
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 +1 -0
- package/dist/chunks/{browser-settings-menu-IMB6HXSU.js → browser-settings-menu-FVMCCUPM.ts} +16 -42
- package/dist/chunks/browser-settings-menu-FVMCCUPM.ts.map +7 -0
- package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4B3EQHOH.ts} +2 -2
- package/dist/chunks/chunk-4B3EQHOH.ts.map +7 -0
- package/dist/chunks/{chunk-SRT3UUIP.js → chunk-LSJSDY2M.ts} +23 -74
- package/dist/chunks/chunk-LSJSDY2M.ts.map +7 -0
- package/dist/chunks/{chunk-LLXOCRNF.js → chunk-V7N3WI5B.ts} +12 -33
- package/dist/chunks/chunk-V7N3WI5B.ts.map +7 -0
- package/dist/chunks/{menu-UJETYMSB.js → menu-MTPSLDHZ.ts} +10 -23
- package/dist/chunks/menu-MTPSLDHZ.ts.map +7 -0
- package/dist/chunks/{tools-6P7EUX37.js → tools-OVNH45IJ.ts} +17 -59
- package/dist/chunks/tools-OVNH45IJ.ts.map +7 -0
- package/dist/index.ts +18 -42
- package/dist/index.ts.map +2 -2
- package/package.json +54 -55
- package/src/browser-manager.ts +872 -942
- package/src/browser-settings-menu.ts +315 -350
- package/src/cdp-client.ts +381 -398
- package/src/chrome-devtools.ts +205 -232
- package/src/display.ts +26 -29
- package/src/lazy-tools.ts +158 -181
- package/src/menu.ts +409 -446
- package/src/render.ts +72 -81
- package/src/runtime.ts +137 -140
- package/src/screenshot.ts +170 -186
- package/src/settings.ts +472 -523
- package/src/tool-names.ts +8 -14
- package/src/tool-selector.ts +242 -271
- package/src/tools.ts +218 -226
- package/src/webmcp/discovery.ts +184 -188
- package/src/webmcp/policy.ts +296 -329
- package/src/webmcp/protocol.ts +324 -337
- package/src/webmcp/tools.ts +136 -147
- package/dist/chunks/browser-settings-menu-IMB6HXSU.js.map +0 -7
- package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
- package/dist/chunks/chunk-LLXOCRNF.js.map +0 -7
- package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
- package/dist/chunks/menu-UJETYMSB.js.map +0 -7
- package/dist/chunks/tools-6P7EUX37.js.map +0 -7
package/src/cdp-client.ts
CHANGED
|
@@ -1,52 +1,47 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
devToolsEndpoint,
|
|
3
|
+
ensureDevToolsEndpoint,
|
|
4
|
+
fetchDevToolsJson,
|
|
5
|
+
formatPageListItem,
|
|
6
|
+
launchHint,
|
|
7
|
+
withEndpointRetry,
|
|
8
8
|
} from "./browser-manager.js";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_ENDPOINT_WAIT_MS,
|
|
11
|
-
DEFAULT_TIMEOUT_MS,
|
|
12
|
-
type DevToolsPage,
|
|
13
|
-
state,
|
|
14
|
-
} from "./runtime.js";
|
|
9
|
+
import { DEFAULT_ENDPOINT_WAIT_MS, DEFAULT_TIMEOUT_MS, type DevToolsPage, state } from "./runtime.js";
|
|
15
10
|
|
|
16
11
|
interface CdpResponse<T = unknown> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
error?: { code: number; data?: unknown; message: string };
|
|
13
|
+
id: number;
|
|
14
|
+
result?: T;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
interface CdpEvent {
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
method: string;
|
|
19
|
+
params?: unknown;
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
interface PendingRequest {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
cleanup: () => void;
|
|
24
|
+
reject: (reason: unknown) => void;
|
|
25
|
+
resolve: (value: unknown) => void;
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
interface EventWaiter<T = unknown> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
cleanup: () => void;
|
|
30
|
+
method: string;
|
|
31
|
+
predicate: (params: unknown) => params is T;
|
|
32
|
+
reject: (reason: unknown) => void;
|
|
33
|
+
resolve: (params: T) => void;
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
export type CdpWebSocketConstructor = new (url: string | URL) => WebSocket;
|
|
42
37
|
|
|
43
38
|
export interface CdpOperationOptions {
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
timeoutMs?: number;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
export interface CdpConnectOptions extends CdpOperationOptions {
|
|
49
|
-
|
|
44
|
+
webSocketConstructor?: CdpWebSocketConstructor;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
const MAX_BUFFERED_EVENTS_PER_METHOD = 32;
|
|
@@ -54,423 +49,411 @@ const MAX_CDP_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
|
54
49
|
const activePageIds = new WeakMap<object, string>();
|
|
55
50
|
|
|
56
51
|
interface PageOperationOptions {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
sessionOwner?: object;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
waitMs?: number;
|
|
60
55
|
}
|
|
61
56
|
|
|
62
57
|
export async function listPages(options: PageOperationOptions = {}) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
58
|
+
const waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;
|
|
59
|
+
await ensureDevToolsEndpoint(waitMs, options.signal, options.sessionOwner);
|
|
60
|
+
return withEndpointRetry(
|
|
61
|
+
async () => {
|
|
62
|
+
const pages = await fetchDevToolsJson<DevToolsPage[]>(
|
|
63
|
+
"/json/list",
|
|
64
|
+
{ signal: options.signal },
|
|
65
|
+
options.sessionOwner,
|
|
66
|
+
);
|
|
67
|
+
return pages.filter((page) => page.type === "page" && page.webSocketDebuggerUrl);
|
|
68
|
+
},
|
|
69
|
+
waitMs,
|
|
70
|
+
options.signal,
|
|
71
|
+
);
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
export async function getPage(pageId: string, options: PageOperationOptions = {}) {
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
const pages = await listPages(options);
|
|
76
|
+
return requirePage(pageId, pages);
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
export async function resolvePage(pageId?: string, options: PageOperationOptions = {}) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
const pages = await listPages(options);
|
|
81
|
+
if (pageId) return requirePage(pageId, pages);
|
|
82
|
+
|
|
83
|
+
const page = resolveDefaultPage(pages, options.sessionOwner);
|
|
84
|
+
if (!page) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
[
|
|
87
|
+
`No Chrome pages found at ${devToolsEndpoint(options.sessionOwner)}.`,
|
|
88
|
+
"Use chrome_devtools_navigate with a URL to create a page, or open a Chrome tab manually.",
|
|
89
|
+
launchHint(options.sessionOwner),
|
|
90
|
+
].join("\n"),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return page;
|
|
100
95
|
}
|
|
101
96
|
|
|
102
|
-
export async function resolvePageForNavigation(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
) {
|
|
106
|
-
const pages = await listPages(options);
|
|
107
|
-
if (pageId) return { created: false, page: requirePage(pageId, pages) };
|
|
97
|
+
export async function resolvePageForNavigation(pageId?: string, options: PageOperationOptions = {}) {
|
|
98
|
+
const pages = await listPages(options);
|
|
99
|
+
if (pageId) return { created: false, page: requirePage(pageId, pages) };
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
const page = resolveDefaultPage(pages, options.sessionOwner);
|
|
102
|
+
if (page) return { created: false, page };
|
|
111
103
|
|
|
112
|
-
|
|
104
|
+
return { created: true, page: await createPage("about:blank", options) };
|
|
113
105
|
}
|
|
114
106
|
|
|
115
107
|
function resolveDefaultPage(pages: DevToolsPage[], owner?: object) {
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
const activePageId = owner ? activePageIds.get(owner) : state.activePageId;
|
|
109
|
+
if (!activePageId) return pages[0];
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
const activePage = pages.find((candidate) => candidate.id === activePageId);
|
|
112
|
+
if (activePage) return activePage;
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
setActivePageId(owner, undefined);
|
|
115
|
+
return pages[0];
|
|
124
116
|
}
|
|
125
117
|
|
|
126
118
|
export function setActivePageId(owner: object | undefined, pageId: string | undefined) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
if (!owner) {
|
|
120
|
+
state.activePageId = pageId;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (pageId) activePageIds.set(owner, pageId);
|
|
124
|
+
else activePageIds.delete(owner);
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
function requirePage(pageId: string, pages: DevToolsPage[]) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
].join("\n"),
|
|
147
|
-
);
|
|
128
|
+
const page = pages.find((candidate) => candidate.id === pageId);
|
|
129
|
+
if (page) return page;
|
|
130
|
+
|
|
131
|
+
const availablePages = pages.map(formatPageListItem).join("\n");
|
|
132
|
+
throw new Error(
|
|
133
|
+
[
|
|
134
|
+
`Chrome DevTools page not found: ${pageId}.`,
|
|
135
|
+
availablePages ? `Available pages:\n${availablePages}` : "No inspectable Chrome pages are currently available.",
|
|
136
|
+
].join("\n"),
|
|
137
|
+
);
|
|
148
138
|
}
|
|
149
139
|
|
|
150
140
|
export async function createPage(url: string, options: PageOperationOptions = {}) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
141
|
+
const waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;
|
|
142
|
+
await ensureDevToolsEndpoint(waitMs, options.signal, options.sessionOwner);
|
|
143
|
+
const page = await withEndpointRetry(
|
|
144
|
+
() =>
|
|
145
|
+
fetchDevToolsJson<DevToolsPage>(
|
|
146
|
+
`/json/new?${encodeURIComponent(url)}`,
|
|
147
|
+
{ method: "PUT", signal: options.signal },
|
|
148
|
+
options.sessionOwner,
|
|
149
|
+
),
|
|
150
|
+
waitMs,
|
|
151
|
+
options.signal,
|
|
152
|
+
);
|
|
153
|
+
if (page.type !== "page" || !page.webSocketDebuggerUrl) {
|
|
154
|
+
throw new Error("Chrome DevTools created a target that is not an inspectable page.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return page;
|
|
168
158
|
}
|
|
169
159
|
|
|
170
160
|
export function formatPage(page: DevToolsPage) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
161
|
+
return {
|
|
162
|
+
id: page.id,
|
|
163
|
+
type: page.type,
|
|
164
|
+
title: page.title,
|
|
165
|
+
url: page.url,
|
|
166
|
+
};
|
|
177
167
|
}
|
|
178
168
|
|
|
179
169
|
export function textResult<T>(text: string, details: T) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text" as const, text }],
|
|
172
|
+
details,
|
|
173
|
+
};
|
|
184
174
|
}
|
|
185
175
|
|
|
186
176
|
export async function withCdp<T>(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
177
|
+
page: DevToolsPage,
|
|
178
|
+
callback: (client: CdpClient) => Promise<T>,
|
|
179
|
+
options: CdpConnectOptions = {},
|
|
190
180
|
) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
181
|
+
if (!page.webSocketDebuggerUrl) throw new Error(`Page has no webSocketDebuggerUrl: ${page.id}`);
|
|
182
|
+
|
|
183
|
+
const client = await CdpClient.connect(page.webSocketDebuggerUrl, options);
|
|
184
|
+
try {
|
|
185
|
+
return await callback(client);
|
|
186
|
+
} finally {
|
|
187
|
+
client.close();
|
|
188
|
+
}
|
|
199
189
|
}
|
|
200
190
|
|
|
201
191
|
export class CdpClient {
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
waiter.reject(reason);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
private throwIfClosed() {
|
|
458
|
-
if (this.#closed) throw new Error("Chrome DevTools WebSocket is closed");
|
|
459
|
-
}
|
|
192
|
+
#closed = false;
|
|
193
|
+
#eventBuffers = new Map<string, unknown[]>();
|
|
194
|
+
#eventWaiters = new Set<EventWaiter>();
|
|
195
|
+
#nextId = 1;
|
|
196
|
+
#pending = new Map<number, PendingRequest>();
|
|
197
|
+
readonly #socket: WebSocket;
|
|
198
|
+
|
|
199
|
+
private constructor(socket: WebSocket) {
|
|
200
|
+
this.#socket = socket;
|
|
201
|
+
socket.addEventListener("message", this.onMessage);
|
|
202
|
+
socket.addEventListener("close", this.onClose);
|
|
203
|
+
socket.addEventListener("error", this.onError);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static connect(url: string, options: CdpConnectOptions = {}) {
|
|
207
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
208
|
+
const WebSocketImplementation = options.webSocketConstructor ?? WebSocket;
|
|
209
|
+
options.signal?.throwIfAborted();
|
|
210
|
+
return new Promise<CdpClient>((resolve, reject) => {
|
|
211
|
+
const socket = new WebSocketImplementation(url);
|
|
212
|
+
let settled = false;
|
|
213
|
+
const settle = (callback: () => void) => {
|
|
214
|
+
if (settled) return;
|
|
215
|
+
settled = true;
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
218
|
+
socket.removeEventListener("open", onOpen);
|
|
219
|
+
socket.removeEventListener("error", onConnectError);
|
|
220
|
+
callback();
|
|
221
|
+
};
|
|
222
|
+
const onOpen = () => settle(() => resolve(new CdpClient(socket)));
|
|
223
|
+
const onConnectError = () =>
|
|
224
|
+
settle(() => {
|
|
225
|
+
socket.close();
|
|
226
|
+
reject(new Error(`Failed to connect to Chrome DevTools WebSocket: ${url}`));
|
|
227
|
+
});
|
|
228
|
+
const onAbort = () =>
|
|
229
|
+
settle(() => {
|
|
230
|
+
socket.close();
|
|
231
|
+
reject(options.signal?.reason ?? abortError("Chrome DevTools connection aborted"));
|
|
232
|
+
});
|
|
233
|
+
const timeout = setTimeout(
|
|
234
|
+
() =>
|
|
235
|
+
settle(() => {
|
|
236
|
+
socket.close();
|
|
237
|
+
reject(new Error(`Timed out connecting to Chrome DevTools WebSocket: ${url}`));
|
|
238
|
+
}),
|
|
239
|
+
timeoutMs,
|
|
240
|
+
);
|
|
241
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
242
|
+
socket.addEventListener("open", onOpen, { once: true });
|
|
243
|
+
socket.addEventListener("error", onConnectError, { once: true });
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
send<T = unknown>(method: string, params: Record<string, unknown> = {}, options: CdpOperationOptions = {}) {
|
|
248
|
+
this.throwIfClosed();
|
|
249
|
+
options.signal?.throwIfAborted();
|
|
250
|
+
const id = this.#nextId;
|
|
251
|
+
this.#nextId += 1;
|
|
252
|
+
return new Promise<T>((resolve, reject) => {
|
|
253
|
+
const onAbort = () => {
|
|
254
|
+
this.#pending.delete(id);
|
|
255
|
+
cleanup();
|
|
256
|
+
reject(options.signal?.reason ?? abortError(`CDP command aborted: ${method}`));
|
|
257
|
+
};
|
|
258
|
+
const timeout = setTimeout(() => {
|
|
259
|
+
this.#pending.delete(id);
|
|
260
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
261
|
+
reject(new Error(`Timed out waiting for CDP response: ${method}`));
|
|
262
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
263
|
+
const cleanup = () => {
|
|
264
|
+
clearTimeout(timeout);
|
|
265
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
266
|
+
};
|
|
267
|
+
this.#pending.set(id, {
|
|
268
|
+
cleanup,
|
|
269
|
+
reject,
|
|
270
|
+
resolve: resolve as (value: unknown) => void,
|
|
271
|
+
});
|
|
272
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
273
|
+
try {
|
|
274
|
+
this.#socket.send(JSON.stringify({ id, method, params }));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.#pending.delete(id);
|
|
277
|
+
cleanup();
|
|
278
|
+
reject(error);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
waitForEvent<T>(
|
|
284
|
+
method: string,
|
|
285
|
+
predicate: (params: unknown) => params is T,
|
|
286
|
+
options: CdpOperationOptions = {},
|
|
287
|
+
): Promise<T> {
|
|
288
|
+
this.throwIfClosed();
|
|
289
|
+
options.signal?.throwIfAborted();
|
|
290
|
+
const buffered = this.#eventBuffers.get(method) ?? [];
|
|
291
|
+
for (const [index, params] of buffered.entries()) {
|
|
292
|
+
if (!predicate(params)) continue;
|
|
293
|
+
buffered.splice(index, 1);
|
|
294
|
+
if (buffered.length === 0) this.#eventBuffers.delete(method);
|
|
295
|
+
return Promise.resolve(params);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return new Promise<T>((resolve, reject) => {
|
|
299
|
+
const waiter = {} as EventWaiter<T>;
|
|
300
|
+
const onAbort = () => {
|
|
301
|
+
this.#eventWaiters.delete(waiter as EventWaiter);
|
|
302
|
+
cleanup();
|
|
303
|
+
reject(options.signal?.reason ?? abortError(`CDP event wait aborted: ${method}`));
|
|
304
|
+
};
|
|
305
|
+
const timeout = setTimeout(() => {
|
|
306
|
+
this.#eventWaiters.delete(waiter as EventWaiter);
|
|
307
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
308
|
+
reject(new Error(`Timed out waiting for CDP event: ${method}`));
|
|
309
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
310
|
+
const cleanup = () => {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
313
|
+
};
|
|
314
|
+
Object.assign(waiter, { cleanup, method, predicate, reject, resolve });
|
|
315
|
+
this.#eventWaiters.add(waiter as EventWaiter);
|
|
316
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
close(reason: unknown = new Error("Chrome DevTools WebSocket closed")) {
|
|
321
|
+
if (this.#closed) return;
|
|
322
|
+
this.#closed = true;
|
|
323
|
+
this.#socket.removeEventListener("message", this.onMessage);
|
|
324
|
+
this.#socket.removeEventListener("close", this.onClose);
|
|
325
|
+
this.#socket.removeEventListener("error", this.onError);
|
|
326
|
+
this.rejectAll(reason);
|
|
327
|
+
this.#eventBuffers.clear();
|
|
328
|
+
try {
|
|
329
|
+
this.#socket.close();
|
|
330
|
+
} catch {
|
|
331
|
+
// The transport is already unusable; pending work has still been rejected.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private readonly onClose = () => {
|
|
336
|
+
this.close(new Error("Chrome DevTools WebSocket closed"));
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
private readonly onError = () => {
|
|
340
|
+
this.close(new Error("Chrome DevTools WebSocket error"));
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
private readonly onMessage = (event: MessageEvent) => {
|
|
344
|
+
if (typeof event.data !== "string") {
|
|
345
|
+
this.close(new Error("Chrome DevTools WebSocket sent an unsupported binary message"));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (event.data.length > MAX_CDP_MESSAGE_BYTES || Buffer.byteLength(event.data, "utf8") > MAX_CDP_MESSAGE_BYTES) {
|
|
349
|
+
this.close(new Error("Chrome DevTools WebSocket message exceeds the 8 MB limit"));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let payload: unknown;
|
|
353
|
+
try {
|
|
354
|
+
payload = JSON.parse(event.data) as unknown;
|
|
355
|
+
} catch {
|
|
356
|
+
this.close(new Error("Chrome DevTools WebSocket sent malformed JSON"));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!isRecord(payload)) {
|
|
360
|
+
this.close(new Error("Chrome DevTools WebSocket sent a malformed message"));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (typeof payload.id === "number") {
|
|
364
|
+
this.handleResponse(payload as unknown as CdpResponse);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (typeof payload.method !== "string") {
|
|
368
|
+
this.close(new Error("Chrome DevTools WebSocket sent a malformed event"));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.handleEvent(payload as unknown as CdpEvent);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
private handleResponse(response: CdpResponse) {
|
|
375
|
+
const pending = this.#pending.get(response.id);
|
|
376
|
+
if (!pending) return;
|
|
377
|
+
this.#pending.delete(response.id);
|
|
378
|
+
pending.cleanup();
|
|
379
|
+
if (response.error) {
|
|
380
|
+
pending.reject(
|
|
381
|
+
new Error(
|
|
382
|
+
`CDP error ${response.error.code}: ${response.error.message}${
|
|
383
|
+
response.error.data === undefined ? "" : ` (${safeJson(response.error.data)})`
|
|
384
|
+
}`,
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
pending.resolve(response.result);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private handleEvent(event: CdpEvent) {
|
|
393
|
+
if (event.method === "Inspector.detached" || event.method === "Target.detachedFromTarget") {
|
|
394
|
+
this.close(new Error(`Chrome DevTools target detached: ${safeJson(event.params)}`));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
for (const waiter of this.#eventWaiters) {
|
|
398
|
+
if (waiter.method !== event.method) continue;
|
|
399
|
+
let matched: boolean;
|
|
400
|
+
try {
|
|
401
|
+
matched = waiter.predicate(event.params);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.#eventWaiters.delete(waiter);
|
|
404
|
+
waiter.cleanup();
|
|
405
|
+
waiter.reject(error);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (!matched) continue;
|
|
409
|
+
this.#eventWaiters.delete(waiter);
|
|
410
|
+
waiter.cleanup();
|
|
411
|
+
waiter.resolve(event.params);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const buffer = this.#eventBuffers.get(event.method) ?? [];
|
|
415
|
+
buffer.push(event.params);
|
|
416
|
+
if (buffer.length > MAX_BUFFERED_EVENTS_PER_METHOD) {
|
|
417
|
+
this.close(
|
|
418
|
+
new Error(
|
|
419
|
+
`Chrome DevTools WebSocket buffered more than ${MAX_BUFFERED_EVENTS_PER_METHOD} ${event.method} events`,
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
this.#eventBuffers.set(event.method, buffer);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private rejectAll(reason: unknown) {
|
|
428
|
+
for (const [id, pending] of this.#pending) {
|
|
429
|
+
this.#pending.delete(id);
|
|
430
|
+
pending.cleanup();
|
|
431
|
+
pending.reject(reason);
|
|
432
|
+
}
|
|
433
|
+
for (const waiter of this.#eventWaiters) {
|
|
434
|
+
this.#eventWaiters.delete(waiter);
|
|
435
|
+
waiter.cleanup();
|
|
436
|
+
waiter.reject(reason);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private throwIfClosed() {
|
|
441
|
+
if (this.#closed) throw new Error("Chrome DevTools WebSocket is closed");
|
|
442
|
+
}
|
|
460
443
|
}
|
|
461
444
|
|
|
462
445
|
function abortError(message: string) {
|
|
463
|
-
|
|
446
|
+
return new DOMException(message, "AbortError");
|
|
464
447
|
}
|
|
465
448
|
|
|
466
449
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
467
|
-
|
|
450
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
468
451
|
}
|
|
469
452
|
|
|
470
453
|
function safeJson(value: unknown) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
454
|
+
try {
|
|
455
|
+
return JSON.stringify(value);
|
|
456
|
+
} catch {
|
|
457
|
+
return String(value);
|
|
458
|
+
}
|
|
476
459
|
}
|