1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
/**
* Unix socket daemon protocol — wire message types, type guards, and constants.
*
* The daemon speaks JSON-line protocol over a Unix domain socket.
* One JSON object per newline. Four message types: request, response, event, control.
*
* References:
* - CDP message format: https://github.com/aslushnikov/getting-started-with-cdp#protocol-fundamentals
* - net.createServer IPC: https://nodejs.org/docs/latest-v22.x/api/net.html#serverlistenpath-backlog-callback
* - readline line-delimited: https://nodejs.org/docs/latest-v22.x/api/readline.html#event-line
*/
// ── Message types ──────────────────────────────────────────────────────────────
/** A CDP command sent from a pi client to the daemon. */
export type WireRequest = {
readonly type: "request";
/** Client-local CDP id. The daemon remaps this to a globally-unique id for Chrome. */
readonly id: number;
readonly method: string;
readonly params?: Record<string, unknown>;
/** Optional CDP session id. Omitted for browser-level commands. */
readonly sessionId?: string;
};
/** A CDP response (result or error) sent from the daemon back to the pi client. */
export type WireResponse = {
readonly type: "response";
/** Matches the id from the corresponding WireRequest. */
readonly id: number;
readonly result?: unknown;
readonly error?: {
readonly code: number;
readonly message: string;
readonly data?: string;
};
};
/** A CDP event forwarded from Chrome through the daemon to pi client(s). */
export type WireEvent = {
readonly type: "event";
readonly method: string;
readonly params?: Record<string, unknown>;
/** Session-scoped events carry this; browser-level events omit it. */
readonly sessionId?: string;
};
/** Daemon lifecycle control messages. Bidirectional. */
export type WireControl = {
readonly type: "control";
readonly action: "register" | "registered" | "deregister" | "shutdown";
/** pi client namespace (required for register/registered/deregister). */
readonly clientId?: string;
/** Human-readable reason for shutdown. */
readonly reason?: string;
};
/** All messages that may appear on the Unix socket. */
export type WireMessage = WireRequest | WireResponse | WireEvent | WireControl;
// ── Type guard ─────────────────────────────────────────────────────────────────
const isObject = (v: unknown): v is Readonly<Record<string, unknown>> =>
typeof v === "object" && v !== null && !Array.isArray(v);
/**
* Validates that an unknown parsed-JSON value is a well-formed WireMessage.
* Does NOT validate the full CDP semantics — only structural shape.
*/
export const isWireMessage = (v: unknown): v is WireMessage => {
if (!isObject(v)) return false;
const t = v["type"];
if (typeof t !== "string") return false;
switch (t) {
case "request": {
if (typeof v["id"] !== "number") return false;
if (typeof v["method"] !== "string") return false;
const sid = v["sessionId"];
if (sid !== undefined && typeof sid !== "string") return false;
return true;
}
case "response": {
if (typeof v["id"] !== "number") return false;
const errVal = v["error"];
if (errVal !== undefined) {
if (!isObject(errVal)) return false;
if (typeof errVal["message"] !== "string") return false;
if (typeof errVal["code"] !== "number") return false;
}
return true;
}
case "event": {
if (typeof v["method"] !== "string") return false;
const sid = v["sessionId"];
if (sid !== undefined && typeof sid !== "string") return false;
return true;
}
case "control": {
const action = v["action"];
if (typeof action !== "string") return false;
if (!["register", "registered", "deregister", "shutdown"].includes(action)) return false;
const cid = v["clientId"];
if (
(action === "register" || action === "registered" || action === "deregister") &&
typeof cid !== "string"
) {
return false;
}
return true;
}
default:
return false;
}
};
// ── Wire message constructors ──────────────────────────────────────────────────
/** Serialize a WireMessage to a JSON line (no trailing newline — caller adds \\n). */
export const serialize = (msg: WireMessage): string => JSON.stringify(msg);
/** Deserialize a single JSON-line string to a WireMessage. Returns null on parse failure. */
export const deserialize = (line: string): WireMessage | null => {
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
return null;
}
if (!isWireMessage(parsed)) return null;
return parsed;
};
// ── Constants ──────────────────────────────────────────────────────────────────
/** Filesystem path for the daemon's Unix domain socket. */
export const DAEMON_SOCKET_PATH = "/tmp/pi-browser-daemon.sock";
/**
* How long the daemon stays alive with zero connected clients before exiting
* (milliseconds). After this period, the daemon cleans up and shuts down.
*/
export const DAEMON_IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
/**
* Hard cap on concurrent pi client connections. Extra connections are rejected
* immediately to prevent resource exhaustion.
*/
export const DAEMON_MAX_CLIENTS = 16;
/**
* If true, the daemon unlinks (deletes) any stale socket file from a previous
* daemon crash before calling server.listen(). Without this, a crashed daemon
* leaves a socket inode that blocks the next listen() call with EADDRINUSE.
*/
export const DAEMON_STALE_SOCKET_CLEANUP = true;
/** CDP WebSocket connection timeout (ms). */
export const CDP_CONNECT_TIMEOUT_MS = 10_000;
/** CDP command timeout when proxying through daemon (ms). */
export const CDP_COMMAND_TIMEOUT_MS = 10_000;
|