@shotkit/shotium 0.1.0 → 0.3.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 +321 -65
- package/dist/daemon_main.js +29 -44
- package/dist/daemon_main.js.map +1 -1
- package/dist/index.d.ts +551 -57
- package/dist/index.js +425 -73
- package/dist/index.js.map +1 -1
- package/dist/protocol-BTeWJDOa.js +474 -0
- package/dist/protocol-BTeWJDOa.js.map +1 -0
- package/native/binding.cc +184 -3
- package/package.json +7 -11
- package/src/index.ts +179 -89
- package/src/lib/binding.ts +110 -0
- package/src/lib/cache.ts +323 -0
- package/src/lib/client.ts +32 -17
- package/src/lib/config.ts +113 -61
- package/src/lib/daemon.ts +97 -62
- package/src/lib/endpoint.ts +19 -12
- package/src/lib/engine.ts +356 -0
- package/src/lib/platform.ts +1 -7
- package/src/lib/request.ts +14 -16
- package/src/types.ts +216 -42
- package/dist/native.d.ts +0 -66
- package/dist/native.js +0 -127
- package/dist/native.js.map +0 -1
- package/dist/platform-DU8DYqmA.js +0 -32
- package/dist/platform-DU8DYqmA.js.map +0 -1
- package/dist/pool-BSgS6vkr.js +0 -356
- package/dist/pool-BSgS6vkr.js.map +0 -1
- package/dist/request-qZXS3N9f.js +0 -43
- package/dist/request-qZXS3N9f.js.map +0 -1
- package/dist/types-x9HtkzeE.d.ts +0 -156
- package/src/lib/pool.ts +0 -243
- package/src/lib/worker.ts +0 -220
- package/src/native.ts +0 -234
package/README.md
CHANGED
|
@@ -7,110 +7,182 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Quick Start
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### 1. Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# npm
|
|
16
|
+
npm install @shotkit/shotium
|
|
17
|
+
|
|
18
|
+
# pnpm
|
|
19
|
+
pnpm add @shotkit/shotium
|
|
20
|
+
|
|
21
|
+
# yarn
|
|
22
|
+
yarn add @shotkit/shotium
|
|
23
|
+
|
|
24
|
+
# bun
|
|
25
|
+
bun add @shotkit/shotium
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Prebuilt platform binaries are installed automatically via npm optional dependencies across six architectures (Windows, macOS, and Linux on x64 and arm64). No local compiler or postinstall build script is required.
|
|
29
|
+
|
|
30
|
+
The package is published as native ESM:
|
|
31
|
+
- `import` is supported on Node.js 18+.
|
|
32
|
+
- Synchronous `require()` is supported on Node.js 22.12+ / 20.19+.
|
|
33
|
+
- Earlier Node.js versions can use dynamic `await import('@shotkit/shotium')`.
|
|
34
|
+
|
|
35
|
+
### 2. Basic Example
|
|
13
36
|
|
|
14
37
|
```ts
|
|
15
|
-
import shotium from '@shotkit/shotium';
|
|
38
|
+
import shotium, { screenshot } from '@shotkit/shotium';
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
// 1. Initialize engine
|
|
41
|
+
shotium.start();
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
// 2. Render remote URLs, local HTML files, or inline HTML strings (data:text/html)
|
|
44
|
+
const { image, stats } = await screenshot({
|
|
45
|
+
file: 'data:text/html,<h1 style="color: #0969da; font-family: sans-serif;">Hello Shotium</h1>',
|
|
46
|
+
viewport: { width: 800, height: 400 },
|
|
23
47
|
});
|
|
24
48
|
|
|
25
|
-
|
|
49
|
+
console.log(`Rendered in ${stats.timing.render}ms, Total: ${stats.timing.total}ms`);
|
|
50
|
+
|
|
51
|
+
// 3. Shut down engine and release resources
|
|
52
|
+
await shotium.stop();
|
|
26
53
|
```
|
|
27
54
|
|
|
28
55
|
---
|
|
29
56
|
|
|
30
|
-
##
|
|
57
|
+
## Table of Contents
|
|
58
|
+
|
|
59
|
+
- [Overview & Key Highlights](#overview--key-highlights)
|
|
60
|
+
- [Execution Mode Selection Guide](#execution-mode-selection-guide)
|
|
61
|
+
- [Usage Modes](#usage-modes)
|
|
62
|
+
- [1. In-Process Engine](#1-in-process-engine)
|
|
63
|
+
- [2. Resident Daemon](#2-resident-daemon-daemon)
|
|
64
|
+
- [API Reference](#api-reference)
|
|
65
|
+
- [`ScreenshotOptions` & `ScreenshotResult`](#screenshotoptions)
|
|
66
|
+
- [`StartOptions` & `StartResult`](#startoptions)
|
|
67
|
+
- [`CaptureStats` Metrics Breakdown](#capturestats)
|
|
68
|
+
- [`daemon` Module & Status](#daemon-module)
|
|
69
|
+
- [`cache` Management Module](#cache-module)
|
|
70
|
+
- [License](#license)
|
|
31
71
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Overview & Key Highlights
|
|
75
|
+
|
|
76
|
+
`@shotkit/shotium` provides Node.js / TypeScript bindings for **shotium**, a stripped-down Chromium engine built specifically for fast static page rendering.
|
|
77
|
+
|
|
78
|
+
By completely removing V8 and multi-process browser overhead, Shotium delivers:
|
|
79
|
+
- **Fast Cold Starts**: Under 350 ms
|
|
80
|
+
- **High Render Speed**: ~47 ms for single viewport capture (down to ~31 ms in-process)
|
|
81
|
+
- **Low Memory Footprint**: ~58 MB idle memory
|
|
82
|
+
|
|
83
|
+
The engine is loaded into the host process as a Node-API addon wrapping a shared library. With zero child process overhead, no inter-process image copying is needed, and `screenshot()` directly returns the encoded image buffer from Blink.
|
|
35
84
|
|
|
36
|
-
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Execution Mode Selection Guide
|
|
88
|
+
|
|
89
|
+
| Use Case | Recommended Mode | Key Benefit |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| **Long-Running Web / API Services** (Express, Fastify, NestJS) | **In-Process Engine** | Zero IPC overhead, zero process startup cost, lowest per-shot latency (~31 ms). |
|
|
92
|
+
| **CLI Tools / CI Pipelines / Serverless Tasks** | **Resident Daemon** | Cross-process pre-warmed engine reuse; connects in **2.3 ms**, eliminating cold starts. |
|
|
37
93
|
|
|
38
94
|
---
|
|
39
95
|
|
|
40
|
-
## Usage
|
|
96
|
+
## Usage Modes
|
|
41
97
|
|
|
42
|
-
### 1.
|
|
98
|
+
### 1. In-Process Engine
|
|
43
99
|
|
|
44
|
-
|
|
100
|
+
The engine runs directly inside your Node.js process via Node-API, bound to the C ABI in [`shot/shot_api.h`](https://github.com/sj817/shotium/blob/main/shot/shot_api.h). `screenshot()` returns the image buffer encoded directly by Blink (~**31 ms** per shot).
|
|
45
101
|
|
|
46
102
|
```ts
|
|
47
|
-
import {
|
|
103
|
+
import shotium, { screenshot } from '@shotkit/shotium';
|
|
48
104
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
runtime.on('timeout', ({ worker, timeout }) => console.warn(`Worker ${worker} timed out (${timeout}ms)`));
|
|
105
|
+
// Start engine and retrieve cache status
|
|
106
|
+
const { cacheDir, cacheActive } = shotium.start();
|
|
52
107
|
|
|
53
|
-
//
|
|
54
|
-
|
|
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({
|
|
108
|
+
// 1. Capture remote URL
|
|
109
|
+
const res1 = await screenshot({
|
|
61
110
|
file: 'https://example.com',
|
|
62
111
|
viewport: { width: 1280, height: 720 },
|
|
63
112
|
type: 'webp',
|
|
64
113
|
quality: 85,
|
|
65
114
|
});
|
|
66
115
|
|
|
67
|
-
|
|
116
|
+
// 2. Capture dynamically assembled inline HTML string (no temporary files on disk)
|
|
117
|
+
const html = `<div style="padding: 24px; background: #f6f8fa;"><h2>Invoice #1024</h2></div>`;
|
|
118
|
+
const res2 = await screenshot({
|
|
119
|
+
file: `data:text/html;charset=utf-8,${encodeURIComponent(html)}`,
|
|
120
|
+
viewport: { width: 600, height: 300 },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 3. Memory reclamation strategies:
|
|
124
|
+
// - releaseMemory(): clears Blink heap, Skia caches, and PartitionAlloc free lists (instant)
|
|
125
|
+
// - releaseWorkingSet: true: additionally asks OS to reclaim physical working set memory
|
|
126
|
+
shotium.releaseMemory({ releaseWorkingSet: true });
|
|
127
|
+
|
|
128
|
+
// 4. Shut down engine
|
|
129
|
+
await shotium.stop();
|
|
68
130
|
```
|
|
69
131
|
|
|
132
|
+
#### Lifecycle & Execution Model
|
|
133
|
+
|
|
134
|
+
- **Process Singleton**:
|
|
135
|
+
- Blink relies on process-wide statics that cannot be cleanly uninitialized.
|
|
136
|
+
- Each process hosts a single engine instance; subsequent `new Runtime()` calls reuse the existing instance.
|
|
137
|
+
- **Start / Stop Semantics**:
|
|
138
|
+
- Calling `stop()` drains the task queue, marks `running: false`, and releases working set memory.
|
|
139
|
+
- Calling `start()` subsequent times re-activates the engine and preserves its warm disk cache.
|
|
140
|
+
- **Configuration Consistency**:
|
|
141
|
+
- Engine options are established at initial creation.
|
|
142
|
+
- Passing conflicting options in subsequent `start()` calls throws an explicit error rather than silently ignoring parameters.
|
|
143
|
+
- **Concurrency & Parallelism**:
|
|
144
|
+
- Within a single engine instance, concurrent screenshot calls are queued and rendered sequentially.
|
|
145
|
+
- To achieve parallel throughput, scale horizontally across multiple worker processes.
|
|
146
|
+
- **Status Reporting**:
|
|
147
|
+
- `start()` and `status()` return `{ running, cacheDir, cacheActive }`.
|
|
148
|
+
- If `cacheDir` cannot be opened, `cacheActive` is set to `false`, and the engine operates safely in cacheless mode.
|
|
149
|
+
|
|
70
150
|
---
|
|
71
151
|
|
|
72
152
|
### 2. Resident Daemon (`daemon`)
|
|
73
153
|
|
|
74
154
|
Recommended for CLI tools, ephemeral CI tasks, or serverless workers where startup latency is critical.
|
|
75
155
|
|
|
76
|
-
|
|
156
|
+
The daemon runs the engine in a standalone background process exposed via a local IPC socket (Named Pipe on Windows, Unix domain socket on POSIX). It pre-renders a blank page upon startup to warm up all subsystems, allowing clients to connect with ~**2.3 ms** latency.
|
|
77
157
|
|
|
78
158
|
```ts
|
|
79
159
|
import { daemon } from '@shotkit/shotium';
|
|
80
160
|
|
|
81
|
-
// Connect to existing daemon
|
|
82
|
-
const client = await daemon.connect(
|
|
161
|
+
// Connect to existing daemon, or automatically launch one in background
|
|
162
|
+
const client = await daemon.connect();
|
|
83
163
|
|
|
84
|
-
|
|
164
|
+
// Dispatch screenshot request
|
|
165
|
+
const { image, stats } = await client.screenshot({
|
|
85
166
|
file: 'https://example.com',
|
|
86
167
|
viewport: { width: 1280, height: 720 },
|
|
87
168
|
});
|
|
88
169
|
|
|
170
|
+
// Manage daemon status or release memory from client connection
|
|
171
|
+
const clientStatus = await client.status();
|
|
172
|
+
await client.releaseMemory({ releaseWorkingSet: false });
|
|
173
|
+
|
|
89
174
|
client.close();
|
|
90
175
|
|
|
91
|
-
//
|
|
92
|
-
const
|
|
176
|
+
// Global daemon operations (optional)
|
|
177
|
+
const daemonInfo = await daemon.status();
|
|
178
|
+
console.log(`Daemon PID: ${daemonInfo.pid}, Uptime: ${daemonInfo.uptimeMs}ms, Served: ${daemonInfo.served}`);
|
|
179
|
+
|
|
180
|
+
// Stop daemon
|
|
93
181
|
await daemon.stop();
|
|
94
182
|
```
|
|
95
183
|
|
|
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
|
-
```
|
|
184
|
+
- **Request Multiplexing**: Multiple requests can be dispatched concurrently over a single connection; each message carries a unique `id`. The daemon processes and returns responses in completion order.
|
|
185
|
+
- **Multi-Instance Isolation**: Each daemon instance renders requests serially. To scale concurrent rendering, launch multiple distinct daemon instances using unique `name` identifiers.
|
|
114
186
|
|
|
115
187
|
---
|
|
116
188
|
|
|
@@ -120,10 +192,10 @@ await native.stop();
|
|
|
120
192
|
|
|
121
193
|
```ts
|
|
122
194
|
interface ScreenshotOptions {
|
|
123
|
-
/** Target URL (http/https/file) or local file path */
|
|
195
|
+
/** Target URL (http/https/file/data) or local file path */
|
|
124
196
|
file: string;
|
|
125
197
|
|
|
126
|
-
/** Output format (default: 'png') */
|
|
198
|
+
/** Output image format (default: 'png') */
|
|
127
199
|
type?: 'png' | 'jpeg' | 'webp';
|
|
128
200
|
|
|
129
201
|
/** Viewport dimensions (default: 1280x720) */
|
|
@@ -132,40 +204,224 @@ interface ScreenshotOptions {
|
|
|
132
204
|
/** Capture full scrollable document */
|
|
133
205
|
fullPage?: boolean;
|
|
134
206
|
|
|
135
|
-
/** Capture
|
|
207
|
+
/** Capture bounding box of matching CSS selector (resolved via Document::querySelector) */
|
|
136
208
|
selector?: string;
|
|
137
209
|
|
|
138
|
-
/** Capture specific rectangular
|
|
210
|
+
/** Capture specific rectangular coordinate region */
|
|
139
211
|
clip?: { x: number; y: number; width: number; height: number };
|
|
140
212
|
|
|
141
|
-
/**
|
|
213
|
+
/** Compression quality: 1-100 (jpeg and webp only, default: 90) */
|
|
142
214
|
quality?: number;
|
|
143
215
|
|
|
144
216
|
/** Device scale factor: 0.01 - 8.0 (default: 1.0) */
|
|
145
217
|
scale?: number;
|
|
146
218
|
|
|
147
|
-
/** Preserve transparent background (png
|
|
219
|
+
/** Preserve transparent background (png and webp only; jpeg has no alpha channel) */
|
|
148
220
|
omitBackground?: boolean;
|
|
149
221
|
|
|
150
|
-
/** Output file
|
|
222
|
+
/** Output file path. If specified, writes directly to disk and image returns null */
|
|
151
223
|
path?: string;
|
|
152
224
|
|
|
153
|
-
/**
|
|
225
|
+
/** Page navigation options */
|
|
154
226
|
pageGotoParams?: {
|
|
227
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
155
228
|
timeout?: number;
|
|
229
|
+
/**
|
|
230
|
+
* load: wait until DOM is parsed and basic resources loaded (default)
|
|
231
|
+
* networkidle: additionally wait for 500ms window with 0 in-flight requests (for late WebFonts/CSS)
|
|
232
|
+
*/
|
|
156
233
|
waitUntil?: 'load' | 'networkidle';
|
|
157
234
|
};
|
|
158
235
|
|
|
159
|
-
/** Allow document to
|
|
236
|
+
/** Allow document to read local file:// subresources (default: false) */
|
|
160
237
|
allowFileAccess?: boolean;
|
|
161
238
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
239
|
+
/** HTTP cache strategy (default: 'default') */
|
|
240
|
+
cache?: 'default' | 'reload' | 'no-store' | 'only-if-cached';
|
|
241
|
+
|
|
242
|
+
/** Extra request headers, sent to same-origin URLs only */
|
|
243
|
+
headers?: Record<string, string>;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
> **Note**: `fullPage`, `selector`, and `clip` are mutually exclusive. Specifying more than one will throw a validation error.
|
|
248
|
+
|
|
249
|
+
#### `ScreenshotResult` Return Structure
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
interface ScreenshotResult {
|
|
253
|
+
/** Encoded image buffer; null when path parameter was specified (written directly to disk) */
|
|
254
|
+
image: Buffer | null;
|
|
255
|
+
/** Detailed capture timing and network statistics */
|
|
256
|
+
stats: CaptureStats;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Parameter Details
|
|
261
|
+
|
|
262
|
+
- **`file` Input Schemes**:
|
|
263
|
+
- Remote URLs: `https://example.com`
|
|
264
|
+
- Local Paths: `./template.html`, `/absolute/path/index.html`, `file:///...`
|
|
265
|
+
- Inline HTML Strings: `data:text/html;charset=utf-8,<h1>Hello</h1>`
|
|
266
|
+
- **`cache` Strategies** (follows Web Fetch API, applies to document and subresources):
|
|
267
|
+
- `default`: Standard HTTP caching behavior.
|
|
268
|
+
- `reload`: Bypasses existing cache, fetches fresh resources from server, and updates cache.
|
|
269
|
+
- `no-store`: Completely disables reading and writing to cache.
|
|
270
|
+
- `only-if-cached`: Retrieves cached entries only; throws an immediate error if cache misses (no network request).
|
|
271
|
+
- **`headers` Scope**:
|
|
272
|
+
- Strictly adheres to Same-Origin policy.
|
|
273
|
+
- Passed headers (such as `Authorization` or `Cookie`) are sent only to the target site, and never forwarded to cross-origin external stylesheets or fonts.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### `StartOptions`
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
interface StartOptions {
|
|
281
|
+
/**
|
|
282
|
+
* HTTP disk cache directory. Defaults to a project-specific directory
|
|
283
|
+
* under ~/.shotium/cache; set to null to disable disk caching.
|
|
284
|
+
*/
|
|
285
|
+
cacheDir?: string | null;
|
|
286
|
+
|
|
287
|
+
/** Maximum size ceiling for cache directory in bytes (default: 256 MB) */
|
|
288
|
+
cacheMaxBytes?: number;
|
|
289
|
+
|
|
290
|
+
/** Custom User-Agent string */
|
|
291
|
+
userAgent?: string;
|
|
292
|
+
|
|
293
|
+
/** Directory containing shotium_data.pak and shotium_strings.pak */
|
|
294
|
+
resourceDir?: string;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### `StartResult` Return Structure
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
interface StartResult {
|
|
302
|
+
/** Whether the runtime instance is currently started */
|
|
303
|
+
running: boolean;
|
|
304
|
+
/** Active cache directory path (null when caching is disabled) */
|
|
305
|
+
cacheDir: string | null;
|
|
306
|
+
/** Whether cache directory was opened successfully and is active */
|
|
307
|
+
cacheActive: boolean;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### `CaptureStats`
|
|
314
|
+
|
|
315
|
+
Every capture operation returns detailed timing breakdown and network statistics:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
interface CaptureStats {
|
|
319
|
+
requests: number; // Total resources requested by document (including itself)
|
|
320
|
+
fromCache: number; // Number of resource bodies served from HTTP disk cache
|
|
321
|
+
failed: number; // Number of failed subresource requests
|
|
322
|
+
bytes: number; // Total decoded body bytes (not transfer size)
|
|
323
|
+
httpStatus: number; // Main document HTTP status code (0 for file: / data: URLs)
|
|
324
|
+
finalUrl: string; // Final URL after resolving redirects
|
|
325
|
+
timing: {
|
|
326
|
+
fetch: number; // Document retrieval: DNS, TCP, TLS, and round-trip latency
|
|
327
|
+
render: number; // Rendering: parsing, subresources, styles, layout, paint
|
|
328
|
+
encode: number; // Image encoding duration
|
|
329
|
+
total: number; // Total wall-clock duration
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Metrics & Timing Breakdown
|
|
335
|
+
|
|
336
|
+
- **Network Latency Breakdown**: For cold `https:` requests, `timing.fetch` represents the majority of total latency. Cache hits reduce fetch latency to sub-millisecond levels:
|
|
337
|
+
|
|
338
|
+
| Scenario | `fetch` Latency | `render` Latency | `total` Latency |
|
|
339
|
+
|---|---|---|---|
|
|
340
|
+
| **Local file / Inline HTML** (`file:` / `data:`) | 0.2 ms | 20 ms | 25 ms |
|
|
341
|
+
| **HTTPS (Cold request)** | 321.1 ms | 16 ms | 350 ms |
|
|
342
|
+
| **HTTPS (Cache hit)** | 0.7 ms | 18 ms | 31 ms |
|
|
343
|
+
|
|
344
|
+
- **`fromCache` Semantics**: Indicates that the response body was served from disk. If an entry is revalidated via conditional request (304 Not Modified), network round-trip latency is still incurred while saving body payload transfer.
|
|
345
|
+
- **Failure Diagnostics (`error.stats`)**: When a capture fails or times out, the error object includes `error.stats` containing network metrics prior to the error.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
### `daemon` Module
|
|
350
|
+
|
|
351
|
+
Manages resident daemon instances and IPC connections:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { daemon } from '@shotkit/shotium';
|
|
355
|
+
|
|
356
|
+
// 1. Establish IPC connection
|
|
357
|
+
const client = await daemon.connect({
|
|
358
|
+
name: 'custom-pool', // Optional: daemon naming isolation
|
|
359
|
+
idleTimeoutMs: 300000, // Idle exit timeout when no connections active (default 5 min; 0 = never)
|
|
360
|
+
prewarm: true, // Pre-renders a blank page upon startup to warm up engine (default true)
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// 2. Client instance methods
|
|
364
|
+
const res = await client.screenshot({ file: 'https://example.com' });
|
|
365
|
+
const status = await client.status();
|
|
366
|
+
await client.releaseMemory({ releaseWorkingSet: false });
|
|
367
|
+
client.close();
|
|
368
|
+
|
|
369
|
+
// 3. Global daemon management
|
|
370
|
+
const info: DaemonStatus = await daemon.status();
|
|
371
|
+
await daemon.stop();
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### `DaemonStatus` Structure
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
interface DaemonStatus {
|
|
378
|
+
pid: number; // Daemon OS process ID
|
|
379
|
+
endpoint: string; // IPC socket path / named pipe
|
|
380
|
+
cacheDir: string | null; // Active disk cache directory
|
|
381
|
+
warm: boolean; // Whether engine pre-warm has completed
|
|
382
|
+
uptimeMs: number; // Uptime in milliseconds
|
|
383
|
+
connections: number; // Current active client connections
|
|
384
|
+
inFlight: number; // Requests currently being rendered
|
|
385
|
+
served: number; // Total completed requests
|
|
386
|
+
idleTimeoutMs: number; // Configured idle timeout
|
|
387
|
+
version: string; // Engine version
|
|
164
388
|
}
|
|
165
389
|
```
|
|
166
390
|
|
|
167
391
|
---
|
|
168
392
|
|
|
393
|
+
### `cache` Module
|
|
394
|
+
|
|
395
|
+
Manages persistent HTTP disk caching across processes and engine lifecycles:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
import { cache } from '@shotkit/shotium';
|
|
399
|
+
|
|
400
|
+
// 1. Directory query
|
|
401
|
+
cache.getDir(); // Current project's cache directory (absolute path)
|
|
402
|
+
cache.getDirs({ target: 'all' }); // List all shotium cache directories on the system
|
|
403
|
+
|
|
404
|
+
// 2. List cached file metadata
|
|
405
|
+
const files = await cache.getFiles(); // [{ url, lastUsedMs, bytes, dir }, ...]
|
|
406
|
+
|
|
407
|
+
// 3. Evict cache and inspect result
|
|
408
|
+
const result: CacheClearResult = await cache.clear({
|
|
409
|
+
glob: ['https://example.com/**'], // Evict by URL glob pattern
|
|
410
|
+
maxAge: 86400, // Evict entries unused for > 24h (seconds)
|
|
411
|
+
maxSize: 64 * 1024 * 1024, // Evict via LRU to under 64 MB
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
console.log(`Removed: ${result.removed}, Bytes before: ${result.bytesBefore}, Bytes after: ${result.bytesAfter}`);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Cache Design & Guidelines
|
|
418
|
+
|
|
419
|
+
- **Directory Structure**: Stored under `~/.shotium/cache/<project-hash>`, avoiding ephemeral `/tmp` directories that are automatically purged on reboot.
|
|
420
|
+
- **Index Integrity**: Cache files are stored using URL-key hashes with an internal index file. Cache eviction must be performed via the `cache` API rather than manual file deletion to preserve index consistency.
|
|
421
|
+
- **Cross-Process Sharing**: Multiple processes may concurrently access the same cache directory safely.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
169
425
|
## License
|
|
170
426
|
|
|
171
427
|
BSD-3-Clause. See [LICENSE](https://github.com/sj817/shotium/blob/main/LICENSE) for details.
|
package/dist/daemon_main.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
1
|
+
import { d as resolveStartOptions, i as Engine, n as encodeFrame, r as endpointFor, t as FrameReader } from "./protocol-BTeWJDOa.js";
|
|
3
2
|
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
4
6
|
import net from "node:net";
|
|
5
7
|
|
|
6
8
|
//#region src/lib/daemon.ts
|
|
@@ -12,15 +14,13 @@ const VERSION = (() => {
|
|
|
12
14
|
return "0.0.0";
|
|
13
15
|
}
|
|
14
16
|
})();
|
|
15
|
-
const SUPERVISOR_MARGIN_MS = 1e4;
|
|
16
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
17
17
|
const DEFAULT_IDLE_TIMEOUT_MS = 3e5;
|
|
18
18
|
var Daemon = class extends EventEmitter {
|
|
19
19
|
options;
|
|
20
20
|
endpointPath;
|
|
21
21
|
idleTimeoutMs;
|
|
22
22
|
prewarmOnStart;
|
|
23
|
-
|
|
23
|
+
engine = new Engine();
|
|
24
24
|
server = null;
|
|
25
25
|
sockets = /* @__PURE__ */ new Set();
|
|
26
26
|
inFlight = 0;
|
|
@@ -47,25 +47,12 @@ var Daemon = class extends EventEmitter {
|
|
|
47
47
|
return this.warmed;
|
|
48
48
|
}
|
|
49
49
|
async listen() {
|
|
50
|
-
|
|
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();
|
|
50
|
+
this.engine.start(this.options);
|
|
61
51
|
this.server = net.createServer((socket) => this.accept(socket));
|
|
62
52
|
this.server.on("error", (error) => this.emit("error", error));
|
|
63
53
|
await this.bind();
|
|
64
54
|
this.armIdleTimer();
|
|
65
|
-
this.emit("ready", {
|
|
66
|
-
endpoint: this.endpointPath,
|
|
67
|
-
workers: this.options.workers
|
|
68
|
-
});
|
|
55
|
+
this.emit("ready", { endpoint: this.endpointPath });
|
|
69
56
|
if (this.prewarmOnStart) await this.prewarm();
|
|
70
57
|
return this;
|
|
71
58
|
}
|
|
@@ -112,29 +99,30 @@ var Daemon = class extends EventEmitter {
|
|
|
112
99
|
}
|
|
113
100
|
}
|
|
114
101
|
async prewarm() {
|
|
115
|
-
const blank =
|
|
116
|
-
|
|
117
|
-
|
|
102
|
+
const blank = path.join(os.tmpdir(), `shotium-prewarm-${process.pid}.html`);
|
|
103
|
+
try {
|
|
104
|
+
fs.writeFileSync(blank, "<!doctype html><title>shotium</title><p>shotium");
|
|
105
|
+
await this.engine.capture({
|
|
118
106
|
file: blank,
|
|
119
107
|
width: 16,
|
|
120
108
|
height: 16
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
});
|
|
110
|
+
this.warmed = true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.emit("error", error);
|
|
113
|
+
} finally {
|
|
114
|
+
fs.rmSync(blank, { force: true });
|
|
115
|
+
}
|
|
116
|
+
this.emit("warm", { warm: this.warmed });
|
|
128
117
|
}
|
|
129
118
|
status() {
|
|
130
119
|
return {
|
|
131
120
|
ok: true,
|
|
132
121
|
pid: process.pid,
|
|
133
122
|
endpoint: this.endpointPath,
|
|
134
|
-
binary: this.options.binary,
|
|
135
|
-
workers: this.options.workers,
|
|
136
123
|
cacheDir: this.options.cacheDir,
|
|
137
|
-
|
|
124
|
+
userAgent: this.options.userAgent,
|
|
125
|
+
resourceDir: this.options.resourceDir,
|
|
138
126
|
warm: this.warmed,
|
|
139
127
|
uptimeMs: Date.now() - this.startedAt,
|
|
140
128
|
connections: this.sockets.size,
|
|
@@ -208,30 +196,27 @@ var Daemon = class extends EventEmitter {
|
|
|
208
196
|
return;
|
|
209
197
|
}
|
|
210
198
|
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
199
|
this.inFlight += 1;
|
|
214
200
|
this.armIdleTimer();
|
|
215
201
|
this.emit("request", {
|
|
216
202
|
id,
|
|
217
203
|
file: request.file
|
|
218
204
|
});
|
|
219
|
-
this.
|
|
220
|
-
timeout,
|
|
221
|
-
retry
|
|
222
|
-
}).then((result) => {
|
|
205
|
+
this.engine.capture(request).then(({ image, stats }) => {
|
|
223
206
|
this.served += 1;
|
|
224
207
|
this.reply(socket, {
|
|
225
208
|
id,
|
|
226
209
|
ok: true,
|
|
227
|
-
bytes:
|
|
228
|
-
path:
|
|
229
|
-
|
|
210
|
+
bytes: image ? image.length : 0,
|
|
211
|
+
path: request.path,
|
|
212
|
+
stats
|
|
213
|
+
}, image);
|
|
230
214
|
}).catch((error) => {
|
|
231
215
|
this.reply(socket, {
|
|
232
216
|
id,
|
|
233
217
|
ok: false,
|
|
234
|
-
error: String(error.message || error)
|
|
218
|
+
error: String(error.message || error),
|
|
219
|
+
stats: error.stats
|
|
235
220
|
});
|
|
236
221
|
}).finally(() => {
|
|
237
222
|
this.inFlight -= 1;
|
|
@@ -267,7 +252,7 @@ var Daemon = class extends EventEmitter {
|
|
|
267
252
|
for (const socket of this.sockets) socket.destroy();
|
|
268
253
|
this.sockets.clear();
|
|
269
254
|
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
270
|
-
await this.
|
|
255
|
+
await this.engine.dispose();
|
|
271
256
|
this.emit("close", {});
|
|
272
257
|
}
|
|
273
258
|
};
|