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