@orkestrel/browser 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.
@@ -0,0 +1,1174 @@
1
+ import { isRecord, isString } from "@orkestrel/contract";
2
+ import { Emitter } from "@orkestrel/emitter";
3
+ //#region src/core/constants.ts
4
+ /** Base64 alphabet, index-ordered, used to build {@link BASE64_LOOKUP}. */
5
+ var BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6
+ /** Base64 character to 6-bit value lookup table, derived from {@link BASE64_CHARS}. */
7
+ var BASE64_LOOKUP = Object.freeze(Object.fromEntries(BASE64_CHARS.split("").map((char, index) => [char, index])));
8
+ /** Default timeout in milliseconds for browser connection, requests, and navigation. */
9
+ var BROWSER_DEFAULT_TIMEOUT_MS = 3e4;
10
+ /**
11
+ * Maximum serialized-character length for an `evaluate()`/`content()` result,
12
+ * enforced IN-PAGE before the result is returned to CDP.
13
+ *
14
+ * @remarks
15
+ * This counts UTF-16 STRING LENGTH (`String#length`), not transport BYTES —
16
+ * the actual CDP frame is UTF-8 encoded (up to 3 bytes/char for common
17
+ * multibyte content) and carries additional JSON/CDP framing overhead on top
18
+ * of the raw content. A `Runtime.evaluate` result above this size, once
19
+ * framed as a CDP JSON response, overflows the native WebSocket inbound frame
20
+ * limit and closes the whole CDP connection — a page-level failure with no
21
+ * clean error. The limit is set well under the observed ~3-4MB transport
22
+ * ceiling (rather than at it) to leave headroom for the length-vs-bytes gap
23
+ * and framing overhead. The cap is enforced by stringifying the candidate
24
+ * result in-page and throwing a recognizable sentinel error (see
25
+ * {@link BROWSER_RESULT_LIMIT_SENTINEL_PREFIX}) before the oversized frame is
26
+ * ever produced, so the caller gets a coded error instead of a dropped
27
+ * connection.
28
+ */
29
+ var BROWSER_RESULT_LIMIT = 25e5;
30
+ /**
31
+ * Distinctive prefix for the in-page result-limit sentinel error, immediately
32
+ * followed by the serialized length.
33
+ *
34
+ * @remarks
35
+ * Deliberately unlikely to appear in a page's own thrown error text (unlike
36
+ * a plain `BROWSER_RESULT_LIMIT: ` label), so {@link BROWSER_RESULT_LIMIT_PATTERN}
37
+ * can distinguish the guard's own throw from a page error that merely
38
+ * mentions similar text.
39
+ */
40
+ var BROWSER_RESULT_LIMIT_SENTINEL_PREFIX = "[[ORKESTREL_BROWSER_RESULT_LIMIT]]";
41
+ /**
42
+ * Matches the in-page result-limit sentinel error message, anchored
43
+ * immediately after the `Error: ` (optionally `Uncaught Error: `) prefix that
44
+ * Chromium prepends to a thrown error's description — so the guard's own
45
+ * throw is recognized only at the message START, not wherever the substring
46
+ * happens to occur.
47
+ */
48
+ var BROWSER_RESULT_LIMIT_PATTERN = new RegExp(`(?:Uncaught )?Error: \\[\\[ORKESTREL_BROWSER_RESULT_LIMIT\\]\\](\\d+)`);
49
+ /** Poll interval in milliseconds while waiting for a selector to appear. */
50
+ var BROWSER_WAIT_POLL_INTERVAL_MS = 100;
51
+ /**
52
+ * Bound (in milliseconds) on the best-effort `Page.stopLoading` call issued
53
+ * after a failed `navigate()`.
54
+ *
55
+ * @remarks
56
+ * A wedged renderer can make the underlying CDP call hang for the full
57
+ * per-call timeout; capping it to a small fixed bound keeps a navigate
58
+ * failure's total latency close to the original timeout instead of doubling
59
+ * it. Best-effort only — never masks the original navigate error.
60
+ */
61
+ var BROWSER_STOP_LOADING_TIMEOUT_MS = 1e3;
62
+ /** Default viewport width in pixels. */
63
+ var BROWSER_DEFAULT_VIEWPORT_WIDTH = 1280;
64
+ /** Default viewport height in pixels. */
65
+ var BROWSER_DEFAULT_VIEWPORT_HEIGHT = 720;
66
+ /** Name of the CDP runtime binding the codegen recorder script calls into. */
67
+ var BROWSER_CODEGEN_BINDING_NAME = "__orkestrelBrowserCodegen";
68
+ /**
69
+ * In-page recorder script injected via `Page.addScriptToEvaluateOnNewDocument`
70
+ * and `Runtime.evaluate`.
71
+ *
72
+ * @remarks
73
+ * Attaches capturing-phase listeners for `click`, `input` (fill), and
74
+ * `change` (select) on `document`, builds a stable CSS selector for the
75
+ * target element, and forwards each action to the CDP binding
76
+ * ({@link BROWSER_CODEGEN_BINDING_NAME}) as a JSON string payload. Guarded to
77
+ * install exactly once per document (`window[name]` sentinel) so repeated
78
+ * injection on every new document is idempotent.
79
+ */
80
+ var BROWSER_CODEGEN_SOURCE = `(() => {
81
+ const bindingName = ${JSON.stringify(BROWSER_CODEGEN_BINDING_NAME)}
82
+ if (window[bindingName + '__installed']) return
83
+ window[bindingName + '__installed'] = true
84
+
85
+ const selectorFor = (el) => {
86
+ if (el.id) return '#' + CSS.escape(el.id)
87
+ const parts = []
88
+ let node = el
89
+ while (node && node.nodeType === 1 && parts.length < 8) {
90
+ let part = node.tagName.toLowerCase()
91
+ if (node.classList && node.classList.length > 0) {
92
+ part += '.' + Array.from(node.classList).map((cls) => CSS.escape(cls)).join('.')
93
+ }
94
+ const parent = node.parentElement
95
+ if (parent) {
96
+ const siblings = Array.from(parent.children).filter((c) => c.tagName === node.tagName)
97
+ if (siblings.length > 1) {
98
+ part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')'
99
+ }
100
+ }
101
+ parts.unshift(part)
102
+ node = parent
103
+ }
104
+ return parts.join(' > ')
105
+ }
106
+
107
+ const send = (payload) => {
108
+ if (typeof window[bindingName] === 'function') {
109
+ window[bindingName](JSON.stringify(payload))
110
+ }
111
+ }
112
+
113
+ document.addEventListener(
114
+ 'click',
115
+ (event) => {
116
+ const target = event.target
117
+ if (!target || target.nodeType !== 1) return
118
+ send({ action: 'click', selector: selectorFor(target) })
119
+ },
120
+ true,
121
+ )
122
+
123
+ const fillableTypes = new Set([
124
+ 'text',
125
+ 'search',
126
+ 'url',
127
+ 'tel',
128
+ 'email',
129
+ 'password',
130
+ 'number',
131
+ ])
132
+
133
+ document.addEventListener(
134
+ 'input',
135
+ (event) => {
136
+ const target = event.target
137
+ if (!target || target.nodeType !== 1) return
138
+
139
+ if (target.isContentEditable) {
140
+ send({ action: 'fill', selector: selectorFor(target), value: target.textContent || '' })
141
+ return
142
+ }
143
+
144
+ if (typeof target.value !== 'string') return
145
+ const tag = target.tagName
146
+ if (tag === 'TEXTAREA') {
147
+ send({ action: 'fill', selector: selectorFor(target), value: target.value })
148
+ return
149
+ }
150
+ if (tag === 'INPUT' && fillableTypes.has((target.type || 'text').toLowerCase())) {
151
+ send({ action: 'fill', selector: selectorFor(target), value: target.value })
152
+ }
153
+ },
154
+ true,
155
+ )
156
+
157
+ document.addEventListener(
158
+ 'change',
159
+ (event) => {
160
+ const target = event.target
161
+ if (!target || target.tagName !== 'SELECT') return
162
+ const values = Array.from(target.selectedOptions || []).map((option) => option.value)
163
+ send({ action: 'select', selector: selectorFor(target), values })
164
+ },
165
+ true,
166
+ )
167
+ })()`;
168
+ //#endregion
169
+ //#region src/core/errors.ts
170
+ /**
171
+ * Base error for all browser automation operations.
172
+ *
173
+ * @remarks
174
+ * Carries a machine-readable `code` and optional `context` (AGENTS §12) so
175
+ * callers can branch in a `catch` without parsing message strings.
176
+ */
177
+ var BrowserError = class extends Error {
178
+ code;
179
+ context;
180
+ constructor(message, code = "BROWSER_ERROR", context) {
181
+ super(message);
182
+ this.name = "BrowserError";
183
+ this.code = code;
184
+ this.context = context;
185
+ }
186
+ };
187
+ /**
188
+ * A selector-based lookup or wait timed out without the element appearing.
189
+ */
190
+ var BrowserSelectorError = class extends BrowserError {
191
+ constructor(message, context) {
192
+ super(message, "BROWSER_SELECTOR_ERROR", context);
193
+ this.name = "BrowserSelectorError";
194
+ }
195
+ };
196
+ /**
197
+ * A CDP request received an error response from the remote endpoint.
198
+ *
199
+ * @remarks
200
+ * Carries the originating `method` plus the CDP error's own `code`,
201
+ * `message`, and `data` (when present) in `context`, so callers can branch
202
+ * on the protocol-level error instead of parsing the message string.
203
+ */
204
+ var CDPError = class extends BrowserError {
205
+ constructor(message, context) {
206
+ super(message, "BROWSER_CDP_ERROR", context);
207
+ this.name = "CDPError";
208
+ }
209
+ };
210
+ /**
211
+ * A CDP request could not be sent or completed because the client was not in
212
+ * a connectable state (not connected, closed while connecting, or the
213
+ * connection dropped mid-request).
214
+ */
215
+ var CDPConnectionError = class extends BrowserError {
216
+ constructor(message, context) {
217
+ super(message, "BROWSER_CDP_CONNECTION_ERROR", context);
218
+ this.name = "CDPConnectionError";
219
+ }
220
+ };
221
+ /**
222
+ * A pending CDP request was not answered within its timeout window.
223
+ */
224
+ var CDPTimeoutError = class extends BrowserError {
225
+ constructor(message, context) {
226
+ super(message, "BROWSER_CDP_TIMEOUT_ERROR", context);
227
+ this.name = "CDPTimeoutError";
228
+ }
229
+ };
230
+ /**
231
+ * An `evaluate()`/`content()` result exceeded {@link BROWSER_RESULT_LIMIT}
232
+ * and was rejected in-page before it could overflow the CDP transport frame.
233
+ */
234
+ var BrowserResultLimitError = class extends BrowserError {
235
+ constructor(message, context) {
236
+ super(message, "BROWSER_RESULT_LIMIT_ERROR", context);
237
+ this.name = "BrowserResultLimitError";
238
+ }
239
+ };
240
+ /**
241
+ * Narrow an unknown value to BrowserError.
242
+ *
243
+ * @param value - Value to check
244
+ * @returns True when value is a BrowserError instance
245
+ */
246
+ function isBrowserError(value) {
247
+ return value instanceof BrowserError;
248
+ }
249
+ /**
250
+ * Narrow an unknown value to BrowserSelectorError.
251
+ *
252
+ * @param value - Value to check
253
+ * @returns True when value is a BrowserSelectorError instance
254
+ */
255
+ function isBrowserSelectorError(value) {
256
+ return value instanceof BrowserSelectorError;
257
+ }
258
+ /**
259
+ * Narrow an unknown value to CDPError.
260
+ *
261
+ * @param value - Value to check
262
+ * @returns True when value is a CDPError instance
263
+ */
264
+ function isCDPError(value) {
265
+ return value instanceof CDPError;
266
+ }
267
+ /**
268
+ * Narrow an unknown value to CDPConnectionError.
269
+ *
270
+ * @param value - Value to check
271
+ * @returns True when value is a CDPConnectionError instance
272
+ */
273
+ function isCDPConnectionError(value) {
274
+ return value instanceof CDPConnectionError;
275
+ }
276
+ /**
277
+ * Narrow an unknown value to CDPTimeoutError.
278
+ *
279
+ * @param value - Value to check
280
+ * @returns True when value is a CDPTimeoutError instance
281
+ */
282
+ function isCDPTimeoutError(value) {
283
+ return value instanceof CDPTimeoutError;
284
+ }
285
+ /**
286
+ * Narrow an unknown value to BrowserResultLimitError.
287
+ *
288
+ * @param value - Value to check
289
+ * @returns True when value is a BrowserResultLimitError instance
290
+ */
291
+ function isBrowserResultLimitError(value) {
292
+ return value instanceof BrowserResultLimitError;
293
+ }
294
+ //#endregion
295
+ //#region src/core/helpers.ts
296
+ /**
297
+ * Decode a base64-encoded string into raw bytes.
298
+ *
299
+ * @remarks
300
+ * Pure JS implementation — no `Buffer`, no `atob` (DOM-only) — so it runs
301
+ * identically in Node and browser environments. Whitespace and `=` padding
302
+ * are ignored; invalid characters are skipped.
303
+ *
304
+ * @param text - Base64-encoded input string
305
+ * @returns Decoded bytes
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * decodeBase64('aGVsbG8=') // Uint8Array [104, 101, 108, 108, 111]
310
+ * ```
311
+ */
312
+ function decodeBase64(text) {
313
+ const clean = text.replace(/[^A-Za-z0-9+/]/g, "");
314
+ const bytes = [];
315
+ let buffer = 0;
316
+ let bits = 0;
317
+ for (const char of clean) {
318
+ const value = BASE64_LOOKUP[char];
319
+ if (value === void 0) continue;
320
+ buffer = buffer << 6 | value;
321
+ bits += 6;
322
+ if (bits >= 8) {
323
+ bits -= 8;
324
+ bytes.push(buffer >> bits & 255);
325
+ }
326
+ }
327
+ return new Uint8Array(bytes);
328
+ }
329
+ /**
330
+ * Wrap a `Runtime.evaluate` expression so the IN-PAGE code stringifies its
331
+ * own result and throws a recognizable error before an oversized result
332
+ * would overflow the CDP transport frame.
333
+ *
334
+ * @remarks
335
+ * A result whose `JSON.stringify` length exceeds `limit` throws
336
+ * `Error('BROWSER_RESULT_LIMIT: <length>')` inside the page instead of being
337
+ * returned — the caller maps that sentinel to a coded
338
+ * {@link BrowserResultLimitError}. A non-serializable result (`undefined`,
339
+ * a function, a symbol) makes `JSON.stringify` return `undefined`, so the
340
+ * length check is skipped and today's undefined-passthrough behavior is
341
+ * unchanged.
342
+ *
343
+ * The expression is placed on its own line inside the wrapper (rather than
344
+ * inline with the guard code) so a trailing `//` line comment in the
345
+ * expression cannot swallow the closing guard syntax that follows it.
346
+ *
347
+ * @param expression - The candidate JavaScript expression to evaluate
348
+ * @param limit - Maximum serialized-character length (see {@link BROWSER_RESULT_LIMIT})
349
+ * @returns The wrapped, guarded expression
350
+ */
351
+ function guardEvaluateExpression(expression, limit) {
352
+ return `(() => { const r = (
353
+ ${expression}
354
+ ); const s = JSON.stringify(r); if (typeof s === 'string' && s.length > ${limit}) throw new Error(${JSON.stringify(BROWSER_RESULT_LIMIT_SENTINEL_PREFIX)} + s.length); return r })()`;
355
+ }
356
+ /**
357
+ * Normalize a raw list of recorded codegen actions.
358
+ *
359
+ * @remarks
360
+ * Collapses consecutive `fill` actions on the same selector into the latest
361
+ * value (a text input fires one `input` event per keystroke) so the
362
+ * compiled script reflects the final typed value rather than every
363
+ * intermediate keystroke.
364
+ *
365
+ * @param actions - Raw recorded actions, in capture order
366
+ * @returns Normalized actions, in the same order
367
+ */
368
+ function normalizeCodegenActions(actions) {
369
+ const result = [];
370
+ for (const action of actions) {
371
+ const previous = result[result.length - 1];
372
+ if (previous !== void 0 && previous.action === "fill" && action.action === "fill" && previous.selector === action.selector) {
373
+ result[result.length - 1] = action;
374
+ continue;
375
+ }
376
+ result.push(action);
377
+ }
378
+ return result;
379
+ }
380
+ /**
381
+ * Parse a codegen binding payload into a typed action.
382
+ *
383
+ * @remarks
384
+ * The in-page recorder script calls the CDP binding with a JSON string
385
+ * payload shaped like a {@link BrowserCodegenAction}. Returns `undefined`
386
+ * when the payload is not valid JSON or does not match a known action shape.
387
+ *
388
+ * @param payload - Raw binding call payload (the CDP `payload` param)
389
+ * @returns The parsed action, or `undefined` when the payload is malformed
390
+ */
391
+ function parseCodegenActionPayload(payload) {
392
+ if (!isString(payload)) return void 0;
393
+ let parsed;
394
+ try {
395
+ parsed = JSON.parse(payload);
396
+ } catch {
397
+ return;
398
+ }
399
+ if (!isRecord(parsed)) return void 0;
400
+ const action = parsed["action"];
401
+ const selector = parsed["selector"];
402
+ if (action === "click" && isString(selector)) return {
403
+ action: "click",
404
+ selector
405
+ };
406
+ if (action === "fill" && isString(selector) && isString(parsed["value"])) return {
407
+ action: "fill",
408
+ selector,
409
+ value: parsed["value"]
410
+ };
411
+ if (action === "select" && isString(selector) && Array.isArray(parsed["values"])) {
412
+ const values = parsed["values"].filter(isString);
413
+ if (values.length === parsed["values"].length) return {
414
+ action: "select",
415
+ selector,
416
+ values
417
+ };
418
+ }
419
+ }
420
+ /**
421
+ * Derive a `navigate` codegen action from a `Page.frameNavigated` CDP event.
422
+ *
423
+ * @remarks
424
+ * Only the top-level (main) frame's navigation is recorded — a frame
425
+ * carrying a `parentId` is a sub-frame and is ignored.
426
+ *
427
+ * @param params - The CDP `Page.frameNavigated` event params
428
+ * @returns A `navigate` action, or `undefined` when the event is not a
429
+ * top-level navigation with a resolvable URL
430
+ */
431
+ function readCodegenNavigateAction(params) {
432
+ const frame = params["frame"];
433
+ if (!isRecord(frame)) return void 0;
434
+ if ("parentId" in frame) return void 0;
435
+ if (!isString(frame["url"])) return void 0;
436
+ return {
437
+ action: "navigate",
438
+ url: frame["url"]
439
+ };
440
+ }
441
+ /**
442
+ * Compile recorded codegen actions into a replayable script.
443
+ *
444
+ * @remarks
445
+ * Emits one statement per action against a `page` object shaped like
446
+ * {@link BrowserPageInterface} (`page.navigate(...)`, `page.click(...)`, …).
447
+ * Both target languages emit an `async function run(page)` body whose
448
+ * statements are `await`-ed; `language` only toggles whether the `page`
449
+ * parameter carries a TypeScript type annotation (default `'javascript'`).
450
+ *
451
+ * @param actions - Normalized actions to compile
452
+ * @param options - Compilation options (target language)
453
+ * @returns The compiled script source
454
+ */
455
+ function compileCodegenScript(actions, options) {
456
+ const language = options?.language ?? "javascript";
457
+ const lines = actions.map((action) => {
458
+ switch (action.action) {
459
+ case "navigate": return `await page.navigate(${JSON.stringify(action.url)})`;
460
+ case "click": return `await page.click(${JSON.stringify(action.selector)})`;
461
+ case "fill": return `await page.fill(${JSON.stringify(action.selector)}, ${JSON.stringify(action.value)})`;
462
+ case "select": return `await page.select(${JSON.stringify(action.selector)}, ${JSON.stringify(action.values)})`;
463
+ }
464
+ });
465
+ if (language === "typescript") return [
466
+ `async function run(page: import('@orkestrel/browser').BrowserPageInterface): Promise<void> {`,
467
+ ...lines.map((line) => `\t${line}`),
468
+ `}`
469
+ ].join("\n");
470
+ return [
471
+ `async function run(page) {`,
472
+ ...lines.map((line) => `\t${line}`),
473
+ `}`
474
+ ].join("\n");
475
+ }
476
+ //#endregion
477
+ //#region src/core/CDPClient.ts
478
+ /**
479
+ * Lightweight Chrome DevTools Protocol client over a {@link CDPTransportInterface}.
480
+ *
481
+ * @remarks
482
+ * Sends JSON-RPC messages over the injected transport and dispatches
483
+ * responses and events. The transport owns the connection itself
484
+ * (WebSocket, pipe, or any other duplex channel); this class owns only
485
+ * the JSON-RPC framing and id/session correlation.
486
+ *
487
+ * Supports session-scoped event subscriptions: when a sessionId is
488
+ * provided to subscribe/unsubscribe, the handler only fires for CDP
489
+ * events carrying that sessionId. Global subscriptions (no sessionId)
490
+ * continue to see ALL events for backwards compatibility.
491
+ */
492
+ var CDPClient = class {
493
+ #transport;
494
+ #messageId = 0;
495
+ #pending = /* @__PURE__ */ new Map();
496
+ #subscriptions = /* @__PURE__ */ new Map();
497
+ #sessionSubscriptions = /* @__PURE__ */ new Map();
498
+ #connected = false;
499
+ #wired = false;
500
+ #timeout;
501
+ #connecting;
502
+ #closeRequested = false;
503
+ constructor(options) {
504
+ this.#transport = options.transport;
505
+ this.#timeout = options.timeout ?? 3e4;
506
+ }
507
+ get connected() {
508
+ return this.#connected;
509
+ }
510
+ async connect() {
511
+ if (this.#connected) return;
512
+ if (this.#connecting !== void 0) return this.#connecting;
513
+ this.#closeRequested = false;
514
+ const attempt = (async () => {
515
+ if (!this.#wired) {
516
+ this.#transport.emitter.on("message", (data) => this.#onMessage(data));
517
+ this.#transport.emitter.on("close", () => this.#onClose());
518
+ this.#transport.emitter.on("error", (error) => this.#onError(error));
519
+ this.#wired = true;
520
+ }
521
+ await this.#transport.start();
522
+ if (this.#closeRequested) {
523
+ this.#closeRequested = false;
524
+ await this.#transport.close();
525
+ throw new CDPConnectionError("CDP client was closed while connecting", { method: "connect" });
526
+ }
527
+ this.#connected = true;
528
+ })();
529
+ this.#connecting = attempt;
530
+ try {
531
+ await attempt;
532
+ } finally {
533
+ this.#connecting = void 0;
534
+ }
535
+ }
536
+ /**
537
+ * Close the transport and re-establish a fresh connection.
538
+ *
539
+ * @remarks
540
+ * Client-level subscriptions (`subscribe`/`unsubscribe` registrations)
541
+ * survive `close()` and remain active after the reconnect — only pending
542
+ * in-flight requests are rejected. This lets a caller reconnect a dropped
543
+ * transport without re-registering every handler.
544
+ */
545
+ async reconnect() {
546
+ await this.close();
547
+ await this.connect();
548
+ }
549
+ async send(method, params, sessionId, timeout) {
550
+ if (!this.#connected) throw new CDPConnectionError("CDP client is not connected", { method });
551
+ const id = this.#nextId();
552
+ const message = {
553
+ id,
554
+ method
555
+ };
556
+ if (params !== void 0) message["params"] = params;
557
+ if (sessionId !== void 0) message["sessionId"] = sessionId;
558
+ const serialized = JSON.stringify(message);
559
+ const effectiveTimeout = timeout ?? this.#timeout;
560
+ return new Promise((resolve, reject) => {
561
+ const timer = setTimeout(() => {
562
+ this.#pending.delete(id);
563
+ reject(new CDPTimeoutError(`CDP request timed out: ${method}`, {
564
+ method,
565
+ timeout: effectiveTimeout
566
+ }));
567
+ }, effectiveTimeout);
568
+ this.#pending.set(id, {
569
+ method,
570
+ resolve,
571
+ reject,
572
+ timer
573
+ });
574
+ this.#transport.send(serialized).catch((thrown) => {
575
+ this.#pending.delete(id);
576
+ clearTimeout(timer);
577
+ reject(thrown);
578
+ });
579
+ });
580
+ }
581
+ subscribe(method, handler, sessionId) {
582
+ if (sessionId !== void 0) {
583
+ let sessionMap = this.#sessionSubscriptions.get(sessionId);
584
+ if (sessionMap === void 0) {
585
+ sessionMap = /* @__PURE__ */ new Map();
586
+ this.#sessionSubscriptions.set(sessionId, sessionMap);
587
+ }
588
+ let handlers = sessionMap.get(method);
589
+ if (handlers === void 0) {
590
+ handlers = /* @__PURE__ */ new Set();
591
+ sessionMap.set(method, handlers);
592
+ }
593
+ handlers.add(handler);
594
+ } else {
595
+ let handlers = this.#subscriptions.get(method);
596
+ if (handlers === void 0) {
597
+ handlers = /* @__PURE__ */ new Set();
598
+ this.#subscriptions.set(method, handlers);
599
+ }
600
+ handlers.add(handler);
601
+ }
602
+ }
603
+ unsubscribe(method, handler, sessionId) {
604
+ if (sessionId !== void 0) {
605
+ const sessionMap = this.#sessionSubscriptions.get(sessionId);
606
+ if (sessionMap !== void 0) {
607
+ const handlers = sessionMap.get(method);
608
+ if (handlers !== void 0) {
609
+ handlers.delete(handler);
610
+ if (handlers.size === 0) sessionMap.delete(method);
611
+ }
612
+ if (sessionMap.size === 0) this.#sessionSubscriptions.delete(sessionId);
613
+ }
614
+ } else {
615
+ const handlers = this.#subscriptions.get(method);
616
+ if (handlers !== void 0) {
617
+ handlers.delete(handler);
618
+ if (handlers.size === 0) this.#subscriptions.delete(method);
619
+ }
620
+ }
621
+ }
622
+ /**
623
+ * Close the transport and reject all pending requests.
624
+ *
625
+ * @remarks
626
+ * Subscriptions registered via `subscribe()` are client-level
627
+ * registrations, not connection-level state — they survive `close()`
628
+ * (and a subsequent `reconnect()`/`connect()`) and keep firing once the
629
+ * transport is active again. Only pending requests are rejected here.
630
+ * If `close()` is called while `connect()` is still awaiting
631
+ * `transport.start()`, the in-flight connect is rejected and the
632
+ * transport is closed deterministically once `start()` resolves.
633
+ */
634
+ async close() {
635
+ if (this.#connecting !== void 0) {
636
+ this.#closeRequested = true;
637
+ await this.#connecting.catch(() => void 0);
638
+ }
639
+ if (!this.#connected) return;
640
+ this.#connected = false;
641
+ for (const [id, entry] of this.#pending) {
642
+ clearTimeout(entry.timer);
643
+ entry.reject(new CDPConnectionError("CDP connection closed", { method: entry.method }));
644
+ this.#pending.delete(id);
645
+ }
646
+ await this.#transport.close();
647
+ }
648
+ #nextId() {
649
+ this.#messageId += 1;
650
+ return this.#messageId;
651
+ }
652
+ #onClose() {
653
+ this.#connected = false;
654
+ for (const [id, entry] of this.#pending) {
655
+ clearTimeout(entry.timer);
656
+ entry.reject(new CDPConnectionError("CDP connection closed", { method: entry.method }));
657
+ this.#pending.delete(id);
658
+ }
659
+ }
660
+ #onError(error) {
661
+ if (!this.#connected) for (const [id, entry] of this.#pending) {
662
+ clearTimeout(entry.timer);
663
+ entry.reject(new CDPConnectionError(`CDP connection failed: ${String(error)}`, {
664
+ method: entry.method,
665
+ error
666
+ }));
667
+ this.#pending.delete(id);
668
+ }
669
+ }
670
+ #onMessage(data) {
671
+ let parsed;
672
+ try {
673
+ parsed = JSON.parse(data);
674
+ } catch {
675
+ return;
676
+ }
677
+ if (!isRecord(parsed)) return;
678
+ if (typeof parsed["id"] === "number") {
679
+ const id = parsed["id"];
680
+ const entry = this.#pending.get(id);
681
+ if (entry !== void 0) {
682
+ this.#pending.delete(id);
683
+ clearTimeout(entry.timer);
684
+ const errorValue = parsed["error"];
685
+ if (isRecord(errorValue)) {
686
+ const message = isString(errorValue["message"]) ? errorValue["message"] : "CDP error";
687
+ const context = { method: entry.method };
688
+ if ("code" in errorValue) context["code"] = errorValue["code"];
689
+ context["message"] = message;
690
+ if ("data" in errorValue) context["data"] = errorValue["data"];
691
+ entry.reject(new CDPError(message, context));
692
+ } else entry.resolve("result" in parsed ? parsed["result"] : void 0);
693
+ }
694
+ return;
695
+ }
696
+ if (isString(parsed["method"])) {
697
+ const method = parsed["method"];
698
+ const rawParams = parsed["params"];
699
+ const params = isRecord(rawParams) ? rawParams : Object.freeze({});
700
+ const globalHandlers = this.#subscriptions.get(method);
701
+ if (globalHandlers !== void 0) for (const handler of [...globalHandlers]) handler(params);
702
+ const eventSessionId = parsed["sessionId"];
703
+ if (isString(eventSessionId)) {
704
+ const sessionMap = this.#sessionSubscriptions.get(eventSessionId);
705
+ if (sessionMap !== void 0) {
706
+ const sessionHandlers = sessionMap.get(method);
707
+ if (sessionHandlers !== void 0) for (const handler of [...sessionHandlers]) handler(params);
708
+ }
709
+ }
710
+ }
711
+ }
712
+ };
713
+ //#endregion
714
+ //#region src/core/BrowserCodegen.ts
715
+ var BrowserCodegen = class {
716
+ #client;
717
+ #sessionId;
718
+ #actions = [];
719
+ #started = false;
720
+ #destroyed = false;
721
+ #emitter;
722
+ constructor(client, sessionId, options) {
723
+ this.#client = client;
724
+ this.#sessionId = sessionId;
725
+ this.#emitter = new Emitter({ on: options?.on });
726
+ }
727
+ get emitter() {
728
+ return this.#emitter;
729
+ }
730
+ get started() {
731
+ return this.#started;
732
+ }
733
+ async start() {
734
+ if (this.#destroyed) return;
735
+ if (this.#started) return;
736
+ this.#started = true;
737
+ this.#client.subscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
738
+ this.#client.subscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
739
+ try {
740
+ await this.#client.send("Runtime.enable", void 0, this.#sessionId);
741
+ await this.#client.send("Runtime.addBinding", { name: BROWSER_CODEGEN_BINDING_NAME }, this.#sessionId);
742
+ await this.#client.send("Page.addScriptToEvaluateOnNewDocument", { source: BROWSER_CODEGEN_SOURCE }, this.#sessionId);
743
+ await this.#client.send("Runtime.evaluate", {
744
+ expression: BROWSER_CODEGEN_SOURCE,
745
+ awaitPromise: true
746
+ }, this.#sessionId);
747
+ } catch (error) {
748
+ this.#client.unsubscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
749
+ this.#client.unsubscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
750
+ this.#started = false;
751
+ throw error;
752
+ }
753
+ this.#emitter.emit("start");
754
+ }
755
+ async stop() {
756
+ if (!this.#started) return this.actions();
757
+ this.#client.unsubscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
758
+ this.#client.unsubscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
759
+ this.#started = false;
760
+ const snapshot = this.actions();
761
+ this.#emitter.emit("stop", snapshot);
762
+ return snapshot;
763
+ }
764
+ actions() {
765
+ return normalizeCodegenActions(this.#actions);
766
+ }
767
+ script(options) {
768
+ return compileCodegenScript(this.actions(), options);
769
+ }
770
+ clear() {
771
+ this.#actions = [];
772
+ this.#emitter.emit("clear");
773
+ }
774
+ async destroy() {
775
+ if (this.#destroyed) return;
776
+ this.#destroyed = true;
777
+ try {
778
+ await this.stop();
779
+ } catch {}
780
+ this.#actions = [];
781
+ this.#emitter.destroy();
782
+ }
783
+ #onBindingCalled = (params) => {
784
+ if (params["name"] !== "__orkestrelBrowserCodegen") return;
785
+ const action = parseCodegenActionPayload(params["payload"]);
786
+ if (action === void 0) return;
787
+ this.#actions.push(action);
788
+ this.#emitter.emit("action", action);
789
+ };
790
+ #onFrameNavigated = (params) => {
791
+ const action = readCodegenNavigateAction(params);
792
+ if (action === void 0) return;
793
+ this.#actions.push(action);
794
+ this.#emitter.emit("action", action);
795
+ };
796
+ };
797
+ //#endregion
798
+ //#region src/core/BrowserPage.ts
799
+ var BrowserPage = class {
800
+ #client;
801
+ #targetId;
802
+ #sessionId;
803
+ #writer;
804
+ #url = "about:blank";
805
+ #closed = false;
806
+ #codegen;
807
+ #destroyHandler;
808
+ constructor(client, targetId, sessionId, writer, url) {
809
+ this.#client = client;
810
+ this.#targetId = targetId;
811
+ this.#sessionId = sessionId;
812
+ this.#writer = writer;
813
+ if (url !== void 0) this.#url = url;
814
+ this.#destroyHandler = (params) => {
815
+ if (isString(params["targetId"]) && params["targetId"] === this.#targetId) this.#closed = true;
816
+ };
817
+ this.#client.subscribe("Target.targetDestroyed", this.#destroyHandler);
818
+ }
819
+ get url() {
820
+ return this.#url;
821
+ }
822
+ get closed() {
823
+ return this.#closed;
824
+ }
825
+ async title() {
826
+ const result = await this.#evaluate("document.title");
827
+ return typeof result === "string" ? result : "";
828
+ }
829
+ async navigate(url, options) {
830
+ const timeout = options?.timeout ?? 3e4;
831
+ const condition = options?.condition ?? "load";
832
+ const wait = this.#waitForLoadEvent(condition, timeout);
833
+ const waitOutcome = wait.promise.then(() => ({ ok: true }), (error) => ({
834
+ ok: false,
835
+ error
836
+ }));
837
+ try {
838
+ const result = await this.#client.send("Page.navigate", { url }, this.#sessionId, timeout);
839
+ if (isRecord(result) && isString(result["errorText"])) throw new BrowserError(`Navigation failed: ${result["errorText"]}`);
840
+ const outcome = await waitOutcome;
841
+ if (!outcome.ok) throw outcome.error;
842
+ } catch (error) {
843
+ wait.cancel();
844
+ await this.#stopLoading(Math.min(timeout, BROWSER_STOP_LOADING_TIMEOUT_MS));
845
+ throw error;
846
+ }
847
+ const currentUrl = await this.#evaluate("location.href");
848
+ this.#url = typeof currentUrl === "string" ? currentUrl : url;
849
+ }
850
+ async content() {
851
+ const [title, html, text, currentUrl] = await Promise.all([
852
+ this.#evaluate("document.title"),
853
+ this.#evaluate(guardEvaluateExpression("document.documentElement.outerHTML", BROWSER_RESULT_LIMIT)),
854
+ this.#evaluate(guardEvaluateExpression("document.body ? document.body.innerText : \"\"", BROWSER_RESULT_LIMIT)),
855
+ this.#evaluate("location.href")
856
+ ]);
857
+ if (typeof currentUrl === "string") this.#url = currentUrl;
858
+ return {
859
+ url: this.#url,
860
+ title: typeof title === "string" ? title : "",
861
+ html: typeof html === "string" ? html : "",
862
+ text: typeof text === "string" ? text : ""
863
+ };
864
+ }
865
+ async screenshot(options) {
866
+ const format = options?.type ?? "png";
867
+ const params = { format };
868
+ if (options?.quality !== void 0 && format === "jpeg") params["quality"] = options.quality;
869
+ if (options?.full === true) {
870
+ const metrics = await this.#client.send("Page.getLayoutMetrics", void 0, this.#sessionId);
871
+ if (isRecord(metrics) && isRecord(metrics["contentSize"])) {
872
+ const contentSize = metrics["contentSize"];
873
+ const width = typeof contentSize["width"] === "number" ? contentSize["width"] : 0;
874
+ const height = typeof contentSize["height"] === "number" ? contentSize["height"] : 0;
875
+ if (width > 0 && height > 0) {
876
+ params["clip"] = {
877
+ x: 0,
878
+ y: 0,
879
+ width,
880
+ height,
881
+ scale: 1
882
+ };
883
+ params["captureBeyondViewport"] = true;
884
+ }
885
+ }
886
+ }
887
+ const result = await this.#client.send("Page.captureScreenshot", params, this.#sessionId);
888
+ if (!isRecord(result) || !isString(result["data"])) throw new BrowserError("Screenshot failed: no data returned");
889
+ const bytes = decodeBase64(result["data"]);
890
+ if (options?.path !== void 0 && this.#writer !== void 0) await this.#writer.write(options.path, bytes);
891
+ return {
892
+ bytes,
893
+ path: options?.path
894
+ };
895
+ }
896
+ async click(selector, options) {
897
+ const timeout = options?.timeout ?? 3e4;
898
+ await this.#waitForSelector(selector, timeout);
899
+ await this.#evaluate(`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (el) { el.scrollIntoView({ block: 'center' }); el.click(); } else { throw new Error('Element not found: ' + ${JSON.stringify(selector)}); } })()`);
900
+ }
901
+ async fill(selector, value, options) {
902
+ const timeout = options?.timeout ?? 3e4;
903
+ await this.#waitForSelector(selector, timeout);
904
+ await this.#evaluate(`(() => {
905
+ const el = document.querySelector(${JSON.stringify(selector)});
906
+ if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
907
+ el.focus();
908
+ if (el.isContentEditable) {
909
+ el.textContent = ${JSON.stringify(value)};
910
+ el.dispatchEvent(new Event('input', { bubbles: true }));
911
+ } else {
912
+ el.value = ${JSON.stringify(value)};
913
+ el.dispatchEvent(new Event('input', { bubbles: true }));
914
+ el.dispatchEvent(new Event('change', { bubbles: true }));
915
+ }
916
+ })()`);
917
+ }
918
+ async select(selector, values, options) {
919
+ const timeout = options?.timeout ?? 3e4;
920
+ await this.#waitForSelector(selector, timeout);
921
+ const valuesJson = JSON.stringify([...values]);
922
+ await this.#evaluate(`(() => {
923
+ const el = document.querySelector(${JSON.stringify(selector)});
924
+ if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
925
+ const vals = ${valuesJson};
926
+ for (const opt of el.options) {
927
+ opt.selected = vals.includes(opt.value);
928
+ }
929
+ el.dispatchEvent(new Event('change', { bubbles: true }));
930
+ })()`);
931
+ }
932
+ async evaluate(expression) {
933
+ return this.#evaluate(guardEvaluateExpression(expression, BROWSER_RESULT_LIMIT));
934
+ }
935
+ async wait(selector, options) {
936
+ const timeout = options?.timeout ?? 3e4;
937
+ await this.#waitForSelector(selector, timeout);
938
+ }
939
+ async frame(name) {
940
+ return (await this.frames()).find((frame) => frame.name === name || frame.url === name);
941
+ }
942
+ async frames() {
943
+ const result = await this.#client.send("Page.getFrameTree", void 0, this.#sessionId);
944
+ if (!isRecord(result) || !isRecord(result["frameTree"])) return [];
945
+ const frames = [];
946
+ this.#flattenFrameTree(result["frameTree"], frames);
947
+ return frames;
948
+ }
949
+ async codegen(options) {
950
+ if (this.#codegen !== void 0) return this.#codegen;
951
+ const codegen = new BrowserCodegen(this.#client, this.#sessionId, options);
952
+ await codegen.start();
953
+ this.#codegen = codegen;
954
+ return codegen;
955
+ }
956
+ async close() {
957
+ if (this.#closed) return;
958
+ this.#closed = true;
959
+ if (this.#codegen !== void 0) {
960
+ try {
961
+ await this.#codegen.destroy();
962
+ } catch {}
963
+ this.#codegen = void 0;
964
+ }
965
+ this.#client.unsubscribe("Target.targetDestroyed", this.#destroyHandler);
966
+ try {
967
+ await this.#client.send("Target.closeTarget", { targetId: this.#targetId });
968
+ } catch {}
969
+ }
970
+ #flattenFrameTree(node, out) {
971
+ const frame = node["frame"];
972
+ if (isRecord(frame) && isString(frame["id"]) && isString(frame["url"])) {
973
+ const parent = frame["parentId"];
974
+ const name = frame["name"];
975
+ out.push({
976
+ id: frame["id"],
977
+ ...isString(parent) ? { parent } : {},
978
+ ...isString(name) && name !== "" ? { name } : {},
979
+ url: frame["url"]
980
+ });
981
+ }
982
+ const children = node["childFrames"];
983
+ if (Array.isArray(children)) {
984
+ for (const child of children) if (isRecord(child)) this.#flattenFrameTree(child, out);
985
+ }
986
+ }
987
+ async #stopLoading(timeout) {
988
+ try {
989
+ await this.#client.send("Page.stopLoading", void 0, this.#sessionId, timeout);
990
+ } catch {}
991
+ }
992
+ async #evaluate(expression) {
993
+ const result = await this.#client.send("Runtime.evaluate", {
994
+ expression,
995
+ returnByValue: true,
996
+ awaitPromise: true
997
+ }, this.#sessionId);
998
+ if (!isRecord(result)) return void 0;
999
+ if (isRecord(result["exceptionDetails"])) {
1000
+ const details = result["exceptionDetails"];
1001
+ if (isRecord(details["exception"]) && isString(details["exception"]["description"])) {
1002
+ const description = details["exception"]["description"];
1003
+ const limitMatch = BROWSER_RESULT_LIMIT_PATTERN.exec(description);
1004
+ if (limitMatch !== null) throw new BrowserResultLimitError("Evaluation result exceeds BROWSER_RESULT_LIMIT", {
1005
+ length: Number(limitMatch[1]),
1006
+ limit: BROWSER_RESULT_LIMIT
1007
+ });
1008
+ throw new BrowserError(description);
1009
+ }
1010
+ throw new BrowserError("JavaScript evaluation failed");
1011
+ }
1012
+ const remoteObject = result["result"];
1013
+ if (!isRecord(remoteObject)) return void 0;
1014
+ if ("value" in remoteObject) return remoteObject["value"];
1015
+ }
1016
+ async #waitForSelector(selector, timeout) {
1017
+ const deadline = Date.now() + timeout;
1018
+ const selectorJson = JSON.stringify(selector);
1019
+ while (Date.now() < deadline) {
1020
+ if (await this.#evaluate(`document.querySelector(${selectorJson}) !== null`) === true) return;
1021
+ await new Promise((resolve) => setTimeout(resolve, 100));
1022
+ }
1023
+ throw new BrowserSelectorError(`Timeout waiting for selector: ${selector}`);
1024
+ }
1025
+ #waitForLoadEvent(condition, timeout) {
1026
+ const eventName = condition === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
1027
+ let settled = false;
1028
+ let timer;
1029
+ let handler;
1030
+ const promise = new Promise((resolve, reject) => {
1031
+ timer = setTimeout(() => {
1032
+ if (settled) return;
1033
+ settled = true;
1034
+ this.#client.unsubscribe(eventName, handler, this.#sessionId);
1035
+ reject(new BrowserError(`Navigation timeout after ${timeout}ms`));
1036
+ }, timeout);
1037
+ handler = () => {
1038
+ if (settled) return;
1039
+ settled = true;
1040
+ clearTimeout(timer);
1041
+ this.#client.unsubscribe(eventName, handler, this.#sessionId);
1042
+ resolve();
1043
+ };
1044
+ this.#client.subscribe(eventName, handler, this.#sessionId);
1045
+ });
1046
+ const cancel = () => {
1047
+ if (settled) return;
1048
+ settled = true;
1049
+ clearTimeout(timer);
1050
+ this.#client.unsubscribe(eventName, handler, this.#sessionId);
1051
+ };
1052
+ return {
1053
+ promise,
1054
+ cancel
1055
+ };
1056
+ }
1057
+ };
1058
+ //#endregion
1059
+ //#region src/core/BrowserContext.ts
1060
+ var BrowserContext = class {
1061
+ #client;
1062
+ #id;
1063
+ #viewport;
1064
+ #writer;
1065
+ #pages = /* @__PURE__ */ new Map();
1066
+ constructor(client, id, viewport, writer) {
1067
+ this.#client = client;
1068
+ this.#id = id;
1069
+ this.#viewport = viewport;
1070
+ this.#writer = writer;
1071
+ }
1072
+ get id() {
1073
+ return this.#id;
1074
+ }
1075
+ page(index) {
1076
+ const i = index ?? 0;
1077
+ const values = [...this.#pages.values()];
1078
+ return i >= 0 && i < values.length ? values[i] : void 0;
1079
+ }
1080
+ pages() {
1081
+ return [...this.#pages.values()];
1082
+ }
1083
+ async create(options) {
1084
+ const createParams = { url: "about:blank" };
1085
+ if (this.#id !== void 0) createParams["browserContextId"] = this.#id;
1086
+ const result = await this.#client.send("Target.createTarget", createParams);
1087
+ if (!isRecord(result) || !isString(result["targetId"])) throw new Error("Failed to create new browser target");
1088
+ const targetId = result["targetId"];
1089
+ const attachResult = await this.#client.send("Target.attachToTarget", {
1090
+ targetId,
1091
+ flatten: true
1092
+ });
1093
+ if (!isRecord(attachResult) || !isString(attachResult["sessionId"])) throw new Error("Failed to attach to browser target");
1094
+ const sessionId = attachResult["sessionId"];
1095
+ await this.#client.send("Page.enable", void 0, sessionId);
1096
+ await this.#client.send("Runtime.enable", void 0, sessionId);
1097
+ const viewport = options?.viewport ?? this.#viewport;
1098
+ if (viewport !== void 0) await this.#client.send("Emulation.setDeviceMetricsOverride", {
1099
+ width: viewport.width,
1100
+ height: viewport.height,
1101
+ deviceScaleFactor: 1,
1102
+ mobile: false
1103
+ }, sessionId);
1104
+ const page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, options?.url);
1105
+ if (options?.url !== void 0 && options.url !== "about:blank") await page.navigate(options.url, { timeout: options.timeout });
1106
+ this.#pages.set(targetId, page);
1107
+ return page;
1108
+ }
1109
+ async sync(targets) {
1110
+ const pageTargets = targets.filter((t) => t.type === "page");
1111
+ const targetIds = new Set(pageTargets.map((t) => t.id));
1112
+ for (const [id, page] of this.#pages) {
1113
+ if (targetIds.has(id)) continue;
1114
+ try {
1115
+ await page.close();
1116
+ } catch {}
1117
+ this.#pages.delete(id);
1118
+ }
1119
+ for (const target of pageTargets) {
1120
+ if (this.#pages.has(target.id)) continue;
1121
+ try {
1122
+ const attachResult = await this.#client.send("Target.attachToTarget", {
1123
+ targetId: target.id,
1124
+ flatten: true
1125
+ });
1126
+ if (!isRecord(attachResult) || !isString(attachResult["sessionId"])) continue;
1127
+ const sessionId = attachResult["sessionId"];
1128
+ await this.#client.send("Page.enable", void 0, sessionId);
1129
+ await this.#client.send("Runtime.enable", void 0, sessionId);
1130
+ if (this.#viewport !== void 0) try {
1131
+ await this.#client.send("Emulation.setDeviceMetricsOverride", {
1132
+ width: this.#viewport.width,
1133
+ height: this.#viewport.height,
1134
+ deviceScaleFactor: 1,
1135
+ mobile: false
1136
+ }, sessionId);
1137
+ } catch {}
1138
+ this.#pages.set(target.id, new BrowserPage(this.#client, target.id, sessionId, this.#writer, target.url));
1139
+ } catch {}
1140
+ }
1141
+ }
1142
+ async close() {
1143
+ for (const page of this.#pages.values()) try {
1144
+ await page.close();
1145
+ } catch {}
1146
+ this.#pages.clear();
1147
+ if (this.#id !== void 0) try {
1148
+ await this.#client.send("Target.disposeBrowserContext", { browserContextId: this.#id });
1149
+ } catch {}
1150
+ }
1151
+ };
1152
+ //#endregion
1153
+ //#region src/core/factories.ts
1154
+ /**
1155
+ * Create a CDP client bound to the given transport.
1156
+ *
1157
+ * @param options - The transport (and optional timeout) the client uses
1158
+ * @returns A {@link CDPClientInterface}
1159
+ *
1160
+ * @example
1161
+ * ```ts
1162
+ * import { createCDPClient } from '@src/core'
1163
+ *
1164
+ * const client = createCDPClient({ transport })
1165
+ * await client.connect()
1166
+ * ```
1167
+ */
1168
+ function createCDPClient(options) {
1169
+ return new CDPClient(options);
1170
+ }
1171
+ //#endregion
1172
+ export { BASE64_CHARS, BASE64_LOOKUP, BROWSER_CODEGEN_BINDING_NAME, BROWSER_CODEGEN_SOURCE, BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_DEFAULT_VIEWPORT_HEIGHT, BROWSER_DEFAULT_VIEWPORT_WIDTH, BROWSER_RESULT_LIMIT, BROWSER_RESULT_LIMIT_PATTERN, BROWSER_RESULT_LIMIT_SENTINEL_PREFIX, BROWSER_STOP_LOADING_TIMEOUT_MS, BROWSER_WAIT_POLL_INTERVAL_MS, BrowserCodegen, BrowserContext, BrowserError, BrowserPage, BrowserResultLimitError, BrowserSelectorError, CDPClient, CDPConnectionError, CDPError, CDPTimeoutError, compileCodegenScript, createCDPClient, decodeBase64, guardEvaluateExpression, isBrowserError, isBrowserResultLimitError, isBrowserSelectorError, isCDPConnectionError, isCDPError, isCDPTimeoutError, normalizeCodegenActions, parseCodegenActionPayload, readCodegenNavigateAction };
1173
+
1174
+ //# sourceMappingURL=index.js.map