diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-03 09:36:47 +0530 |
|---|---|---|
| committer | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-03 09:37:10 +0530 |
| commit | 28e6390a2a7bdb5db2e70c7c40d8eb884ddedf67 (patch) | |
| tree | a1d939f33ef83039f5d87d182c0401b9566daf7c /src/cdp | |
| parent | 0b32e88f84f36a759d7191e8bdfd09a44789fa95 (diff) | |
refactor: extract event queue from transport into dedicated module
Diffstat (limited to 'src/cdp')
| -rw-r--r-- | src/cdp/daemon-transport.ts | 76 | ||||
| -rw-r--r-- | src/cdp/event-queue.ts | 128 | ||||
| -rw-r--r-- | src/cdp/transport.ts | 73 |
3 files changed, 137 insertions, 140 deletions
diff --git a/src/cdp/daemon-transport.ts b/src/cdp/daemon-transport.ts index 9d4202c..4b2fb78 100644 --- a/src/cdp/daemon-transport.ts +++ b/src/cdp/daemon-transport.ts @@ -17,6 +17,7 @@ import { type Result, err, ok } from "../util/result"; import { type CdpError, cdpError } from "../cdp/errors"; import type { CdpTransport } from "../cdp/transport"; import type { CdpEvent } from "../cdp/types"; +import { type Pending, makeEventQueue, makeOnClose, rejectAllPending, sendWithTimeout } from "./event-queue"; import { DAEMON_SOCKET_PATH, type WireRequest, @@ -25,47 +26,6 @@ import { serialize, } from "../daemon/protocol"; -// ── Types ────────────────────────────────────────────────────────────────────── - -type EventQueue = { - readonly push: (e: CdpEvent) => void; - readonly end: () => void; - readonly iter: AsyncIterable<CdpEvent>; -}; - -// ── Helpers ──────────────────────────────────────────────────────────────────── - -const makeEventQueue = (): EventQueue => { - const buf: CdpEvent[] = []; - const waiters: Array<(v: IteratorResult<CdpEvent>) => void> = []; - let ended = false; - return { - push(e) { - if (ended) return; - const w = waiters.shift(); - if (w) w({ value: e, done: false }); - else buf.push(e); - }, - end() { - ended = true; - for (const w of waiters.splice(0)) w({ value: undefined as unknown as CdpEvent, done: true }); - }, - iter: { - [Symbol.asyncIterator]() { - return { - next: (): Promise<IteratorResult<CdpEvent>> => - new Promise((resolve) => { - const next = buf.shift(); - if (next) resolve({ value: next, done: false }); - else if (ended) resolve({ value: undefined as unknown as CdpEvent, done: true }); - else waiters.push(resolve); - }), - }; - }, - }, - }; -}; - // ── Implementation ───────────────────────────────────────────────────────────── export const createDaemonTransport = (clientId: string): CdpTransport => { @@ -73,20 +33,12 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { let rl: ReturnType<typeof createInterface> | null = null; let queue = makeEventQueue(); const closeListeners = new Set<() => void>(); - const pending = new Map<number, { - resolve: (v: Result<unknown, CdpError>) => void; - timer: ReturnType<typeof setTimeout>; - method: string; - }>(); + const pending = new Map<number, Pending>(); let registered = false; let nextRequestId = 1; const cleanup = (reason: string): void => { - for (const [, p] of pending) { - clearTimeout(p.timer); - p.resolve(err(cdpError("transport_closed", reason, p.method))); - } - pending.clear(); + rejectAllPending(pending, reason); queue.end(); queue = makeEventQueue(); registered = false; @@ -237,22 +189,7 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { ...(sessionId !== null && sessionId !== undefined ? { sessionId } : {}), }; - return new Promise((resolve) => { - const timer = setTimeout(() => { - pending.delete(id); - resolve(err(cdpError("timeout", `Daemon timeout after ${timeoutMs}ms: ${method}`, method))); - }, timeoutMs); - - pending.set(id, { resolve, timer, method }); - - try { - socket!.write(serialize(req) + "\n"); - } catch (e) { - clearTimeout(timer); - pending.delete(id); - resolve(err(cdpError("transport_closed", e instanceof Error ? e.message : String(e), method))); - } - }); + return sendWithTimeout(pending, id, method, timeoutMs, "Daemon", () => socket!.write(serialize(req) + "\n")); }; // ── Events ───────────────────────────────────────────────────────────── @@ -268,10 +205,7 @@ export const createDaemonTransport = (clientId: string): CdpTransport => { return "open"; }; - const onClose = (cb: () => void): (() => void) => { - closeListeners.add(cb); - return () => closeListeners.delete(cb); - }; + const onClose = makeOnClose(closeListeners); return { connect, close, request, events, state, onClose }; }; diff --git a/src/cdp/event-queue.ts b/src/cdp/event-queue.ts new file mode 100644 index 0000000..281fb3f --- /dev/null +++ b/src/cdp/event-queue.ts @@ -0,0 +1,128 @@ +/** + * Shared transport primitives for CdpTransport implementations. + * + * Both the WebSocket transport (transport.ts, talking to Chrome directly) and + * the daemon transport (daemon-transport.ts, talking to the daemon over a Unix + * socket) need an identical single-consumer async event queue plus the same + * pending-request bookkeeping. This module is the single source of truth for + * that shared machinery. + */ + +import { type Result, err } from "../util/result"; +import { type CdpError, cdpError } from "./errors"; +import type { CdpEvent } from "./types"; + +export type CdpResult = Result<unknown, CdpError>; + +/** + * A single-consumer async queue of CDP events. + * + * `push` enqueues an event (dropped silently after `end`). `end` terminates the + * stream — any pending and future iterator `next()` calls resolve `done:true`. + * `iter` is the AsyncIterable consumed by one for-await loop; a second consumer + * would silently steal events from the first. + */ +export type EventQueue = { + readonly push: (e: CdpEvent) => void; + readonly end: () => void; + readonly iter: AsyncIterable<CdpEvent>; +}; + +export const makeEventQueue = (): EventQueue => { + const buf: CdpEvent[] = []; + const waiters: Array<(v: IteratorResult<CdpEvent>) => void> = []; + let ended = false; + return { + push(e) { + if (ended) return; + const w = waiters.shift(); + if (w) w({ value: e, done: false }); + else buf.push(e); + }, + end() { + ended = true; + // value: undefined as unknown as CdpEvent is the correct iterator-protocol + // pattern: when done=true the value field is conventionally undefined but + // the TS type still requires CdpEvent. + for (const w of waiters.splice(0)) w({ value: undefined as unknown as CdpEvent, done: true }); + }, + iter: { + [Symbol.asyncIterator]() { + return { + next: (): Promise<IteratorResult<CdpEvent>> => + new Promise((resolve) => { + const next = buf.shift(); + if (next) resolve({ value: next, done: false }); + else if (ended) resolve({ value: undefined as unknown as CdpEvent, done: true }); + else waiters.push(resolve); + }), + }; + }, + }, + }; +}; + +/** An in-flight CDP request awaiting its response or timeout. */ +export type Pending = { + readonly resolve: (v: Result<unknown, CdpError>) => void; + readonly timer: ReturnType<typeof setTimeout>; + readonly method: string; +}; + +/** + * Reject every in-flight request with a transport_closed error and clear the + * map. Called from each transport's cleanup path when the underlying connection + * goes away. + */ +export const rejectAllPending = (pending: Map<number, Pending>, reason: string): void => { + for (const [, p] of pending) { + clearTimeout(p.timer); + p.resolve(err(cdpError("transport_closed", reason, p.method))); + } + pending.clear(); +}; + +/** + * Send a CDP request and return a promise that settles when the matching + * response arrives, the request times out, or the send throws. + * + * The transport supplies a `send` callback that puts the already-serialized + * payload onto its wire (WebSocket frame or Unix-socket line); everything else + * — registering the pending entry, the timeout timer, and synchronous send + * failure handling — is identical across transports and lives here. + * + * `timeoutLabel` is the transport's prefix for the timeout error message + * (e.g. "CDP" or "Daemon"). + */ +export const sendWithTimeout = ( + pending: Map<number, Pending>, + id: number, + method: string, + timeoutMs: number, + timeoutLabel: string, + send: () => void, +): Promise<CdpResult> => + new Promise((resolve) => { + const timer = setTimeout(() => { + pending.delete(id); + resolve(err(cdpError("timeout", `${timeoutLabel} timeout after ${timeoutMs}ms: ${method}`, method))); + }, timeoutMs); + pending.set(id, { resolve, timer, method }); + try { + send(); + } catch (e) { + clearTimeout(timer); + pending.delete(id); + resolve(err(cdpError("transport_closed", e instanceof Error ? e.message : String(e), method))); + } + }); + +/** + * Register a close listener, returning an unsubscribe function. Identical + * across transports, so it lives here. + */ +export const makeOnClose = (closeListeners: Set<() => void>) => + (cb: () => void): (() => void) => { + closeListeners.add(cb); + return () => closeListeners.delete(cb); + }; diff --git a/src/cdp/transport.ts b/src/cdp/transport.ts index c5a5a41..f3bcd16 100644 --- a/src/cdp/transport.ts +++ b/src/cdp/transport.ts @@ -3,15 +3,10 @@ import { type Result, err, ok } from "../util/result"; import { type CdpError, cdpError } from "./errors"; import { isCdpRawMessage } from "./types"; import type { CdpEvent, CdpRawMessage } from "./types"; +import { type Pending, makeEventQueue, makeOnClose, rejectAllPending, sendWithTimeout } from "./event-queue"; const DEFAULT_TIMEOUT_MS = 15_000; -type Pending = { - readonly resolve: (v: Result<unknown, CdpError>) => void; - readonly timer: ReturnType<typeof setTimeout>; - readonly method: string; -}; - export type CdpTransport = { connect(url: string, opts?: { timeoutMs?: number }): Promise<Result<void, CdpError>>; close(): Promise<void>; @@ -33,46 +28,6 @@ export const createCdpTransport = (): CdpTransport => { const pending = new Map<number, Pending>(); const closeListeners = new Set<() => void>(); - type EventQueue = { - readonly push: (e: CdpEvent) => void; - readonly end: () => void; - readonly iter: AsyncIterable<CdpEvent>; - }; - - const makeEventQueue = (): EventQueue => { - const buf: CdpEvent[] = []; - const waiters: Array<(v: IteratorResult<CdpEvent>) => void> = []; - let ended = false; - return { - push(e) { - if (ended) return; - const w = waiters.shift(); - if (w) w({ value: e, done: false }); - else buf.push(e); - }, - end() { - ended = true; - // value: undefined as unknown as CdpEvent is the correct iterator-protocol - // pattern: when done=true the value field is conventionally undefined but - // the TS type still requires CdpEvent. - for (const w of waiters.splice(0)) w({ value: undefined as unknown as CdpEvent, done: true }); - }, - iter: { - [Symbol.asyncIterator]() { - return { - next: (): Promise<IteratorResult<CdpEvent>> => - new Promise((resolve) => { - const next = buf.shift(); - if (next) resolve({ value: next, done: false }); - else if (ended) resolve({ value: undefined as unknown as CdpEvent, done: true }); - else waiters.push(resolve); - }), - }; - }, - }, - }; - }; - let queue = makeEventQueue(); const handleMessage = (raw: string): void => { @@ -109,11 +64,7 @@ export const createCdpTransport = (): CdpTransport => { }; const cleanup = (reason: string): void => { - for (const [, p] of pending) { - clearTimeout(p.timer); - p.resolve(err(cdpError("transport_closed", reason, p.method))); - } - pending.clear(); + rejectAllPending(pending, reason); queue.end(); queue = makeEventQueue(); for (const cb of closeListeners) cb(); @@ -184,20 +135,7 @@ export const createCdpTransport = (): CdpTransport => { const payload: Record<string, unknown> = { id, method, params }; if (sessionId) payload["sessionId"] = sessionId; const json = JSON.stringify(payload); - return new Promise((resolve) => { - const timer = setTimeout(() => { - pending.delete(id); - resolve(err(cdpError("timeout", `CDP timeout after ${timeoutMs}ms: ${method}`, method))); - }, timeoutMs); - pending.set(id, { resolve, timer, method }); - try { - sock.send(json); - } catch (e) { - clearTimeout(timer); - pending.delete(id); - resolve(err(cdpError("transport_closed", e instanceof Error ? e.message : String(e), method))); - } - }); + return sendWithTimeout(pending, id, method, timeoutMs, "CDP", () => sock.send(json)); }, events(): AsyncIterable<CdpEvent> { // Single-consumer: each connection's event stream may be iterated by @@ -213,9 +151,6 @@ export const createCdpTransport = (): CdpTransport => { if (ws.readyState === WebSocket.OPEN) return "open"; return "closed"; }, - onClose(cb): () => void { - closeListeners.add(cb); - return () => closeListeners.delete(cb); - }, + onClose: makeOnClose(closeListeners), }; }; |
