@shotkit/shotium 0.0.1 → 0.1.0

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.
package/README.md CHANGED
@@ -1,3 +1,171 @@
1
1
  # @shotkit/shotium
2
2
 
3
- Placeholder release. Real content is published via CI (OIDC).
3
+ > High-performance static HTML/CSS screenshot engine powered by a stripped Chromium Blink core.
4
+
5
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/sj817/shotium/blob/main/LICENSE)
6
+ [![npm version](https://img.shields.io/npm/v/@shotkit/shotium.svg)](https://www.npmjs.com/package/@shotkit/shotium)
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ `@shotkit/shotium` provides Node.js / TypeScript bindings for **shotium**, a stripped-down Chromium engine built specifically for fast static page rendering. By completely removing V8 and browser chrome overhead, Shotium delivers cold starts under 350 ms, single-shot captures in ~47 ms, and an idle memory footprint of ~58 MB.
13
+
14
+ ```ts
15
+ import shotium from '@shotkit/shotium';
16
+
17
+ shotium.runtime.start({ workers: 4 });
18
+
19
+ const png = await shotium.screenshot({
20
+ file: 'https://example.com',
21
+ viewport: { width: 1280, height: 720 },
22
+ fullPage: true,
23
+ });
24
+
25
+ await shotium.runtime.stop();
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install @shotkit/shotium
34
+ ```
35
+
36
+ Prebuilt platform binaries are installed automatically via npm optional dependencies.
37
+
38
+ ---
39
+
40
+ ## Usage
41
+
42
+ ### 1. Multi-Process Pool (`runtime`)
43
+
44
+ Recommended for standard backend servers and continuous job queues.
45
+
46
+ ```ts
47
+ import { runtime, screenshot } from '@shotkit/shotium';
48
+
49
+ // Optional: listen to runtime lifecycle events
50
+ runtime.on('crash', ({ worker }) => console.warn(`Worker ${worker} recovered from crash`));
51
+ runtime.on('timeout', ({ worker, timeout }) => console.warn(`Worker ${worker} timed out (${timeout}ms)`));
52
+
53
+ // Start pool
54
+ runtime.start({
55
+ workers: 4, // Default: Math.max(1, Math.floor(cpuCount / 2))
56
+ cacheDir: '/var/tmp/shotium-cache' // Optional HTTP disk cache
57
+ });
58
+
59
+ // Take screenshot (returns Buffer or writes to disk if 'path' is specified)
60
+ const buffer = await screenshot({
61
+ file: 'https://example.com',
62
+ viewport: { width: 1280, height: 720 },
63
+ type: 'webp',
64
+ quality: 85,
65
+ });
66
+
67
+ await runtime.stop();
68
+ ```
69
+
70
+ ---
71
+
72
+ ### 2. Resident Daemon (`daemon`)
73
+
74
+ Recommended for CLI tools, ephemeral CI tasks, or serverless workers where startup latency is critical.
75
+
76
+ `daemon` keeps a pre-warmed worker pool listening behind a local socket (Named Pipe on Windows, Unix domain socket on POSIX).
77
+
78
+ ```ts
79
+ import { daemon } from '@shotkit/shotium';
80
+
81
+ // Connect to existing daemon (automatically starts one if none is running)
82
+ const client = await daemon.connect({ workers: 4 });
83
+
84
+ const png = await client.screenshot({
85
+ file: 'https://example.com',
86
+ viewport: { width: 1280, height: 720 },
87
+ });
88
+
89
+ client.close();
90
+
91
+ // Check status or stop daemon
92
+ const status = await daemon.status();
93
+ await daemon.stop();
94
+ ```
95
+
96
+ ---
97
+
98
+ ### 3. In-Process Native Engine (`@shotkit/shotium/native`)
99
+
100
+ Recommended for single-process, single-threaded batch rendering with minimum overhead (~31 ms per shot).
101
+
102
+ ```ts
103
+ import { native } from '@shotkit/shotium/native';
104
+
105
+ const png = await native.screenshot({
106
+ file: 'https://example.com',
107
+ viewport: { width: 1280, height: 720 },
108
+ });
109
+
110
+ // Purge cache and release working set after batch
111
+ native.purge({ releaseWorkingSet: true });
112
+ await native.stop();
113
+ ```
114
+
115
+ ---
116
+
117
+ ## API Reference
118
+
119
+ ### `ScreenshotOptions`
120
+
121
+ ```ts
122
+ interface ScreenshotOptions {
123
+ /** Target URL (http/https/file) or local file path */
124
+ file: string;
125
+
126
+ /** Output format (default: 'png') */
127
+ type?: 'png' | 'jpeg' | 'webp';
128
+
129
+ /** Viewport dimensions (default: 1280x720) */
130
+ viewport?: { width?: number; height?: number };
131
+
132
+ /** Capture full scrollable document */
133
+ fullPage?: boolean;
134
+
135
+ /** Capture element bounding box matching selector */
136
+ selector?: string;
137
+
138
+ /** Capture specific rectangular crop */
139
+ clip?: { x: number; y: number; width: number; height: number };
140
+
141
+ /** Image compression quality: 1-100 (jpeg and webp only, default: 90) */
142
+ quality?: number;
143
+
144
+ /** Device scale factor: 0.01 - 8.0 (default: 1.0) */
145
+ scale?: number;
146
+
147
+ /** Preserve transparent background (png/webp only) */
148
+ omitBackground?: boolean;
149
+
150
+ /** Output file destination path (returns null if specified) */
151
+ path?: string;
152
+
153
+ /** Navigation & wait options */
154
+ pageGotoParams?: {
155
+ timeout?: number;
156
+ waitUntil?: 'load' | 'networkidle';
157
+ };
158
+
159
+ /** Allow document to access local file:// resources (default: false) */
160
+ allowFileAccess?: boolean;
161
+
162
+ /** Auto retry count on failure (default: 0) */
163
+ retry?: number;
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## License
170
+
171
+ BSD-3-Clause. See [LICENSE](https://github.com/sj817/shotium/blob/main/LICENSE) for details.
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,322 @@
1
+ import { a as resolveStartOptions, i as endpointFor, n as FrameReader, r as encodeFrame, t as Pool } from "./pool-BSgS6vkr.js";
2
+ import { EventEmitter } from "node:events";
3
+ import fs from "node:fs";
4
+ import net from "node:net";
5
+
6
+ //#region src/lib/daemon.ts
7
+ const VERSION = (() => {
8
+ try {
9
+ const manifest = fs.readFileSync(new URL("../package.json", import.meta.url), "utf8");
10
+ return JSON.parse(manifest).version ?? "0.0.0";
11
+ } catch {
12
+ return "0.0.0";
13
+ }
14
+ })();
15
+ const SUPERVISOR_MARGIN_MS = 1e4;
16
+ const DEFAULT_TIMEOUT_MS = 3e4;
17
+ const DEFAULT_IDLE_TIMEOUT_MS = 3e5;
18
+ var Daemon = class extends EventEmitter {
19
+ options;
20
+ endpointPath;
21
+ idleTimeoutMs;
22
+ prewarmOnStart;
23
+ pool = null;
24
+ server = null;
25
+ sockets = /* @__PURE__ */ new Set();
26
+ inFlight = 0;
27
+ served = 0;
28
+ warmed = false;
29
+ startedAt = Date.now();
30
+ idleTimer = null;
31
+ closing = false;
32
+ constructor(options = {}) {
33
+ super();
34
+ this.options = resolveStartOptions(options);
35
+ this.endpointPath = endpointFor({
36
+ ...this.options,
37
+ name: options.name,
38
+ endpoint: options.endpoint
39
+ });
40
+ this.idleTimeoutMs = options.idleTimeoutMs === void 0 ? DEFAULT_IDLE_TIMEOUT_MS : options.idleTimeoutMs;
41
+ this.prewarmOnStart = options.prewarm !== false;
42
+ }
43
+ get endpoint() {
44
+ return this.endpointPath;
45
+ }
46
+ get warm() {
47
+ return this.warmed;
48
+ }
49
+ async listen() {
50
+ const pool = new Pool(this.options);
51
+ this.pool = pool;
52
+ for (const event of [
53
+ "exit",
54
+ "crash",
55
+ "timeout",
56
+ "worker-restart",
57
+ "worker-error",
58
+ "stderr"
59
+ ]) pool.on(event, (payload) => this.emit(event, payload));
60
+ pool.start();
61
+ this.server = net.createServer((socket) => this.accept(socket));
62
+ this.server.on("error", (error) => this.emit("error", error));
63
+ await this.bind();
64
+ this.armIdleTimer();
65
+ this.emit("ready", {
66
+ endpoint: this.endpointPath,
67
+ workers: this.options.workers
68
+ });
69
+ if (this.prewarmOnStart) await this.prewarm();
70
+ return this;
71
+ }
72
+ bind() {
73
+ return new Promise((resolve, reject) => {
74
+ const server = this.server;
75
+ const onError = (error) => {
76
+ if (error.code === "EADDRINUSE" && process.platform !== "win32") {
77
+ const probe = net.connect(this.endpointPath);
78
+ probe.on("connect", () => {
79
+ probe.destroy();
80
+ reject(error);
81
+ });
82
+ probe.on("error", () => {
83
+ try {
84
+ fs.unlinkSync(this.endpointPath);
85
+ } catch {
86
+ reject(error);
87
+ return;
88
+ }
89
+ server.listen(this.endpointPath, () => {
90
+ this.restrict();
91
+ resolve();
92
+ });
93
+ });
94
+ return;
95
+ }
96
+ reject(error);
97
+ };
98
+ server.once("error", onError);
99
+ server.listen(this.endpointPath, () => {
100
+ server.removeListener("error", onError);
101
+ this.restrict();
102
+ resolve();
103
+ });
104
+ });
105
+ }
106
+ restrict() {
107
+ if (process.platform === "win32") return;
108
+ try {
109
+ fs.chmodSync(this.endpointPath, 384);
110
+ } catch (error) {
111
+ this.emit("error", error);
112
+ }
113
+ }
114
+ async prewarm() {
115
+ const blank = "data:text/html,<!doctype html><title>shotium</title>";
116
+ await Promise.all(Array.from({ length: this.options.workers }, () => {
117
+ return this.pool.submit({
118
+ file: blank,
119
+ width: 16,
120
+ height: 16
121
+ }, {
122
+ timeout: 4e4,
123
+ retry: 1
124
+ }).catch(() => null);
125
+ }));
126
+ this.warmed = true;
127
+ this.emit("warm", { workers: this.options.workers });
128
+ }
129
+ status() {
130
+ return {
131
+ ok: true,
132
+ pid: process.pid,
133
+ endpoint: this.endpointPath,
134
+ binary: this.options.binary,
135
+ workers: this.options.workers,
136
+ cacheDir: this.options.cacheDir,
137
+ args: this.options.args,
138
+ warm: this.warmed,
139
+ uptimeMs: Date.now() - this.startedAt,
140
+ connections: this.sockets.size,
141
+ inFlight: this.inFlight,
142
+ served: this.served,
143
+ idleTimeoutMs: this.idleTimeoutMs,
144
+ version: VERSION
145
+ };
146
+ }
147
+ accept(socket) {
148
+ socket.on("error", () => socket.destroy());
149
+ this.sockets.add(socket);
150
+ this.armIdleTimer();
151
+ const reader = new FrameReader();
152
+ socket.on("data", (chunk) => {
153
+ reader.push(chunk);
154
+ for (;;) {
155
+ const frame = reader.next();
156
+ if (frame === null) return;
157
+ this.dispatch(socket, frame);
158
+ }
159
+ });
160
+ socket.on("close", () => {
161
+ this.sockets.delete(socket);
162
+ this.armIdleTimer();
163
+ });
164
+ }
165
+ dispatch(socket, frame) {
166
+ let message;
167
+ try {
168
+ message = JSON.parse(frame.toString("utf8"));
169
+ } catch {
170
+ this.reply(socket, {
171
+ id: null,
172
+ ok: false,
173
+ error: "shotium: request is not JSON"
174
+ });
175
+ return;
176
+ }
177
+ const id = message.id === void 0 ? null : message.id;
178
+ const op = message.op || "screenshot";
179
+ if (op === "status") {
180
+ this.reply(socket, {
181
+ ...this.status(),
182
+ id
183
+ });
184
+ return;
185
+ }
186
+ if (op === "ping") {
187
+ this.reply(socket, {
188
+ id,
189
+ ok: true
190
+ });
191
+ return;
192
+ }
193
+ if (op === "shutdown") {
194
+ this.reply(socket, {
195
+ id,
196
+ ok: true,
197
+ stopping: true
198
+ });
199
+ socket.end(() => void this.close());
200
+ return;
201
+ }
202
+ if (op !== "screenshot") {
203
+ this.reply(socket, {
204
+ id,
205
+ ok: false,
206
+ error: `shotium: unknown op "${op}"`
207
+ });
208
+ return;
209
+ }
210
+ const request = message.request || {};
211
+ const timeout = (typeof message.timeout === "number" ? message.timeout : DEFAULT_TIMEOUT_MS) + SUPERVISOR_MARGIN_MS;
212
+ const retry = typeof message.retry === "number" ? message.retry : 0;
213
+ this.inFlight += 1;
214
+ this.armIdleTimer();
215
+ this.emit("request", {
216
+ id,
217
+ file: request.file
218
+ });
219
+ this.pool.submit(request, {
220
+ timeout,
221
+ retry
222
+ }).then((result) => {
223
+ this.served += 1;
224
+ this.reply(socket, {
225
+ id,
226
+ ok: true,
227
+ bytes: result.image ? result.image.length : 0,
228
+ path: result.header ? result.header.path : void 0
229
+ }, result.image);
230
+ }).catch((error) => {
231
+ this.reply(socket, {
232
+ id,
233
+ ok: false,
234
+ error: String(error.message || error)
235
+ });
236
+ }).finally(() => {
237
+ this.inFlight -= 1;
238
+ this.emit("response", { id });
239
+ this.armIdleTimer();
240
+ });
241
+ }
242
+ reply(socket, header, payload) {
243
+ if (socket.destroyed) return;
244
+ socket.write(encodeFrame(Buffer.from(JSON.stringify(header), "utf8")));
245
+ socket.write(encodeFrame(payload || Buffer.alloc(0)));
246
+ }
247
+ armIdleTimer() {
248
+ if (this.idleTimer) {
249
+ clearTimeout(this.idleTimer);
250
+ this.idleTimer = null;
251
+ }
252
+ if (!this.idleTimeoutMs || this.closing) return;
253
+ if (this.sockets.size > 0 || this.inFlight > 0) return;
254
+ this.idleTimer = setTimeout(() => {
255
+ this.emit("idle-exit", { idleTimeoutMs: this.idleTimeoutMs });
256
+ this.close();
257
+ }, this.idleTimeoutMs);
258
+ this.idleTimer.unref();
259
+ }
260
+ async close() {
261
+ if (this.closing) return;
262
+ this.closing = true;
263
+ if (this.idleTimer) {
264
+ clearTimeout(this.idleTimer);
265
+ this.idleTimer = null;
266
+ }
267
+ for (const socket of this.sockets) socket.destroy();
268
+ this.sockets.clear();
269
+ await new Promise((resolve) => this.server.close(() => resolve()));
270
+ await this.pool.stop();
271
+ this.emit("close", {});
272
+ }
273
+ };
274
+
275
+ //#endregion
276
+ //#region src/daemon_main.ts
277
+ async function main() {
278
+ const encoded = process.argv[2];
279
+ if (!encoded) {
280
+ process.stderr.write("shotium: daemon_main expects a base64 config\n");
281
+ process.exit(2);
282
+ }
283
+ const options = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
284
+ const daemon = new Daemon(options);
285
+ daemon.on("stderr", ({ worker, line }) => {
286
+ process.stderr.write(`shotium worker ${worker}: ${line}\n`);
287
+ });
288
+ for (const event of [
289
+ "crash",
290
+ "timeout",
291
+ "worker-restart",
292
+ "worker-error",
293
+ "idle-exit"
294
+ ]) daemon.on(event, (payload) => {
295
+ const detail = payload && payload.error ? {
296
+ ...payload,
297
+ error: String(payload.error.message ?? payload.error)
298
+ } : payload;
299
+ process.stderr.write(`shotium daemon ${event}: ${JSON.stringify(detail)}\n`);
300
+ });
301
+ daemon.on("error", (error) => {
302
+ process.stderr.write(`shotium daemon error: ${error && error.message || error}\n`);
303
+ });
304
+ try {
305
+ await daemon.listen();
306
+ } catch (error) {
307
+ if (error?.code === "EADDRINUSE") process.exit(0);
308
+ process.stderr.write(`shotium: daemon failed to start: ${error}\n`);
309
+ process.exit(1);
310
+ }
311
+ const shutdown = () => {
312
+ daemon.close().then(() => process.exit(0), () => process.exit(1));
313
+ };
314
+ process.on("SIGINT", shutdown);
315
+ process.on("SIGTERM", shutdown);
316
+ daemon.on("close", () => process.exit(0));
317
+ }
318
+ main();
319
+
320
+ //#endregion
321
+ export { };
322
+ //# sourceMappingURL=daemon_main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon_main.js","names":[],"sources":["../src/lib/daemon.ts","../src/daemon_main.ts"],"sourcesContent":["import {EventEmitter} from 'node:events';\nimport fs from 'node:fs';\nimport net from 'node:net';\n\nimport type {DaemonOptions, DaemonStatus} from '../types.js';\n\nimport {resolveStartOptions} from './config.js';\nimport type {ResolvedStartOptions} from './config.js';\nimport {endpointFor} from './endpoint.js';\nimport {Pool} from './pool.js';\nimport {FrameReader, encodeFrame} from './protocol.js';\nimport type {WireRequest} from './request.js';\n\n// Our own version, for status(). Read rather than imported: an import\n// attribute would do it too, but only on a node new enough that this package\n// would not run on the rest. The URL is relative to the built module, which\n// sits one directory below the manifest.\nconst VERSION = (() => {\n try {\n const manifest =\n fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8');\n return (JSON.parse(manifest) as {version?: string}).version ?? '0.0.0';\n } catch {\n return '0.0.0';\n }\n})();\n\n// How much longer than the page's own deadline the daemon waits before it\n// decides a worker is not going to answer at all. Same margin, same reasoning\n// as index.ts: the worker fails a slow page by itself and replies.\nconst SUPERVISOR_MARGIN_MS = 10000;\nconst DEFAULT_TIMEOUT_MS = 30000;\nconst DEFAULT_IDLE_TIMEOUT_MS = 300000;\n\n// One message off the socket. `op` defaults to screenshot because that is what\n// almost every message is.\ninterface DaemonMessage {\n id?: number|null;\n op?: 'screenshot'|'status'|'ping'|'shutdown';\n request?: WireRequest;\n timeout?: number;\n retry?: number;\n}\n\ninterface DaemonReply {\n id: number|null;\n ok?: boolean;\n error?: string;\n bytes?: number;\n path?: string;\n stopping?: boolean;\n}\n\n// A worker pool that outlives the process that asked for it.\n//\n// The pool in index.ts is already resident, but only for as long as the Node\n// process holding it: a command-line invocation, a CI step, a serverless\n// handler and a `node -e` all pay for starting workers and then throw them\n// away. This is the same pool behind a socket, so the second caller -- in a\n// different process, minutes later -- pays a connect() and nothing else.\n//\n// The wire format is the worker's own, one level up: a request frame of JSON,\n// answered by a header frame and a payload frame. What it adds is `id`, so one\n// connection can have several requests in flight; the worker protocol cannot,\n// because a worker renders one document at a time, and multiplexing is exactly\n// what the pool in the middle is for.\n//\n// -> [len][{\"id\":7,\"op\":\"screenshot\",\"request\":{...}}]\n// <- [len][{\"id\":7,\"ok\":true,\"bytes\":97756}] [len][<PNG>]\n//\n// Events: ready, request, response, idle-exit, error, plus the pool's own.\nclass Daemon extends EventEmitter {\n private readonly options: ResolvedStartOptions;\n private readonly endpointPath: string;\n private readonly idleTimeoutMs: number;\n private readonly prewarmOnStart: boolean;\n private pool: Pool|null = null;\n private server: net.Server|null = null;\n private sockets = new Set<net.Socket>();\n private inFlight = 0;\n private served = 0;\n private warmed = false;\n private startedAt = Date.now();\n private idleTimer: NodeJS.Timeout|null = null;\n private closing = false;\n\n constructor(options: DaemonOptions = {}) {\n super();\n this.options = resolveStartOptions(options);\n this.endpointPath = endpointFor({\n ...this.options,\n name: options.name,\n endpoint: options.endpoint,\n });\n this.idleTimeoutMs = options.idleTimeoutMs === undefined ?\n DEFAULT_IDLE_TIMEOUT_MS :\n options.idleTimeoutMs;\n this.prewarmOnStart = options.prewarm !== false;\n }\n\n get endpoint(): string {\n return this.endpointPath;\n }\n\n get warm(): boolean {\n return this.warmed;\n }\n\n // Brings the pool up and starts listening. The pipe existing *is* the\n // readiness signal -- a client's connect() either succeeds or the daemon is\n // not up -- so nothing is bound until the pool has been asked to start.\n async listen(): Promise<this> {\n const pool = new Pool(this.options);\n this.pool = pool;\n for (const event of ['exit', 'crash', 'timeout', 'worker-restart',\n 'worker-error', 'stderr']) {\n pool.on(event, (payload) => this.emit(event, payload));\n }\n pool.start();\n\n this.server = net.createServer((socket) => this.accept(socket));\n this.server.on('error', (error) => this.emit('error', error));\n await this.bind();\n this.armIdleTimer();\n this.emit('ready',\n {endpoint: this.endpointPath, workers: this.options.workers});\n if (this.prewarmOnStart) {\n await this.prewarm();\n }\n return this;\n }\n\n private bind(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const server = this.server!;\n const onError = (error: NodeJS.ErrnoException) => {\n // A unix socket file outlives the process that made it, so EADDRINUSE\n // means either a live daemon or a leftover path. Connecting is the only\n // way to tell them apart: refused means nobody is home, and the file\n // can go.\n if (error.code === 'EADDRINUSE' && process.platform !== 'win32') {\n const probe = net.connect(this.endpointPath);\n probe.on('connect', () => {\n probe.destroy();\n reject(error);\n });\n probe.on('error', () => {\n try {\n fs.unlinkSync(this.endpointPath);\n } catch {\n reject(error);\n return;\n }\n server.listen(this.endpointPath, () => {\n this.restrict();\n resolve();\n });\n });\n return;\n }\n reject(error);\n };\n server.once('error', onError);\n server.listen(this.endpointPath, () => {\n server.removeListener('error', onError);\n this.restrict();\n resolve();\n });\n });\n }\n\n // Who may talk to this daemon.\n //\n // It matters because a request may set `allowFileAccess`, so a stranger who\n // can connect can have a document read this machine's filesystem and get the\n // result back as a picture. On POSIX the socket is a file and 0600 says only\n // its owner may connect.\n //\n // On Windows it is a named pipe, and node exposes no way to give one an ACL:\n // the default lets any account on the machine open it. A daemon on a shared\n // Windows host is therefore as trusted as the machine's users are -- render\n // in-process, or with a binary that has no file access, if that is not\n // acceptable.\n private restrict(): void {\n if (process.platform === 'win32') {\n return;\n }\n try {\n fs.chmodSync(this.endpointPath, 0o600);\n } catch (error) {\n this.emit('error', error);\n }\n }\n\n // Renders one throwaway document per worker so that the first real request\n // does not pay for whatever each process initialises lazily. The pool hands\n // one request to each free worker, and there are exactly as many requests as\n // workers, so every process is touched.\n //\n // `data:` rather than a file, because a daemon started without\n // --allow-file-access would otherwise be prewarmed by a request it refuses.\n async prewarm(): Promise<void> {\n const blank = 'data:text/html,<!doctype html><title>shotium</title>';\n await Promise.all(Array.from({length: this.options.workers}, () => {\n return this.pool!\n .submit({file: blank, width: 16, height: 16},\n {timeout: DEFAULT_TIMEOUT_MS + SUPERVISOR_MARGIN_MS, retry: 1})\n .catch(() => null);\n }));\n this.warmed = true;\n this.emit('warm', {workers: this.options.workers});\n }\n\n status(): DaemonStatus {\n return {\n ok: true,\n pid: process.pid,\n endpoint: this.endpointPath,\n binary: this.options.binary,\n workers: this.options.workers,\n cacheDir: this.options.cacheDir,\n args: this.options.args,\n warm: this.warmed,\n uptimeMs: Date.now() - this.startedAt,\n connections: this.sockets.size,\n inFlight: this.inFlight,\n served: this.served,\n idleTimeoutMs: this.idleTimeoutMs,\n version: VERSION,\n };\n }\n\n private accept(socket: net.Socket): void {\n socket.on('error', () => socket.destroy());\n this.sockets.add(socket);\n this.armIdleTimer();\n\n const reader = new FrameReader();\n socket.on('data', (chunk: Buffer) => {\n reader.push(chunk);\n for (;;) {\n const frame = reader.next();\n if (frame === null) {\n return;\n }\n this.dispatch(socket, frame);\n }\n });\n socket.on('close', () => {\n this.sockets.delete(socket);\n this.armIdleTimer();\n });\n }\n\n private dispatch(socket: net.Socket, frame: Buffer): void {\n let message: DaemonMessage;\n try {\n message = JSON.parse(frame.toString('utf8')) as DaemonMessage;\n } catch {\n this.reply(\n socket, {id: null, ok: false, error: 'shotium: request is not JSON'});\n return;\n }\n\n const id = message.id === undefined ? null : message.id;\n const op = message.op || 'screenshot';\n if (op === 'status') {\n this.reply(socket, {...this.status(), id});\n return;\n }\n if (op === 'ping') {\n this.reply(socket, {id, ok: true});\n return;\n }\n if (op === 'shutdown') {\n this.reply(socket, {id, ok: true, stopping: true});\n // After the reply is on the wire, not before: a client that asked for a\n // shutdown is entitled to hear that it happened.\n socket.end(() => void this.close());\n return;\n }\n if (op !== 'screenshot') {\n this.reply(socket, {id, ok: false, error: `shotium: unknown op \"${op}\"`});\n return;\n }\n\n const request = message.request || ({} as WireRequest);\n const timeout = (typeof message.timeout === 'number' ? message.timeout :\n DEFAULT_TIMEOUT_MS) +\n SUPERVISOR_MARGIN_MS;\n const retry = typeof message.retry === 'number' ? message.retry : 0;\n\n this.inFlight += 1;\n this.armIdleTimer();\n this.emit('request', {id, file: request.file});\n this.pool!.submit(request, {timeout, retry})\n .then((result) => {\n this.served += 1;\n this.reply(\n socket,\n {\n id,\n ok: true,\n bytes: result.image ? result.image.length : 0,\n path: result.header ? result.header.path : undefined,\n },\n result.image);\n })\n .catch((error: Error) => {\n this.reply(\n socket, {id, ok: false, error: String(error.message || error)});\n })\n .finally(() => {\n this.inFlight -= 1;\n this.emit('response', {id});\n this.armIdleTimer();\n });\n }\n\n private reply(\n socket: net.Socket, header: DaemonReply|(DaemonStatus&{id: number|null}),\n payload?: Buffer|null): void {\n if (socket.destroyed) {\n return;\n }\n socket.write(encodeFrame(Buffer.from(JSON.stringify(header), 'utf8')));\n socket.write(encodeFrame(payload || Buffer.alloc(0)));\n }\n\n // Idle is \"nobody connected and nothing rendering\". A client that holds its\n // socket open -- a long-lived service using connect() -- keeps the daemon\n // alive without having to poll it.\n private armIdleTimer(): void {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n if (!this.idleTimeoutMs || this.closing) {\n return;\n }\n if (this.sockets.size > 0 || this.inFlight > 0) {\n return;\n }\n this.idleTimer = setTimeout(() => {\n this.emit('idle-exit', {idleTimeoutMs: this.idleTimeoutMs});\n void this.close();\n }, this.idleTimeoutMs);\n this.idleTimer.unref();\n }\n\n async close(): Promise<void> {\n if (this.closing) {\n return;\n }\n this.closing = true;\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n for (const socket of this.sockets) {\n socket.destroy();\n }\n this.sockets.clear();\n await new Promise<void>((resolve) => this.server!.close(() => resolve()));\n await this.pool!.stop();\n this.emit('close', {});\n }\n}\n\nexport {Daemon, DEFAULT_IDLE_TIMEOUT_MS};\n","// The entry point of a detached daemon process.\n//\n// The configuration arrives as one base64 argument rather than as flags,\n// because it contains paths that a Windows command line would otherwise quote\n// badly, and because the client and the daemon have to agree on it exactly:\n// the endpoint is a hash of these fields, so a value mangled in transit would\n// produce a daemon listening where nobody looks. See endpoint.ts.\n//\n// It is a build entry of its own, and not a chunk, because lib/client.ts\n// spawns it by path -- `node dist/daemon_main.js <base64 json>` -- and a name\n// the bundler chose would be a name that changes.\n\nimport {Daemon} from './lib/daemon.js';\nimport type {DaemonOptions} from './types.js';\n\nasync function main(): Promise<void> {\n const encoded = process.argv[2];\n if (!encoded) {\n process.stderr.write('shotium: daemon_main expects a base64 config\\n');\n process.exit(2);\n }\n const options = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as\n DaemonOptions;\n const daemon = new Daemon(options);\n\n daemon.on('stderr', ({worker, line}: {worker: number, line: string}) => {\n process.stderr.write(`shotium worker ${worker}: ${line}\\n`);\n });\n for (const event of ['crash', 'timeout', 'worker-restart', 'worker-error',\n 'idle-exit']) {\n daemon.on(event, (payload: {error?: unknown}) => {\n // An Error does not survive JSON.stringify -- it comes out as {} -- and\n // its message is the whole point of logging a worker that would not\n // start.\n const detail = payload && payload.error ?\n {\n ...payload,\n error: String(\n (payload.error as Error).message ?? payload.error),\n } :\n payload;\n process.stderr.write(\n `shotium daemon ${event}: ${JSON.stringify(detail)}\\n`);\n });\n }\n // An 'error' with nobody listening is thrown by EventEmitter itself, which\n // would turn a socket that failed after binding -- something the daemon can\n // survive -- into a dead pool.\n daemon.on('error', (error: Error) => {\n process.stderr.write(\n `shotium daemon error: ${(error && error.message) || error}\\n`);\n });\n\n try {\n await daemon.listen();\n } catch (error) {\n // Losing the race to bind is the ordinary outcome when two clients start a\n // daemon at the same moment: the other one is up, this one is not needed,\n // and the client that spawned it will connect to the winner. Anything else\n // is a real failure and says so.\n if ((error as NodeJS.ErrnoException | null)?.code === 'EADDRINUSE') {\n process.exit(0);\n }\n process.stderr.write(`shotium: daemon failed to start: ${error}\\n`);\n process.exit(1);\n }\n\n const shutdown = () => {\n daemon.close().then(() => process.exit(0), () => process.exit(1));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n daemon.on('close', () => process.exit(0));\n}\n\nvoid main();\n"],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import { a as PageGotoParams, c as StartOptions, l as Viewport, n as DaemonOptions, o as PurgeOptions, r as DaemonStatus, s as ScreenshotOptions, t as Clip, u as WorkerEvent } from "./types-x9HtkzeE.js";
2
+ import { EventEmitter } from "node:events";
3
+ import net from "node:net";
4
+ //#region src/lib/client.d.ts
5
+ interface ClientReply {
6
+ id: number;
7
+ ok?: boolean;
8
+ error?: string;
9
+ path?: string;
10
+ }
11
+ interface ClientResult {
12
+ header: ClientReply;
13
+ image: Buffer | null;
14
+ }
15
+ declare class DaemonClient extends EventEmitter {
16
+ private readonly socket;
17
+ private readonly endpointPath;
18
+ private readonly pending;
19
+ private nextId;
20
+ private header;
21
+ private reader;
22
+ constructor(socket: net.Socket, endpoint: string);
23
+ get endpoint(): string;
24
+ get closed(): boolean;
25
+ private onData;
26
+ private settle;
27
+ private failAll;
28
+ send(message: Record<string, unknown>): Promise<ClientResult>;
29
+ /** Resolves to the image, or to null when `path` was given. */
30
+ screenshot(options: ScreenshotOptions): Promise<Buffer | null>;
31
+ status(): Promise<DaemonStatus>;
32
+ shutdown(): Promise<{
33
+ ok: boolean;
34
+ }>;
35
+ close(): void;
36
+ }
37
+ //#endregion
38
+ //#region src/index.d.ts
39
+ /** The five things a caller does with the resident pool. */
40
+ interface Daemon {
41
+ /** Connects, starting a daemon if none is listening. */
42
+ connect(options?: DaemonOptions): Promise<DaemonClient>;
43
+ /** One screenshot through the daemon, connection and all. */
44
+ screenshot(options: ScreenshotOptions & {
45
+ daemon?: DaemonOptions;
46
+ }): Promise<Buffer | null>;
47
+ /** Starts one if it is not up, and reports what is there either way. */
48
+ start(options?: DaemonOptions): Promise<DaemonStatus & {
49
+ spawned: boolean;
50
+ }>;
51
+ status(options?: DaemonOptions): Promise<Partial<DaemonStatus> & {
52
+ running: boolean;
53
+ endpoint: string;
54
+ }>;
55
+ stop(options?: DaemonOptions): Promise<{
56
+ stopped: boolean;
57
+ endpoint: string;
58
+ }>;
59
+ }
60
+ interface Runtime {
61
+ on(event: 'ready', listener: (info: {
62
+ workers: number;
63
+ }) => void): this;
64
+ on(event: 'exit', listener: (event: WorkerEvent) => void): this;
65
+ on(event: 'crash', listener: (event: WorkerEvent) => void): this;
66
+ on(event: 'timeout', listener: (event: {
67
+ worker: number;
68
+ timeout: number;
69
+ }) => void): this;
70
+ on(event: 'worker-restart', listener: (event: {
71
+ worker: number;
72
+ reason: string;
73
+ delay: number;
74
+ }) => void): this;
75
+ /** A worker could not be started at all -- a missing or unusable binary. */
76
+ on(event: 'worker-error', listener: (event: {
77
+ worker: number;
78
+ error: Error;
79
+ }) => void): this;
80
+ on(event: 'stderr', listener: (event: {
81
+ worker: number;
82
+ line: string;
83
+ }) => void): this;
84
+ }
85
+ /**
86
+ * The library's one runtime: a pool of worker processes plus its lifecycle.
87
+ *
88
+ * `runtime` below is the singleton, because the expensive part is the
89
+ * processes and a second runtime would double them for no gain. Anyone who
90
+ * genuinely wants two constructs a Runtime directly.
91
+ *
92
+ * Its pool lives and dies with this process. `daemon` is the same pool behind
93
+ * a socket, for callers whose process does not live long enough to be worth
94
+ * starting one.
95
+ */
96
+ declare class Runtime extends EventEmitter {
97
+ private pool;
98
+ get running(): boolean;
99
+ /**
100
+ * Starts the pool. Safe to call twice; the second call is a no-op, so that
101
+ * library code can call it defensively.
102
+ *
103
+ * Every option has a default: the binary is `$SHOTIUM_BINARY`, then the
104
+ * platform package, then `./bin/shotium.exe`; the worker count is half the
105
+ * cores, at least one and at most four; the cache root is a directory under
106
+ * the system temp, and `null` disables caching.
107
+ */
108
+ start(options?: StartOptions): this;
109
+ /** Stops every worker. The pool can be started again afterwards. */
110
+ stop(): Promise<void>;
111
+ /**
112
+ * Renders one screenshot. Resolves to the encoded image, or to `null` when
113
+ * `path` was given and the worker wrote the file itself.
114
+ */
115
+ screenshot(options: ScreenshotOptions): Promise<Buffer | null>;
116
+ }
117
+ /** The shared pool: one per process, started on first use. */
118
+ declare const runtime: Runtime;
119
+ /** One screenshot through the shared pool, starting it if it is not up. */
120
+ declare const screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
121
+ /**
122
+ * The resident pool: workers that outlive the process that started them,
123
+ * reachable over a named pipe on Windows and a unix socket elsewhere. For
124
+ * callers that are short-lived themselves. See lib/daemon.ts.
125
+ */
126
+ declare const daemon: Daemon;
127
+ declare const _default: {
128
+ Runtime: typeof Runtime;
129
+ runtime: Runtime;
130
+ screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
131
+ daemon: Daemon;
132
+ };
133
+ //#endregion
134
+ export { type Clip, Daemon, type DaemonClient, type DaemonOptions, type DaemonStatus, type PageGotoParams, type PurgeOptions, Runtime, type ScreenshotOptions, type StartOptions, type Viewport, type WorkerEvent, daemon, _default as default, runtime, screenshot };
135
+ //# sourceMappingURL=index.d.ts.map