@shotkit/shotium 0.2.0 → 0.3.1
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 +322 -60
- package/dist/daemon_main.js +9 -7
- package/dist/daemon_main.js.map +1 -1
- package/dist/index.d.ts +406 -35
- package/dist/index.js +389 -41
- package/dist/index.js.map +1 -1
- package/dist/protocol-rQEcQPAC.js +479 -0
- package/dist/protocol-rQEcQPAC.js.map +1 -0
- package/native/binding.cc +212 -11
- package/package.json +7 -7
- package/src/index.ts +145 -33
- package/src/lib/binding.ts +22 -1
- package/src/lib/cache.ts +323 -0
- package/src/lib/client.ts +24 -5
- package/src/lib/config.ts +113 -6
- package/src/lib/daemon.ts +29 -6
- package/src/lib/engine.ts +256 -58
- package/src/lib/request.ts +10 -1
- package/src/types.ts +214 -4
- package/dist/engine-Xe7nH-1i.js +0 -267
- package/dist/engine-Xe7nH-1i.js.map +0 -1
package/src/types.ts
CHANGED
|
@@ -33,6 +33,79 @@ export interface Viewport {
|
|
|
33
33
|
height?: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* What a capture may do with the HTTP cache, spelled the way `fetch` spells
|
|
38
|
+
* it.
|
|
39
|
+
*
|
|
40
|
+
* - `default`: ordinary HTTP semantics. A fresh entry is used without asking,
|
|
41
|
+
* a stale one is revalidated, and the response updates the cache.
|
|
42
|
+
* - `reload`: read nothing, write everything -- the browser's reload button.
|
|
43
|
+
* The next capture is fast again.
|
|
44
|
+
* - `no-store`: neither read nor write. For a page that should not be left on
|
|
45
|
+
* this machine's disk, which an authenticated one usually should not.
|
|
46
|
+
* - `only-if-cached`: the network may not be touched and a miss is an error.
|
|
47
|
+
* Useful for a deterministic re-render of something already fetched.
|
|
48
|
+
*/
|
|
49
|
+
export type CacheMode = 'default'|'reload'|'no-store'|'only-if-cached';
|
|
50
|
+
|
|
51
|
+
/** Where the milliseconds went. */
|
|
52
|
+
export interface CaptureTiming {
|
|
53
|
+
/**
|
|
54
|
+
* Fetching the top-level document. For a cold `https:` URL this is DNS, TCP,
|
|
55
|
+
* TLS and a round trip, and it is routinely larger than everything below --
|
|
56
|
+
* which is the single most useful thing this object says.
|
|
57
|
+
*/
|
|
58
|
+
fetch: number;
|
|
59
|
+
/** Parse, subresources, style, layout, prepaint, paint. */
|
|
60
|
+
render: number;
|
|
61
|
+
/** Page/frame creation and synchronous document installation. */
|
|
62
|
+
setup: number;
|
|
63
|
+
/** Waiting for parsing, load completion and subresources. */
|
|
64
|
+
wait: number;
|
|
65
|
+
/** Capture selection plus style/layout/lifecycle advancement. */
|
|
66
|
+
lifecycle: number;
|
|
67
|
+
/** Extracting Blink's paint record. */
|
|
68
|
+
paint: number;
|
|
69
|
+
/** Raster-surface preparation and paint-record replay. */
|
|
70
|
+
raster: number;
|
|
71
|
+
encode: number;
|
|
72
|
+
/** Wall clock for the whole capture, so the phases above can be checked. */
|
|
73
|
+
total: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What one capture cost, and where its bytes came from. */
|
|
77
|
+
export interface CaptureStats {
|
|
78
|
+
/** Every resource the document asked for, itself included. */
|
|
79
|
+
requests: number;
|
|
80
|
+
/**
|
|
81
|
+
* Answered from the HTTP cache -- the body came from disk.
|
|
82
|
+
*
|
|
83
|
+
* Not the same as "no network was touched". A stale entry that can be
|
|
84
|
+
* revalidated costs a conditional request and a 304, and counts here too;
|
|
85
|
+
* what the cache saved is the download rather than the round trip. That is
|
|
86
|
+
* why `timing.fetch` can be tens of milliseconds with this set.
|
|
87
|
+
*/
|
|
88
|
+
fromCache: number;
|
|
89
|
+
failed: number;
|
|
90
|
+
/** Decoded body bytes, summed -- not the transfer size. */
|
|
91
|
+
bytes: number;
|
|
92
|
+
/** The document's own status. 0 for a `file:` URL. */
|
|
93
|
+
httpStatus: number;
|
|
94
|
+
/** After redirects, which is what relative URLs resolved against. */
|
|
95
|
+
finalUrl: string;
|
|
96
|
+
timing: CaptureTiming;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One screenshot, and what taking it cost. */
|
|
100
|
+
export interface ScreenshotResult {
|
|
101
|
+
/**
|
|
102
|
+
* The encoded image, or `null` when `path` was given: the engine wrote the
|
|
103
|
+
* file itself and there is nothing left to hand back.
|
|
104
|
+
*/
|
|
105
|
+
image: Buffer|null;
|
|
106
|
+
stats: CaptureStats;
|
|
107
|
+
}
|
|
108
|
+
|
|
36
109
|
export interface ScreenshotOptions {
|
|
37
110
|
/** An http/https/file URL, or a local path. */
|
|
38
111
|
file: string;
|
|
@@ -68,15 +141,48 @@ export interface ScreenshotOptions {
|
|
|
68
141
|
* is rendered on.
|
|
69
142
|
*/
|
|
70
143
|
allowFileAccess?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* What this capture may do with the HTTP cache. Default `default`.
|
|
146
|
+
*
|
|
147
|
+
* It applies to the subresources as well as the document: a `reload` that
|
|
148
|
+
* refreshed the HTML and reused yesterday's stylesheet would be a confusing
|
|
149
|
+
* thing to have asked for.
|
|
150
|
+
*/
|
|
151
|
+
cache?: CacheMode;
|
|
152
|
+
/**
|
|
153
|
+
* Extra request headers, sent with the document and with the subresources
|
|
154
|
+
* that are same-origin with it.
|
|
155
|
+
*
|
|
156
|
+
* Same-origin is the whole rule and it is not configurable. A caller passing
|
|
157
|
+
* `Authorization` or `Cookie` means it for the site being photographed; a
|
|
158
|
+
* page that pulls a script from a CDN must not have the credential
|
|
159
|
+
* forwarded there.
|
|
160
|
+
*/
|
|
161
|
+
headers?: Record<string, string>;
|
|
71
162
|
}
|
|
72
163
|
|
|
73
164
|
export interface StartOptions {
|
|
74
165
|
/**
|
|
75
|
-
* Root of the HTTP disk cache. `null` disables caching entirely
|
|
76
|
-
*
|
|
77
|
-
*
|
|
166
|
+
* Root of the HTTP disk cache. `null` disables caching entirely.
|
|
167
|
+
*
|
|
168
|
+
* The default is a per-project directory under the system temporary
|
|
169
|
+
* directory -- see `cache.getDir()`. Caching is on by default because the
|
|
170
|
+
* alternative turned out to be worse: without it every capture of an
|
|
171
|
+
* `https:` URL pays for DNS, TLS and a round trip, which for a small page is
|
|
172
|
+
* most of the time the call takes and all of the time the caller did not
|
|
173
|
+
* expect to spend.
|
|
78
174
|
*/
|
|
79
175
|
cacheDir?: string|null;
|
|
176
|
+
/**
|
|
177
|
+
* Ceiling on the cache directory, in bytes. Default 256 MB.
|
|
178
|
+
*
|
|
179
|
+
* Zero is not "unlimited" -- it hands the decision to the backend, which
|
|
180
|
+
* sizes itself against the volume's free space. That was a reasonable
|
|
181
|
+
* default when every user of the cache had named a directory on purpose; for
|
|
182
|
+
* one that appears by default under `~/.shotium` because somebody imported a
|
|
183
|
+
* library, a number somebody chose is better than a number nobody did.
|
|
184
|
+
*/
|
|
185
|
+
cacheMaxBytes?: number;
|
|
80
186
|
/** Overrides the built-in user agent string. */
|
|
81
187
|
userAgent?: string;
|
|
82
188
|
/**
|
|
@@ -86,6 +192,36 @@ export interface StartOptions {
|
|
|
86
192
|
resourceDir?: string;
|
|
87
193
|
}
|
|
88
194
|
|
|
195
|
+
/** What `start()` reports about the engine it brought up. */
|
|
196
|
+
export interface StartResult {
|
|
197
|
+
/**
|
|
198
|
+
* Whether this lifecycle is started.
|
|
199
|
+
*
|
|
200
|
+
* A process has at most one engine, so `false` here does not mean there is
|
|
201
|
+
* nothing running -- it means this `Runtime` is stood down. `cacheDir` below
|
|
202
|
+
* is still answered from the engine, because a stood-down engine keeps its
|
|
203
|
+
* cache directory and reporting `null` would say the cache had gone away
|
|
204
|
+
* when what went away was the willingness to render.
|
|
205
|
+
*/
|
|
206
|
+
running: boolean;
|
|
207
|
+
/** The directory in use, or `null` when caching is off. */
|
|
208
|
+
cacheDir: string|null;
|
|
209
|
+
/**
|
|
210
|
+
* Whether that directory is actually being cached into.
|
|
211
|
+
*
|
|
212
|
+
* A directory that cannot be created or written to costs nothing visible:
|
|
213
|
+
* the engine renders exactly as well without a cache, only slower, and every
|
|
214
|
+
* capture pays for the network again for a reason nothing reports. `false`
|
|
215
|
+
* with a `cacheDir` set means the open failed; `false` with `cacheDir: null`
|
|
216
|
+
* means no cache was asked for.
|
|
217
|
+
*
|
|
218
|
+
* It is not about sharing. Several processes may use one directory and all
|
|
219
|
+
* of them cache -- the backend takes no cross-process lock -- so `true` in
|
|
220
|
+
* two processes at once is the ordinary answer.
|
|
221
|
+
*/
|
|
222
|
+
cacheActive: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
89
225
|
export interface DaemonOptions extends StartOptions {
|
|
90
226
|
/**
|
|
91
227
|
* Address the daemon by name instead of by configuration. Without it the
|
|
@@ -133,7 +269,16 @@ export interface DaemonStatus {
|
|
|
133
269
|
version: string;
|
|
134
270
|
}
|
|
135
271
|
|
|
136
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Options for `releaseMemory()`.
|
|
274
|
+
*
|
|
275
|
+
* Named for what it does rather than for `purge`, which it was called until
|
|
276
|
+
* 0.3. With `cache.clear()` in the API the old name reads as though it clears
|
|
277
|
+
* the cache, and it does not: it hands back blink's heap, skia's caches and
|
|
278
|
+
* PartitionAlloc's free lists, all of which the engine rebuilds on demand.
|
|
279
|
+
* Nothing on disk is touched.
|
|
280
|
+
*/
|
|
281
|
+
export interface ReleaseMemoryOptions {
|
|
137
282
|
/**
|
|
138
283
|
* Also ask the OS to take the engine's pages back. The next screenshot pays
|
|
139
284
|
* them back in soft page faults -- a few milliseconds -- so this is for when
|
|
@@ -141,3 +286,68 @@ export interface PurgeOptions {
|
|
|
141
286
|
*/
|
|
142
287
|
releaseWorkingSet?: boolean;
|
|
143
288
|
}
|
|
289
|
+
|
|
290
|
+
/** Which cache directory an operation is about. */
|
|
291
|
+
export interface CacheTarget {
|
|
292
|
+
/**
|
|
293
|
+
* `current` (the default) is this project's directory, `all` is every
|
|
294
|
+
* directory shotium has created under the shared root -- `~/.shotium/cache`
|
|
295
|
+
* -- and a string is either an absolute path or one project hash as
|
|
296
|
+
* `getDir()` reports it.
|
|
297
|
+
*
|
|
298
|
+
* The absolute path is there because `start({cacheDir})` accepts any
|
|
299
|
+
* directory: without it, a caller who chose their own cache would have the
|
|
300
|
+
* one cache these methods could not see.
|
|
301
|
+
*
|
|
302
|
+
* `all` exists because the directories are per-project by default, so
|
|
303
|
+
* "clear shotium's caches" is otherwise something a caller cannot express
|
|
304
|
+
* without already knowing where the other projects were.
|
|
305
|
+
*/
|
|
306
|
+
target?: 'current'|'all'|(string&{});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** One resource the cache is holding. */
|
|
310
|
+
export interface CacheEntry {
|
|
311
|
+
/** The resource, not the backend's key -- see `cache.getFiles()`. */
|
|
312
|
+
url: string;
|
|
313
|
+
/** Milliseconds since the Unix epoch. */
|
|
314
|
+
lastUsedMs: number;
|
|
315
|
+
bytes: number;
|
|
316
|
+
/** Which cache directory it was found in. */
|
|
317
|
+
dir: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface CacheClearOptions extends CacheTarget {
|
|
321
|
+
/**
|
|
322
|
+
* Glob patterns matched against entry URLs -- not against filenames, which
|
|
323
|
+
* are hashes and would match nothing anybody would think to write.
|
|
324
|
+
*
|
|
325
|
+
* Supports `*` (within a path segment), `**` (across segments), `?` and
|
|
326
|
+
* `{a,b}`. Matching happens here rather than in the engine: the entries come
|
|
327
|
+
* back first, the patterns are applied to their URLs, and the ones that
|
|
328
|
+
* matched are what gets removed.
|
|
329
|
+
*/
|
|
330
|
+
glob?: string[];
|
|
331
|
+
/**
|
|
332
|
+
* Remove entries not used for this many seconds. `0`, the default, means no
|
|
333
|
+
* age limit.
|
|
334
|
+
*/
|
|
335
|
+
maxAge?: number;
|
|
336
|
+
/**
|
|
337
|
+
* Evict least-recently-used entries until the directory is at or below this
|
|
338
|
+
* many bytes. `0`, the default, means no size limit.
|
|
339
|
+
*/
|
|
340
|
+
maxSize?: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface CacheClearResult {
|
|
344
|
+
/**
|
|
345
|
+
* How many entries went. `-1` when the whole directory was dropped in one
|
|
346
|
+
* operation, which the backend does without counting them.
|
|
347
|
+
*/
|
|
348
|
+
removed: number;
|
|
349
|
+
bytesBefore: number;
|
|
350
|
+
bytesAfter: number;
|
|
351
|
+
/** Which directory this result is for. */
|
|
352
|
+
dir: string;
|
|
353
|
+
}
|
package/dist/engine-Xe7nH-1i.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import crypto from "node:crypto";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
|
|
8
|
-
//#region src/lib/config.ts
|
|
9
|
-
function resolveStartOptions(options = {}) {
|
|
10
|
-
return {
|
|
11
|
-
cacheDir: options.cacheDir ?? null,
|
|
12
|
-
userAgent: options.userAgent,
|
|
13
|
-
resourceDir: options.resourceDir
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
//#endregion
|
|
18
|
-
//#region src/lib/endpoint.ts
|
|
19
|
-
function endpointKey(options) {
|
|
20
|
-
if (options.name) return String(options.name);
|
|
21
|
-
const identity = JSON.stringify([
|
|
22
|
-
options.cacheDir === null || options.cacheDir === void 0 ? null : path.resolve(options.cacheDir),
|
|
23
|
-
options.userAgent ?? null,
|
|
24
|
-
options.resourceDir ? path.resolve(options.resourceDir) : null
|
|
25
|
-
]);
|
|
26
|
-
return crypto.createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
27
|
-
}
|
|
28
|
-
function endpointFor(options = {}) {
|
|
29
|
-
if (options.endpoint) return options.endpoint;
|
|
30
|
-
if (process.env.SHOTIUM_ENDPOINT) return process.env.SHOTIUM_ENDPOINT;
|
|
31
|
-
const key = endpointKey(options);
|
|
32
|
-
if (process.platform === "win32") return `\\\\.\\pipe\\shotium-${key}`;
|
|
33
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
34
|
-
return path.join(os.tmpdir(), `shotium-${uid}-${key}.sock`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/lib/protocol.ts
|
|
39
|
-
const HEADER_BYTES = 4;
|
|
40
|
-
function encodeFrame(payload) {
|
|
41
|
-
const header = Buffer.allocUnsafe(4);
|
|
42
|
-
header.writeUInt32LE(payload.length, 0);
|
|
43
|
-
return Buffer.concat([header, payload]);
|
|
44
|
-
}
|
|
45
|
-
var FrameReader = class {
|
|
46
|
-
buffer = Buffer.alloc(0);
|
|
47
|
-
push(chunk) {
|
|
48
|
-
this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
49
|
-
}
|
|
50
|
-
next() {
|
|
51
|
-
if (this.buffer.length < 4) return null;
|
|
52
|
-
const length = this.buffer.readUInt32LE(0);
|
|
53
|
-
if (this.buffer.length < 4 + length) return null;
|
|
54
|
-
const frame = this.buffer.subarray(4, 4 + length);
|
|
55
|
-
this.buffer = this.buffer.subarray(4 + length);
|
|
56
|
-
return frame;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/lib/request.ts
|
|
62
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
63
|
-
const WIRE_FIELDS = /* @__PURE__ */ new Set([
|
|
64
|
-
"file",
|
|
65
|
-
"type",
|
|
66
|
-
"fullPage",
|
|
67
|
-
"selector",
|
|
68
|
-
"quality",
|
|
69
|
-
"scale",
|
|
70
|
-
"omitBackground",
|
|
71
|
-
"path",
|
|
72
|
-
"pageGotoParams",
|
|
73
|
-
"clip",
|
|
74
|
-
"viewport",
|
|
75
|
-
"allowFileAccess"
|
|
76
|
-
]);
|
|
77
|
-
function toRequest(options) {
|
|
78
|
-
if (!options || typeof options !== "object") throw new TypeError("shotium: screenshot(options) needs an object");
|
|
79
|
-
if (typeof options.file !== "string" || options.file.length === 0) throw new TypeError("shotium: options.file is required");
|
|
80
|
-
const request = {};
|
|
81
|
-
for (const [key, value] of Object.entries(options)) {
|
|
82
|
-
if (value === void 0) continue;
|
|
83
|
-
if (!WIRE_FIELDS.has(key)) throw new TypeError(`shotium: unknown option "${key}"`);
|
|
84
|
-
request[key] = value;
|
|
85
|
-
}
|
|
86
|
-
if (request.viewport) {
|
|
87
|
-
const { width, height } = request.viewport;
|
|
88
|
-
delete request.viewport;
|
|
89
|
-
if (width !== void 0) request.width = width;
|
|
90
|
-
if (height !== void 0) request.height = height;
|
|
91
|
-
}
|
|
92
|
-
return request;
|
|
93
|
-
}
|
|
94
|
-
function timeoutFor(options) {
|
|
95
|
-
const timeout = options.pageGotoParams && options.pageGotoParams.timeout;
|
|
96
|
-
return typeof timeout === "number" ? timeout : DEFAULT_TIMEOUT_MS;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
//#endregion
|
|
100
|
-
//#region src/lib/platform.ts
|
|
101
|
-
const require$1 = createRequire(import.meta.url);
|
|
102
|
-
const PACKAGES = {
|
|
103
|
-
"win32-x64": "@shotkit/shotium-win32-x64",
|
|
104
|
-
"win32-arm64": "@shotkit/shotium-win32-arm64",
|
|
105
|
-
"darwin-x64": "@shotkit/shotium-darwin-x64",
|
|
106
|
-
"darwin-arm64": "@shotkit/shotium-darwin-arm64",
|
|
107
|
-
"linux-x64": "@shotkit/shotium-linux-x64",
|
|
108
|
-
"linux-arm64": "@shotkit/shotium-linux-arm64"
|
|
109
|
-
};
|
|
110
|
-
function packageName(platform = process.platform, arch = process.arch) {
|
|
111
|
-
return PACKAGES[`${platform}-${arch}`] ?? null;
|
|
112
|
-
}
|
|
113
|
-
function packageDir() {
|
|
114
|
-
const name = packageName();
|
|
115
|
-
if (!name) return null;
|
|
116
|
-
try {
|
|
117
|
-
return path.dirname(require$1.resolve(`${name}/package.json`));
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
//#endregion
|
|
124
|
-
//#region src/lib/binding.ts
|
|
125
|
-
const require = createRequire(import.meta.url);
|
|
126
|
-
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
127
|
-
function candidates() {
|
|
128
|
-
const found = [];
|
|
129
|
-
const dir = packageDir();
|
|
130
|
-
if (dir) found.push(path.join(dir, "shotium.node"));
|
|
131
|
-
found.push(path.join(HERE, "..", "native", "build", "Release", "shotium.node"));
|
|
132
|
-
return found;
|
|
133
|
-
}
|
|
134
|
-
let binding = null;
|
|
135
|
-
let loadedFrom = null;
|
|
136
|
-
/**
|
|
137
|
-
* The addon, loaded once. Throws if there is none for this platform, which is
|
|
138
|
-
* the only failure this package cannot work around: there is nothing else to
|
|
139
|
-
* fall back to.
|
|
140
|
-
*/
|
|
141
|
-
function load() {
|
|
142
|
-
if (binding) return binding;
|
|
143
|
-
const tried = candidates();
|
|
144
|
-
for (const candidate of tried) {
|
|
145
|
-
if (!fs.existsSync(candidate)) continue;
|
|
146
|
-
binding = require(candidate);
|
|
147
|
-
loadedFrom = path.dirname(candidate);
|
|
148
|
-
return binding;
|
|
149
|
-
}
|
|
150
|
-
const expected = packageName();
|
|
151
|
-
throw new Error(`shotium: no engine for this platform.
|
|
152
|
-
looked in:\n ${tried.join("\n ")}\n` + (expected ? ` It ships in ${expected}, which npm installs as an optional dependency of this package. If the install skipped optional dependencies, it is not there.
|
|
153
|
-
` : ` There is no build for ${process.platform}-${process.arch}.\n`));
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* The directory the addon came from, or null before the first load(). The
|
|
157
|
-
* resource packs ship beside it, which is what this is for.
|
|
158
|
-
*/
|
|
159
|
-
function directory() {
|
|
160
|
-
return loadedFrom;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
//#endregion
|
|
164
|
-
//#region src/lib/engine.ts
|
|
165
|
-
let startedInThisProcess = false;
|
|
166
|
-
/**
|
|
167
|
-
* Blink, in this process, and the queue in front of it.
|
|
168
|
-
*
|
|
169
|
-
* There is one renderer and there is no way to have two. Blink is a
|
|
170
|
-
* process-wide singleton: it is initialised once, there is no path to a second
|
|
171
|
-
* one, and `worker_threads` do not change that because they share the process.
|
|
172
|
-
* So captures are serialised however many callers there are, and a program
|
|
173
|
-
* that wants four at once wants four processes.
|
|
174
|
-
*
|
|
175
|
-
* The queue is not about fairness. Each capture occupies a libuv thread pool
|
|
176
|
-
* thread for as long as the render takes, and there are four of those by
|
|
177
|
-
* default, shared with fs and dns -- so letting four screenshots go at once
|
|
178
|
-
* would stall the host's file reads for a fifth of a second at a time while
|
|
179
|
-
* gaining nothing, since the engine serialises them anyway.
|
|
180
|
-
*/
|
|
181
|
-
var Engine = class {
|
|
182
|
-
handle = null;
|
|
183
|
-
stopped = false;
|
|
184
|
-
tail = Promise.resolve();
|
|
185
|
-
get running() {
|
|
186
|
-
return this.handle !== null;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Starts the engine. Safe to call twice; the second call is a no-op, so that
|
|
190
|
-
* library code can call it defensively.
|
|
191
|
-
*
|
|
192
|
-
* Not safe to call after `stop()`, and not because of anything here: Blink
|
|
193
|
-
* starts once per process and cannot be restarted. Another engine means
|
|
194
|
-
* another process.
|
|
195
|
-
*/
|
|
196
|
-
start(options = {}) {
|
|
197
|
-
if (this.handle) return this;
|
|
198
|
-
if (this.stopped) throw new Error("shotium: this engine was stopped, and Blink cannot be started again in a process that has already run it. Start another process, or keep the engine up between screenshots.");
|
|
199
|
-
if (startedInThisProcess) throw new Error("shotium: an engine has already run in this process. Blink is a process-wide singleton -- there is one per process, ever -- so a second Runtime cannot have one. Use the shared `runtime`, or run another process.");
|
|
200
|
-
const native = load();
|
|
201
|
-
const resolved = resolveStartOptions(options);
|
|
202
|
-
const engineOptions = {};
|
|
203
|
-
if (resolved.cacheDir !== null) engineOptions.cacheDir = resolved.cacheDir;
|
|
204
|
-
if (resolved.userAgent !== void 0) engineOptions.userAgent = resolved.userAgent;
|
|
205
|
-
engineOptions.resourceDir = resolved.resourceDir ?? directory();
|
|
206
|
-
this.handle = native.create(JSON.stringify(engineOptions));
|
|
207
|
-
startedInThisProcess = true;
|
|
208
|
-
return this;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Stops the engine, after whatever is queued.
|
|
212
|
-
*
|
|
213
|
-
* Final for this process: see the note above. A program that will want
|
|
214
|
-
* another screenshot later should leave the engine up and call `purge()`
|
|
215
|
-
* instead, which hands back the memory without giving up the engine.
|
|
216
|
-
*/
|
|
217
|
-
async stop() {
|
|
218
|
-
if (!this.handle) return;
|
|
219
|
-
this.stopped = true;
|
|
220
|
-
const handle = this.handle;
|
|
221
|
-
this.handle = null;
|
|
222
|
-
await this.tail.catch(() => {});
|
|
223
|
-
load().destroy(handle);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Hands back what the engine is holding but can rebuild.
|
|
227
|
-
* `releaseWorkingSet` additionally asks the OS for the pages, which the next
|
|
228
|
-
* screenshot pays back in soft faults -- worth it when there may not be a
|
|
229
|
-
* next one soon.
|
|
230
|
-
*
|
|
231
|
-
* The daemon does this for itself on a timer because it can watch its own
|
|
232
|
-
* request stream go quiet. Here the queue belongs to the caller, so the
|
|
233
|
-
* caller is the one who knows a batch has ended.
|
|
234
|
-
*/
|
|
235
|
-
purge({ releaseWorkingSet = false } = {}) {
|
|
236
|
-
if (!this.handle) return;
|
|
237
|
-
load().purge(this.handle, releaseWorkingSet);
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
241
|
-
* `path` was given and the engine wrote the file itself.
|
|
242
|
-
*/
|
|
243
|
-
async screenshot(options) {
|
|
244
|
-
return this.capture(toRequest(options));
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* The same, for a request that is already in wire form.
|
|
248
|
-
*
|
|
249
|
-
* The daemon reads these off a socket, where they arrived having been
|
|
250
|
-
* validated by the client that sent them. Re-deriving one from
|
|
251
|
-
* ScreenshotOptions would mean the daemon validating a request it cannot see
|
|
252
|
-
* the original of, and rejecting fields a newer client legitimately sent.
|
|
253
|
-
*/
|
|
254
|
-
async capture(request) {
|
|
255
|
-
if (!this.handle) this.start();
|
|
256
|
-
const handle = this.handle;
|
|
257
|
-
const native = load();
|
|
258
|
-
const result = this.tail.catch(() => {}).then(() => native.capture(handle, JSON.stringify(request)));
|
|
259
|
-
this.tail = result.catch(() => {});
|
|
260
|
-
const image = await result;
|
|
261
|
-
return request.path ? null : image;
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
//#endregion
|
|
266
|
-
export { encodeFrame as a, FrameReader as i, timeoutFor as n, endpointFor as o, toRequest as r, resolveStartOptions as s, Engine as t };
|
|
267
|
-
//# sourceMappingURL=engine-Xe7nH-1i.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"engine-Xe7nH-1i.js","names":["require","platformPackage.packageDir","platformPackage.packageName","binding.load","binding.directory"],"sources":["../src/lib/config.ts","../src/lib/endpoint.ts","../src/lib/protocol.ts","../src/lib/request.ts","../src/lib/platform.ts","../src/lib/binding.ts","../src/lib/engine.ts"],"sourcesContent":["import type {StartOptions} from '../types.js';\n\n// StartOptions with every hole filled in. `cacheDir` is still nullable here\n// because null is an answer -- \"no disk cache\" -- and not an absent one.\nexport interface ResolvedStartOptions {\n cacheDir: string|null;\n userAgent?: string;\n resourceDir?: string;\n}\n\n// The one place that decides what \"no options\" means.\n//\n// It is shared rather than duplicated because the daemon's address is a hash of\n// its configuration: if two callers filled in defaults even slightly\n// differently, one would compute an address no daemon is listening on and\n// start a second engine next to the first one that was already warm. See\n// endpoint.ts.\n//\n// The default for `cacheDir` is null -- no disk cache. A program holding the\n// engine is often short-lived, and a cache it never reads twice is a directory\n// it leaves behind. The daemon, which is the case where a cache does pay for\n// itself, is also the case where the caller is already passing options.\nfunction resolveStartOptions(options: StartOptions = {}): ResolvedStartOptions {\n return {\n cacheDir: options.cacheDir ?? null,\n userAgent: options.userAgent,\n resourceDir: options.resourceDir,\n };\n}\n\nexport {resolveStartOptions};\n","import crypto from 'node:crypto';\nimport os from 'node:os';\nimport path from 'node:path';\n\n// What endpointFor() needs to know: a resolved configuration, plus the two\n// ways of overriding the address it would derive from one.\nexport interface EndpointOptions {\n cacheDir?: string|null;\n userAgent?: string;\n resourceDir?: string;\n name?: string;\n endpoint?: string;\n}\n\n// Where a daemon listens, derived from what it was asked to be.\n//\n// The address is a hash of the configuration -- cache root, user agent,\n// resource directory -- rather than a fixed name, because attaching to\n// whatever daemon happens to be up would mean rendering with someone else's\n// settings. Two configurations are two daemons; the same configuration, from\n// any process, is one.\n//\n// Every field of EndpointOptions is optional, so nothing here fails to compile\n// when a field is dropped from the configuration -- it just stops being part\n// of the identity, and every caller collapses onto one address. That happened\n// once, when the worker pool went away and this was left hashing three fields\n// that no longer existed. If a field is added to StartOptions and it changes\n// what the engine renders, it belongs in the array below.\n//\n// A caller who wants a daemon by name instead of by configuration passes\n// `name`, which replaces the hash. That is the escape hatch for a service that\n// starts its daemon deliberately and wants clients to find it without\n// repeating the configuration.\nfunction endpointKey(options: EndpointOptions): string {\n if (options.name) {\n return String(options.name);\n }\n const identity = JSON.stringify([\n options.cacheDir === null || options.cacheDir === undefined ?\n null :\n path.resolve(options.cacheDir),\n options.userAgent ?? null,\n options.resourceDir ? path.resolve(options.resourceDir) : null,\n ]);\n return crypto.createHash('sha256').update(identity).digest('hex').slice(0, 16);\n}\n\n// Windows has named pipes and no filesystem sockets; POSIX has the reverse.\n// Both are net.connect() addresses, which is the only reason the rest of the\n// daemon can ignore the difference.\n//\n// The pipe namespace is per-machine but the socket path is per-user, so the\n// uid goes in the POSIX name to keep two users on one host from colliding on a\n// path only one of them can open.\nfunction endpointFor(options: EndpointOptions = {}): string {\n if (options.endpoint) {\n return options.endpoint;\n }\n if (process.env.SHOTIUM_ENDPOINT) {\n return process.env.SHOTIUM_ENDPOINT;\n }\n const key = endpointKey(options);\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\shotium-${key}`;\n }\n const uid = typeof process.getuid === 'function' ? process.getuid() : 0;\n return path.join(os.tmpdir(), `shotium-${uid}-${key}.sock`);\n}\n\nexport {endpointFor, endpointKey};\n","// The wire format shotium.exe --serve speaks, in both directions: a 4-byte\n// little-endian length followed by that many bytes.\n//\n// Length-prefixed rather than line-delimited because the payload is binary and\n// a newline inside a PNG is not a message boundary. See shot/shot_server.h for\n// the same description from the other end.\n//\n// -> [len][{\"file\":\"...\",\"width\":1248,...}]\n// <- [len][{\"ok\":true,\"bytes\":97756}] [len][<PNG bytes>]\n// <- [len][{\"ok\":false,\"error\":\"...\"}] [0]\n\nconst HEADER_BYTES = 4;\n\nfunction encodeFrame(payload: Buffer): Buffer {\n const header = Buffer.allocUnsafe(HEADER_BYTES);\n header.writeUInt32LE(payload.length, 0);\n return Buffer.concat([header, payload]);\n}\n\nfunction encodeRequest(request: unknown): Buffer {\n return encodeFrame(Buffer.from(JSON.stringify(request), 'utf8'));\n}\n\n// Reassembles frames out of whatever sizes the pipe hands over.\n//\n// A stream is not a sequence of messages: one read can carry half a header, or\n// three responses and the start of a fourth. Everything downstream assumes\n// whole frames, so this is the only place that has to know that.\nclass FrameReader {\n private buffer: Buffer = Buffer.alloc(0);\n\n push(chunk: Buffer): void {\n this.buffer = this.buffer.length === 0 ?\n chunk :\n Buffer.concat([this.buffer, chunk]);\n }\n\n // The next complete frame, or null when there is not one yet.\n next(): Buffer|null {\n if (this.buffer.length < HEADER_BYTES) {\n return null;\n }\n const length = this.buffer.readUInt32LE(0);\n if (this.buffer.length < HEADER_BYTES + length) {\n return null;\n }\n const frame = this.buffer.subarray(HEADER_BYTES, HEADER_BYTES + length);\n this.buffer = this.buffer.subarray(HEADER_BYTES + length);\n return frame;\n }\n}\n\nexport {HEADER_BYTES, encodeFrame, encodeRequest, FrameReader};\n","import type {Clip, PageGotoParams, ScreenshotOptions} from '../types.js';\n\nconst DEFAULT_TIMEOUT_MS = 30000;\n\n// What actually goes down the pipe: ScreenshotOptions with the viewport\n// flattened -- see toRequest below for why.\nexport interface WireRequest {\n file: string;\n type?: 'png'|'jpeg'|'webp';\n fullPage?: boolean;\n selector?: string;\n quality?: number;\n scale?: number;\n omitBackground?: boolean;\n path?: string;\n pageGotoParams?: PageGotoParams;\n clip?: Clip;\n allowFileAccess?: boolean;\n width?: number;\n height?: number;\n}\n\n// Everything the worker understands, and nothing else. An unknown field is a\n// typo, and a typo that is silently dropped is a screenshot that quietly\n// ignored what was asked for -- so this rejects rather than filters.\n//\n// It is a runtime check even though the argument has a type, because the\n// argument having a type says nothing about a caller who is not compiled\n// against it: a JavaScript program, or a JSON body from somewhere else.\nconst WIRE_FIELDS = new Set([\n 'file',\n 'type',\n 'fullPage',\n 'selector',\n 'quality',\n 'scale',\n 'omitBackground',\n 'path',\n 'pageGotoParams',\n 'clip',\n 'viewport',\n 'allowFileAccess',\n]);\n\n// One ScreenshotOptions, checked and flattened into what goes on the wire.\n//\n// It lives here rather than in index.ts because the engine in this process and\n// the daemon both send it: a request that is valid through one entry point and\n// rejected through the other would be a difference nobody asked for.\nfunction toRequest(options: ScreenshotOptions): WireRequest {\n if (!options || typeof options !== 'object') {\n throw new TypeError('shotium: screenshot(options) needs an object');\n }\n if (typeof options.file !== 'string' || options.file.length === 0) {\n throw new TypeError('shotium: options.file is required');\n }\n\n const request: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(options)) {\n if (value === undefined) {\n continue;\n }\n if (!WIRE_FIELDS.has(key)) {\n throw new TypeError(`shotium: unknown option \"${key}\"`);\n }\n request[key] = value;\n }\n\n // The viewport is flattened because the worker takes width and height at the\n // top level: it is one screenshot's frame, not a nested object on the wire.\n if (request.viewport) {\n const {width, height} = request.viewport as {\n width?: number,\n height?: number,\n };\n delete request.viewport;\n if (width !== undefined) {\n request.width = width;\n }\n if (height !== undefined) {\n request.height = height;\n }\n }\n return request as unknown as WireRequest;\n}\n\nfunction timeoutFor(options: ScreenshotOptions): number {\n const timeout = options.pageGotoParams && options.pageGotoParams.timeout;\n return typeof timeout === 'number' ? timeout : DEFAULT_TIMEOUT_MS;\n}\n\nexport {\n DEFAULT_TIMEOUT_MS,\n WIRE_FIELDS,\n timeoutFor,\n toRequest,\n};\n","import {createRequire} from 'node:module';\nimport path from 'node:path';\n\n// require.resolve is the resolver, and ESM has no synchronous equivalent\n// that answers for a package that may not be installed at all.\nconst require = createRequire(import.meta.url);\n\n// Which package carries the engine for this machine.\n//\n// The engine is not in this package and cannot be: it is a Chromium build,\n// 41 MB per platform and architecture, six of them, and `npm install` is never\n// going to produce one. So the bytes live in six packages of their own and\n// this one depends on all six as optionalDependencies with `os` and `cpu` set,\n// which is npm's way of saying \"install the one that matches this machine and\n// skip the other five\". A machine nobody builds for installs none of them and\n// still gets a working package -- it just has to be pointed at an engine.\n//\n// The alternative, a postinstall script that downloads a tarball, was not\n// chosen. It defeats a lockfile, which is supposed to pin what you get; it\n// fails behind a registry mirror, which is the one place a large dependency\n// most needs to work; and it runs code at install time in exchange for saving\n// nothing that npm was not already doing.\n//\n// Key and name are both `${process.platform}-${process.arch}`, so the table is\n// the identity map with a prefix on it. That is deliberate: the value npm\n// matches `os` and `cpu` against is process.platform, and a package named for\n// anything else makes the reader hold two spellings of one machine in their\n// head. It is also what every other package of this shape does -- esbuild,\n// swc, lightningcss all publish darwin-arm64 and win32-x64.\n//\n// The release archives spell it win/mac instead -- shotium-mac-arm64.7z --\n// and that is not going to change either. They are downloaded by people, and\n// `mac` is what people call it. So the two spellings do differ, in the one\n// place where each is right: the registry gets node's, the download page gets\n// the reader's.\nconst PACKAGES: Readonly<Record<string, string>> = {\n 'win32-x64': '@shotkit/shotium-win32-x64',\n 'win32-arm64': '@shotkit/shotium-win32-arm64',\n 'darwin-x64': '@shotkit/shotium-darwin-x64',\n 'darwin-arm64': '@shotkit/shotium-darwin-arm64',\n 'linux-x64': '@shotkit/shotium-linux-x64',\n 'linux-arm64': '@shotkit/shotium-linux-arm64',\n};\n\nfunction packageName(\n platform: string = process.platform,\n arch: string = process.arch): string|null {\n return PACKAGES[`${platform}-${arch}`] ?? null;\n}\n\n// Where the matching platform package unpacked, or null if it is not installed.\n//\n// require.resolve rather than a path built from the module's own location: the\n// package can be hoisted to a workspace root, nested under this one, or left\n// in a pnpm store with a symlink pointing at it, and the resolver is the only\n// thing that knows which of those happened.\nfunction packageDir(): string|null {\n const name = packageName();\n if (!name) {\n return null;\n }\n try {\n return path.dirname(require.resolve(`${name}/package.json`));\n } catch {\n return null;\n }\n}\n\nexport {PACKAGES, packageDir, packageName};\n","import fs from 'node:fs';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport * as platformPackage from './platform.js';\n\n// A .node addon is a CommonJS artefact: there is no ESM loader for one.\nconst require = createRequire(import.meta.url);\n\n// ESM has no __dirname. This is the same thing, from the module's own URL.\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * The engine handle the addon hands back. Opaque on purpose: everything that\n * can be done with it is a call on the binding below.\n */\nexport type Engine = unknown;\n\n/** What native/binding.cc exports. See shot/shot_api.h for the C ABI. */\nexport interface NativeBinding {\n create(optionsJson: string): Engine;\n destroy(engine: Engine): void;\n purge(engine: Engine, releaseWorkingSet: boolean): void;\n capture(engine: Engine, requestJson: string): Promise<Buffer>;\n}\n\n// Where the addon and the library beside it live.\n//\n// The platform package is what ships -- the .node sits next to the shared\n// library it is linked against, which is the whole reason the two travel in\n// one package rather than two. native/build/Release is where node-gyp puts a\n// local build; it exists in a checkout and not in an install, so the two never\n// compete in practice. Both paths are relative to this file's build output,\n// which is one directory below the package root.\nfunction candidates(): string[] {\n const found: string[] = [];\n const dir = platformPackage.packageDir();\n if (dir) {\n found.push(path.join(dir, 'shotium.node'));\n }\n found.push(\n path.join(HERE, '..', 'native', 'build', 'Release', 'shotium.node'));\n return found;\n}\n\nlet binding: NativeBinding|null = null;\nlet loadedFrom: string|null = null;\n\n/**\n * The addon, loaded once. Throws if there is none for this platform, which is\n * the only failure this package cannot work around: there is nothing else to\n * fall back to.\n */\nexport function load(): NativeBinding {\n if (binding) {\n return binding;\n }\n const tried = candidates();\n for (const candidate of tried) {\n if (!fs.existsSync(candidate)) {\n continue;\n }\n // Not wrapped in a try: a .node that is there and will not load is a\n // broken installation, and the loader's own message -- a missing\n // dependency, an architecture mismatch -- says more than anything that\n // could be substituted for it.\n binding = require(candidate) as NativeBinding;\n loadedFrom = path.dirname(candidate);\n return binding;\n }\n const expected = platformPackage.packageName();\n throw new Error(\n 'shotium: no engine for this platform.\\n' +\n ` looked in:\\n ${tried.join('\\n ')}\\n` +\n (expected ?\n ` It ships in ${expected}, which npm installs as an optional ` +\n 'dependency of this package. If the install skipped optional ' +\n 'dependencies, it is not there.\\n' :\n ` There is no build for ${process.platform}-${process.arch}.\\n`));\n}\n\n/**\n * The directory the addon came from, or null before the first load(). The\n * resource packs ship beside it, which is what this is for.\n */\nexport function directory(): string|null {\n return loadedFrom;\n}\n","import * as binding from './binding.js';\nimport type {Engine as Handle} from './binding.js';\nimport {toRequest} from './request.js';\nimport type {WireRequest} from './request.js';\nimport type {PurgeOptions, ScreenshotOptions, StartOptions} from '../types.js';\n\nimport {resolveStartOptions} from './config.js';\n\n// One per process, ever. Not one at a time -- one.\n//\n// This is not a rule of this file, it is what Blink is: initialising it writes\n// process-wide statics it has no path to undo, so shot_engine_destroy() gives\n// back what it can and the process still cannot make another. The C API\n// returns SHOT_ERR_STATE for a second create whether or not the first is\n// still alive. See shot/shot_api.h.\n//\n// So `stop()` is final for the process, and this flag exists to say that in\n// words at the call site. Without it a caller who stops and starts again gets\n// SHOT_ERR_STATE out of the addon -- a true error, arriving one layer too deep\n// to explain that the answer is a second process rather than a retry.\nlet startedInThisProcess = false;\n\n/**\n * Blink, in this process, and the queue in front of it.\n *\n * There is one renderer and there is no way to have two. Blink is a\n * process-wide singleton: it is initialised once, there is no path to a second\n * one, and `worker_threads` do not change that because they share the process.\n * So captures are serialised however many callers there are, and a program\n * that wants four at once wants four processes.\n *\n * The queue is not about fairness. Each capture occupies a libuv thread pool\n * thread for as long as the render takes, and there are four of those by\n * default, shared with fs and dns -- so letting four screenshots go at once\n * would stall the host's file reads for a fifth of a second at a time while\n * gaining nothing, since the engine serialises them anyway.\n */\nexport class Engine {\n private handle: Handle|null = null;\n private stopped = false;\n private tail: Promise<unknown> = Promise.resolve();\n\n get running(): boolean {\n return this.handle !== null;\n }\n\n /**\n * Starts the engine. Safe to call twice; the second call is a no-op, so that\n * library code can call it defensively.\n *\n * Not safe to call after `stop()`, and not because of anything here: Blink\n * starts once per process and cannot be restarted. Another engine means\n * another process.\n */\n start(options: StartOptions = {}): this {\n if (this.handle) {\n return this;\n }\n if (this.stopped) {\n throw new Error(\n 'shotium: this engine was stopped, and Blink cannot be started ' +\n 'again in a process that has already run it. Start another ' +\n 'process, or keep the engine up between screenshots.');\n }\n if (startedInThisProcess) {\n throw new Error(\n 'shotium: an engine has already run in this process. Blink is a ' +\n 'process-wide singleton -- there is one per process, ever -- so a ' +\n 'second Runtime cannot have one. Use the shared `runtime`, or run ' +\n 'another process.');\n }\n const native = binding.load();\n const resolved = resolveStartOptions(options);\n\n const engineOptions: Record<string, unknown> = {};\n if (resolved.cacheDir !== null) {\n engineOptions.cacheDir = resolved.cacheDir;\n }\n if (resolved.userAgent !== undefined) {\n engineOptions.userAgent = resolved.userAgent;\n }\n // The packs sit beside the library, and the library cannot find itself on\n // Linux -- the path the engine resolves for \"this module\" goes through\n // /proc/self/exe, which names node. Saying it here is cheaper than\n // teaching the engine a second way to look. See shot_api.h.\n engineOptions.resourceDir = resolved.resourceDir ?? binding.directory();\n\n this.handle = native.create(JSON.stringify(engineOptions));\n startedInThisProcess = true;\n return this;\n }\n\n /**\n * Stops the engine, after whatever is queued.\n *\n * Final for this process: see the note above. A program that will want\n * another screenshot later should leave the engine up and call `purge()`\n * instead, which hands back the memory without giving up the engine.\n */\n async stop(): Promise<void> {\n if (!this.handle) {\n return;\n }\n this.stopped = true;\n // After the queue, not before: destroy() waits for a capture in flight\n // anyway, and doing it in order means a caller's last screenshot resolves\n // rather than racing the shutdown.\n const handle = this.handle;\n this.handle = null;\n await this.tail.catch(() => {});\n binding.load().destroy(handle);\n }\n\n /**\n * Hands back what the engine is holding but can rebuild.\n * `releaseWorkingSet` additionally asks the OS for the pages, which the next\n * screenshot pays back in soft faults -- worth it when there may not be a\n * next one soon.\n *\n * The daemon does this for itself on a timer because it can watch its own\n * request stream go quiet. Here the queue belongs to the caller, so the\n * caller is the one who knows a batch has ended.\n */\n purge({releaseWorkingSet = false}: PurgeOptions = {}): void {\n if (!this.handle) {\n return;\n }\n binding.load().purge(this.handle, releaseWorkingSet);\n }\n\n /**\n * Renders one screenshot. Resolves to the encoded image, or to `null` when\n * `path` was given and the engine wrote the file itself.\n */\n // `async` and not a plain function returning capture()'s promise: toRequest()\n // throws, and a caller who wrote `screenshot(bad).catch(...)` would get the\n // throw past the catch and into the surrounding frame. The whole surface is\n // promise-shaped, so a bad request is a rejection like everything else.\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n // Before anything else, and before the queue: a malformed request should\n // be a rejection now rather than one that waits its turn.\n return this.capture(toRequest(options));\n }\n\n /**\n * The same, for a request that is already in wire form.\n *\n * The daemon reads these off a socket, where they arrived having been\n * validated by the client that sent them. Re-deriving one from\n * ScreenshotOptions would mean the daemon validating a request it cannot see\n * the original of, and rejecting fields a newer client legitimately sent.\n */\n async capture(request: WireRequest): Promise<Buffer|null> {\n if (!this.handle) {\n this.start();\n }\n const handle = this.handle;\n const native = binding.load();\n\n // Chain onto the tail so that captures run one at a time. The catch keeps\n // one failure from poisoning everything queued behind it.\n const result = this.tail.catch(() => {}).then(\n () => native.capture(handle, JSON.stringify(request)));\n this.tail = result.catch(() => {});\n const image = await result;\n return request.path ? null : image;\n }\n}\n"],"mappings":"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"}
|