aboutsummaryrefslogtreecommitdiff
path: root/src/cdp/event-queue.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-03 09:36:47 +0530
committeramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-03 09:37:10 +0530
commit28e6390a2a7bdb5db2e70c7c40d8eb884ddedf67 (patch)
treea1d939f33ef83039f5d87d182c0401b9566daf7c /src/cdp/event-queue.ts
parent0b32e88f84f36a759d7191e8bdfd09a44789fa95 (diff)
refactor: extract event queue from transport into dedicated module
Diffstat (limited to 'src/cdp/event-queue.ts')
-rw-r--r--src/cdp/event-queue.ts128
1 files changed, 128 insertions, 0 deletions
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);
+ };