@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.
@@ -66,10 +66,4 @@ function packageDir(): string|null {
66
66
  }
67
67
  }
68
68
 
69
- // What the engine executable is called, which is not what the platform calls
70
- // it: Windows wants the extension and nothing else does.
71
- function binaryName(): string {
72
- return process.platform === 'win32' ? 'shotium.exe' : 'shotium';
73
- }
74
-
75
- export {PACKAGES, binaryName, packageDir, packageName};
69
+ export {PACKAGES, packageDir, packageName};
@@ -1,14 +1,14 @@
1
- import type {Clip, PageGotoParams, ScreenshotOptions} from '../types.js';
1
+ import type {
2
+ CacheMode,
3
+ Clip,
4
+ PageGotoParams,
5
+ ScreenshotOptions,
6
+ } from '../types.js';
2
7
 
3
8
  const DEFAULT_TIMEOUT_MS = 30000;
4
- // How much longer than the page's own deadline a supervisor waits before
5
- // deciding the worker is not going to answer at all. The worker fails a slow
6
- // page by itself and replies; this margin covers process startup and the
7
- // encode, and firing it means something worse than a slow page.
8
- const SUPERVISOR_MARGIN_MS = 10000;
9
9
 
10
- // What actually goes down the pipe. It is ScreenshotOptions with the viewport
11
- // flattened and `retry` taken out -- see toRequest below for why each.
10
+ // What actually goes down the pipe: ScreenshotOptions with the viewport
11
+ // flattened -- see toRequest below for why.
12
12
  export interface WireRequest {
13
13
  file: string;
14
14
  type?: 'png'|'jpeg'|'webp';
@@ -21,6 +21,8 @@ export interface WireRequest {
21
21
  pageGotoParams?: PageGotoParams;
22
22
  clip?: Clip;
23
23
  allowFileAccess?: boolean;
24
+ cache?: CacheMode;
25
+ headers?: Record<string, string>;
24
26
  width?: number;
25
27
  height?: number;
26
28
  }
@@ -45,12 +47,14 @@ const WIRE_FIELDS = new Set([
45
47
  'clip',
46
48
  'viewport',
47
49
  'allowFileAccess',
50
+ 'cache',
51
+ 'headers',
48
52
  ]);
49
53
 
50
54
  // One ScreenshotOptions, checked and flattened into what goes on the wire.
51
55
  //
52
- // It lives here rather than in index.ts because the in-process pool and the
53
- // daemon both send it: a request that is valid through one entry point and
56
+ // It lives here rather than in index.ts because the engine in this process and
57
+ // the daemon both send it: a request that is valid through one entry point and
54
58
  // rejected through the other would be a difference nobody asked for.
55
59
  function toRequest(options: ScreenshotOptions): WireRequest {
56
60
  if (!options || typeof options !== 'object') {
@@ -65,11 +69,6 @@ function toRequest(options: ScreenshotOptions): WireRequest {
65
69
  if (value === undefined) {
66
70
  continue;
67
71
  }
68
- // retry is the supervisor's, not the worker's: it decides how many times a
69
- // request is re-sent, which is not something the worker could act on.
70
- if (key === 'retry') {
71
- continue;
72
- }
73
72
  if (!WIRE_FIELDS.has(key)) {
74
73
  throw new TypeError(`shotium: unknown option "${key}"`);
75
74
  }
@@ -101,7 +100,6 @@ function timeoutFor(options: ScreenshotOptions): number {
101
100
 
102
101
  export {
103
102
  DEFAULT_TIMEOUT_MS,
104
- SUPERVISOR_MARGIN_MS,
105
103
  WIRE_FIELDS,
106
104
  timeoutFor,
107
105
  toRequest,
package/src/types.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  // The vocabulary of the package: what a caller passes in and what comes back.
2
2
  //
3
3
  // It lives in one file rather than beside the code that reads each field
4
- // because these types are the published API. index.ts and native.ts both
5
- // re-export them, so a consumer sees one `ScreenshotOptions` whichever entry
6
- // point they came through -- and, more to the point, so there is one place
7
- // where adding an option means adding it.
4
+ // because these types are the published API: index.ts re-exports them, so
5
+ // there is one place where adding an option means adding it.
8
6
 
9
7
  /** A region of the document, in CSS pixels. */
10
8
  export interface Clip {
@@ -35,6 +33,69 @@ export interface Viewport {
35
33
  height?: number;
36
34
  }
37
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
+ encode: number;
62
+ /** Wall clock for the whole capture, so the three above can be checked. */
63
+ total: number;
64
+ }
65
+
66
+ /** What one capture cost, and where its bytes came from. */
67
+ export interface CaptureStats {
68
+ /** Every resource the document asked for, itself included. */
69
+ requests: number;
70
+ /**
71
+ * Answered from the HTTP cache -- the body came from disk.
72
+ *
73
+ * Not the same as "no network was touched". A stale entry that can be
74
+ * revalidated costs a conditional request and a 304, and counts here too;
75
+ * what the cache saved is the download rather than the round trip. That is
76
+ * why `timing.fetch` can be tens of milliseconds with this set.
77
+ */
78
+ fromCache: number;
79
+ failed: number;
80
+ /** Decoded body bytes, summed -- not the transfer size. */
81
+ bytes: number;
82
+ /** The document's own status. 0 for a `file:` URL. */
83
+ httpStatus: number;
84
+ /** After redirects, which is what relative URLs resolved against. */
85
+ finalUrl: string;
86
+ timing: CaptureTiming;
87
+ }
88
+
89
+ /** One screenshot, and what taking it cost. */
90
+ export interface ScreenshotResult {
91
+ /**
92
+ * The encoded image, or `null` when `path` was given: the engine wrote the
93
+ * file itself and there is nothing left to hand back.
94
+ */
95
+ image: Buffer|null;
96
+ stats: CaptureStats;
97
+ }
98
+
38
99
  export interface ScreenshotOptions {
39
100
  /** An http/https/file URL, or a local path. */
40
101
  file: string;
@@ -70,35 +131,92 @@ export interface ScreenshotOptions {
70
131
  * is rendered on.
71
132
  */
72
133
  allowFileAccess?: boolean;
73
- /** How many times to re-send after a crash or a timeout. Default 0. */
74
- retry?: number;
134
+ /**
135
+ * What this capture may do with the HTTP cache. Default `default`.
136
+ *
137
+ * It applies to the subresources as well as the document: a `reload` that
138
+ * refreshed the HTML and reused yesterday's stylesheet would be a confusing
139
+ * thing to have asked for.
140
+ */
141
+ cache?: CacheMode;
142
+ /**
143
+ * Extra request headers, sent with the document and with the subresources
144
+ * that are same-origin with it.
145
+ *
146
+ * Same-origin is the whole rule and it is not configurable. A caller passing
147
+ * `Authorization` or `Cookie` means it for the site being photographed; a
148
+ * page that pulls a script from a CDN must not have the credential
149
+ * forwarded there.
150
+ */
151
+ headers?: Record<string, string>;
75
152
  }
76
153
 
77
154
  export interface StartOptions {
78
155
  /**
79
- * Path to `shotium.exe`. Default `$SHOTIUM_BINARY`, then the platform
80
- * package for this machine, then `./bin/shotium.exe`.
156
+ * Root of the HTTP disk cache. `null` disables caching entirely.
157
+ *
158
+ * The default is a per-project directory under the system temporary
159
+ * directory -- see `cache.getDir()`. Caching is on by default because the
160
+ * alternative turned out to be worse: without it every capture of an
161
+ * `https:` URL pays for DNS, TLS and a round trip, which for a small page is
162
+ * most of the time the call takes and all of the time the caller did not
163
+ * expect to spend.
81
164
  */
82
- binary?: string;
83
- /** Worker processes. Default half the cores, at least one, at most four. */
84
- workers?: number;
85
- /** Root of the per-worker HTTP disk caches. `null` disables caching. */
86
165
  cacheDir?: string|null;
87
- /** Extra flags passed to every worker. */
88
- args?: string[];
166
+ /**
167
+ * Ceiling on the cache directory, in bytes. Default 256 MB.
168
+ *
169
+ * Zero is not "unlimited" -- it hands the decision to the backend, which
170
+ * sizes itself against the volume's free space. That was a reasonable
171
+ * default when every user of the cache had named a directory on purpose; for
172
+ * one that appears by default under `~/.shotium` because somebody imported a
173
+ * library, a number somebody chose is better than a number nobody did.
174
+ */
175
+ cacheMaxBytes?: number;
176
+ /** Overrides the built-in user agent string. */
177
+ userAgent?: string;
178
+ /**
179
+ * Where `shotium_data.pak` and `shotium_strings.pak` are. Defaults to the
180
+ * directory the engine was loaded from, which is where they ship.
181
+ */
182
+ resourceDir?: string;
89
183
  }
90
184
 
91
- export interface WorkerEvent {
92
- worker: number;
93
- code?: number|null;
94
- signal?: NodeJS.Signals|null;
185
+ /** What `start()` reports about the engine it brought up. */
186
+ export interface StartResult {
187
+ /**
188
+ * Whether this lifecycle is started.
189
+ *
190
+ * A process has at most one engine, so `false` here does not mean there is
191
+ * nothing running -- it means this `Runtime` is stood down. `cacheDir` below
192
+ * is still answered from the engine, because a stood-down engine keeps its
193
+ * cache directory and reporting `null` would say the cache had gone away
194
+ * when what went away was the willingness to render.
195
+ */
196
+ running: boolean;
197
+ /** The directory in use, or `null` when caching is off. */
198
+ cacheDir: string|null;
199
+ /**
200
+ * Whether that directory is actually being cached into.
201
+ *
202
+ * A directory that cannot be created or written to costs nothing visible:
203
+ * the engine renders exactly as well without a cache, only slower, and every
204
+ * capture pays for the network again for a reason nothing reports. `false`
205
+ * with a `cacheDir` set means the open failed; `false` with `cacheDir: null`
206
+ * means no cache was asked for.
207
+ *
208
+ * It is not about sharing. Several processes may use one directory and all
209
+ * of them cache -- the backend takes no cross-process lock -- so `true` in
210
+ * two processes at once is the ordinary answer.
211
+ */
212
+ cacheActive: boolean;
95
213
  }
96
214
 
97
215
  export interface DaemonOptions extends StartOptions {
98
216
  /**
99
217
  * Address the daemon by name instead of by configuration. Without it the
100
- * endpoint is a hash of `binary`, `workers`, `cacheDir` and `args`, so a
101
- * client never attaches to a pool that renders with something other than
218
+ * endpoint is a hash of `cacheDir`, `userAgent` and `resourceDir`, so a
219
+ * client never attaches to a daemon that renders with something other than
102
220
  * what it asked for.
103
221
  */
104
222
  name?: string;
@@ -110,9 +228,8 @@ export interface DaemonOptions extends StartOptions {
110
228
  */
111
229
  idleTimeoutMs?: number;
112
230
  /**
113
- * Render one throwaway document per worker at startup, so the first real
114
- * request does not pay for whatever a worker initialises lazily. Default
115
- * true.
231
+ * Render one throwaway document at startup, so the first real request does
232
+ * not pay for whatever the engine initialises lazily. Default true.
116
233
  */
117
234
  prewarm?: boolean;
118
235
  /** Fail instead of starting a daemon when none is listening. */
@@ -129,11 +246,10 @@ export interface DaemonStatus {
129
246
  spawned?: boolean;
130
247
  pid: number;
131
248
  endpoint: string;
132
- binary: string;
133
- workers: number;
134
249
  cacheDir: string|null;
135
- args: string[];
136
- /** Every worker has rendered at least once. */
250
+ userAgent?: string;
251
+ resourceDir?: string;
252
+ /** The engine has rendered at least once. */
137
253
  warm: boolean;
138
254
  uptimeMs: number;
139
255
  connections: number;
@@ -143,27 +259,85 @@ export interface DaemonStatus {
143
259
  version: string;
144
260
  }
145
261
 
146
- export interface NativeStartOptions {
262
+ /**
263
+ * Options for `releaseMemory()`.
264
+ *
265
+ * Named for what it does rather than for `purge`, which it was called until
266
+ * 0.3. With `cache.clear()` in the API the old name reads as though it clears
267
+ * the cache, and it does not: it hands back blink's heap, skia's caches and
268
+ * PartitionAlloc's free lists, all of which the engine rebuilds on demand.
269
+ * Nothing on disk is touched.
270
+ */
271
+ export interface ReleaseMemoryOptions {
147
272
  /**
148
- * Root of the HTTP disk cache. `null` disables caching entirely, which is
149
- * the default here: an in-process engine is often a short-lived program, and
150
- * a cache it never reads twice is a directory it leaves behind.
273
+ * Also ask the OS to take the engine's pages back. The next screenshot pays
274
+ * them back in soft page faults -- a few milliseconds -- so this is for when
275
+ * there may not be a next one soon.
151
276
  */
152
- cacheDir?: string|null;
153
- /** Overrides the built-in user agent string. */
154
- userAgent?: string;
277
+ releaseWorkingSet?: boolean;
278
+ }
279
+
280
+ /** Which cache directory an operation is about. */
281
+ export interface CacheTarget {
155
282
  /**
156
- * Where `shotium_data.pak` and `shotium_strings.pak` are. Defaults to the
157
- * directory the native engine was loaded from, which is where they ship.
283
+ * `current` (the default) is this project's directory, `all` is every
284
+ * directory shotium has created under the shared root -- `~/.shotium/cache`
285
+ * -- and a string is either an absolute path or one project hash as
286
+ * `getDir()` reports it.
287
+ *
288
+ * The absolute path is there because `start({cacheDir})` accepts any
289
+ * directory: without it, a caller who chose their own cache would have the
290
+ * one cache these methods could not see.
291
+ *
292
+ * `all` exists because the directories are per-project by default, so
293
+ * "clear shotium's caches" is otherwise something a caller cannot express
294
+ * without already knowing where the other projects were.
158
295
  */
159
- resourceDir?: string;
296
+ target?: 'current'|'all'|(string&{});
160
297
  }
161
298
 
162
- export interface PurgeOptions {
299
+ /** One resource the cache is holding. */
300
+ export interface CacheEntry {
301
+ /** The resource, not the backend's key -- see `cache.getFiles()`. */
302
+ url: string;
303
+ /** Milliseconds since the Unix epoch. */
304
+ lastUsedMs: number;
305
+ bytes: number;
306
+ /** Which cache directory it was found in. */
307
+ dir: string;
308
+ }
309
+
310
+ export interface CacheClearOptions extends CacheTarget {
163
311
  /**
164
- * Also ask the OS to take the engine's pages back. The next screenshot pays
165
- * them back in soft page faults -- a few milliseconds -- so this is for when
166
- * there may not be a next one soon.
312
+ * Glob patterns matched against entry URLs -- not against filenames, which
313
+ * are hashes and would match nothing anybody would think to write.
314
+ *
315
+ * Supports `*` (within a path segment), `**` (across segments), `?` and
316
+ * `{a,b}`. Matching happens here rather than in the engine: the entries come
317
+ * back first, the patterns are applied to their URLs, and the ones that
318
+ * matched are what gets removed.
167
319
  */
168
- releaseWorkingSet?: boolean;
320
+ glob?: string[];
321
+ /**
322
+ * Remove entries not used for this many seconds. `0`, the default, means no
323
+ * age limit.
324
+ */
325
+ maxAge?: number;
326
+ /**
327
+ * Evict least-recently-used entries until the directory is at or below this
328
+ * many bytes. `0`, the default, means no size limit.
329
+ */
330
+ maxSize?: number;
331
+ }
332
+
333
+ export interface CacheClearResult {
334
+ /**
335
+ * How many entries went. `-1` when the whole directory was dropped in one
336
+ * operation, which the backend does without counting them.
337
+ */
338
+ removed: number;
339
+ bytesBefore: number;
340
+ bytesAfter: number;
341
+ /** Which directory this result is for. */
342
+ dir: string;
169
343
  }
package/dist/native.d.ts DELETED
@@ -1,66 +0,0 @@
1
- import { i as NativeStartOptions, o as PurgeOptions, s as ScreenshotOptions } from "./types-x9HtkzeE.js";
2
- //#region src/native.d.ts
3
- /**
4
- * The engine, in this process, and the queue in front of it.
5
- *
6
- * Same options and same output as `runtime`, and a different set of trades.
7
- * There is no worker process, so there is nothing to start, nothing to find on
8
- * disk and no pipe: a screenshot costs about a third less than through the
9
- * pool, and the whole thing is one process instead of five.
10
- *
11
- * What it gives up is what a separate process was providing for free. One
12
- * renderer, because blink is a process-wide singleton and `worker_threads`
13
- * share the process, so requests are serialised however many callers there
14
- * are. And no crash isolation: a renderer that dies takes the host program
15
- * with it, where the pool would have retried.
16
- *
17
- * The queue is not about fairness. Each capture occupies a libuv thread pool
18
- * thread for as long as the render takes, and there are four of those by
19
- * default, shared with fs and dns -- so letting four screenshots go at once
20
- * would stall the host's file reads for a fifth of a second at a time while
21
- * gaining nothing, since the engine serialises them anyway.
22
- */
23
- declare class NativeRuntime {
24
- private engine;
25
- private tail;
26
- get running(): boolean;
27
- /**
28
- * Starts the engine. Safe to call twice; the second call is a no-op.
29
- *
30
- * `cacheDir` is the HTTP disk cache and `null` disables it, which is the
31
- * default here. `resourceDir` is where `shotium_data.pak` and
32
- * `shotium_strings.pak` are, and defaults to the directory the addon was
33
- * loaded from, which is where they ship.
34
- */
35
- start(options?: NativeStartOptions): this;
36
- /** Stops the engine, after whatever is queued. */
37
- stop(): Promise<void>;
38
- /**
39
- * Hands back what the engine is holding but can rebuild.
40
- * `releaseWorkingSet` additionally asks the OS for the pages, which the next
41
- * screenshot pays back in soft faults -- worth it when there may not be a
42
- * next one soon.
43
- *
44
- * The resident worker does this for itself on a timer because it can watch
45
- * its own request stream go quiet. Here the queue belongs to the caller, so
46
- * the caller is the one who knows a batch has ended.
47
- */
48
- purge({ releaseWorkingSet }?: PurgeOptions): void;
49
- /**
50
- * Renders one screenshot. Resolves to the encoded image, or to `null` when
51
- * `path` was given and the engine wrote the file itself.
52
- */
53
- screenshot(options: ScreenshotOptions): Promise<Buffer | null>;
54
- }
55
- /** The shared in-process engine, started on first use. */
56
- declare const native: NativeRuntime;
57
- /** One screenshot through the shared in-process engine. */
58
- declare const screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
59
- declare const _default: {
60
- NativeRuntime: typeof NativeRuntime;
61
- native: NativeRuntime;
62
- screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
63
- };
64
- //#endregion
65
- export { NativeRuntime, type NativeStartOptions, type PurgeOptions, type ScreenshotOptions, _default as default, native, screenshot };
66
- //# sourceMappingURL=native.d.ts.map
package/dist/native.js DELETED
@@ -1,127 +0,0 @@
1
- import { n as packageDir, r as packageName } from "./platform-DU8DYqmA.js";
2
- import { r as toRequest } from "./request-qZXS3N9f.js";
3
- import { createRequire } from "node:module";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- //#region src/native.ts
9
- const require = createRequire(import.meta.url);
10
- const HERE = path.dirname(fileURLToPath(import.meta.url));
11
- function candidates() {
12
- const found = [];
13
- const dir = packageDir();
14
- if (dir) found.push(path.join(dir, "shotium.node"));
15
- found.push(path.join(HERE, "..", "native", "build", "Release", "shotium.node"));
16
- return found;
17
- }
18
- let binding = null;
19
- let bindingDir = null;
20
- function load() {
21
- if (binding) return binding;
22
- const tried = candidates();
23
- for (const candidate of tried) {
24
- if (!fs.existsSync(candidate)) continue;
25
- binding = require(candidate);
26
- bindingDir = path.dirname(candidate);
27
- return binding;
28
- }
29
- const expected = packageName();
30
- throw new Error(`shotium: no native engine for this platform.
31
- looked in:\n ${tried.join("\n ")}\n` + (expected ? ` It ships in ${expected}, which npm installs as an optional dependency of this package.
32
- ` : ` There is no build for ${process.platform}-${process.arch}.\n`) + " import(\"@shotkit/shotium\") uses worker processes instead and needs no addon.");
33
- }
34
- /**
35
- * The engine, in this process, and the queue in front of it.
36
- *
37
- * Same options and same output as `runtime`, and a different set of trades.
38
- * There is no worker process, so there is nothing to start, nothing to find on
39
- * disk and no pipe: a screenshot costs about a third less than through the
40
- * pool, and the whole thing is one process instead of five.
41
- *
42
- * What it gives up is what a separate process was providing for free. One
43
- * renderer, because blink is a process-wide singleton and `worker_threads`
44
- * share the process, so requests are serialised however many callers there
45
- * are. And no crash isolation: a renderer that dies takes the host program
46
- * with it, where the pool would have retried.
47
- *
48
- * The queue is not about fairness. Each capture occupies a libuv thread pool
49
- * thread for as long as the render takes, and there are four of those by
50
- * default, shared with fs and dns -- so letting four screenshots go at once
51
- * would stall the host's file reads for a fifth of a second at a time while
52
- * gaining nothing, since the engine serialises them anyway.
53
- */
54
- var NativeRuntime = class {
55
- engine = null;
56
- tail = Promise.resolve();
57
- get running() {
58
- return this.engine !== null;
59
- }
60
- /**
61
- * Starts the engine. Safe to call twice; the second call is a no-op.
62
- *
63
- * `cacheDir` is the HTTP disk cache and `null` disables it, which is the
64
- * default here. `resourceDir` is where `shotium_data.pak` and
65
- * `shotium_strings.pak` are, and defaults to the directory the addon was
66
- * loaded from, which is where they ship.
67
- */
68
- start(options = {}) {
69
- if (this.engine) return this;
70
- const native = load();
71
- const engineOptions = {};
72
- if (options.cacheDir !== null && options.cacheDir !== void 0) engineOptions.cacheDir = options.cacheDir;
73
- if (options.userAgent !== void 0) engineOptions.userAgent = options.userAgent;
74
- engineOptions.resourceDir = options.resourceDir || bindingDir;
75
- this.engine = native.create(JSON.stringify(engineOptions));
76
- return this;
77
- }
78
- /** Stops the engine, after whatever is queued. */
79
- async stop() {
80
- if (!this.engine) return;
81
- const engine = this.engine;
82
- this.engine = null;
83
- await this.tail.catch(() => {});
84
- load().destroy(engine);
85
- }
86
- /**
87
- * Hands back what the engine is holding but can rebuild.
88
- * `releaseWorkingSet` additionally asks the OS for the pages, which the next
89
- * screenshot pays back in soft faults -- worth it when there may not be a
90
- * next one soon.
91
- *
92
- * The resident worker does this for itself on a timer because it can watch
93
- * its own request stream go quiet. Here the queue belongs to the caller, so
94
- * the caller is the one who knows a batch has ended.
95
- */
96
- purge({ releaseWorkingSet = false } = {}) {
97
- if (!this.engine) return;
98
- load().purge(this.engine, releaseWorkingSet);
99
- }
100
- /**
101
- * Renders one screenshot. Resolves to the encoded image, or to `null` when
102
- * `path` was given and the engine wrote the file itself.
103
- */
104
- async screenshot(options) {
105
- const request = toRequest(options);
106
- if (!this.engine) this.start();
107
- const engine = this.engine;
108
- const native = load();
109
- const result = this.tail.catch(() => {}).then(() => native.capture(engine, JSON.stringify(request)));
110
- this.tail = result.catch(() => {});
111
- const image = await result;
112
- return request.path ? null : image;
113
- }
114
- };
115
- /** The shared in-process engine, started on first use. */
116
- const native = new NativeRuntime();
117
- /** One screenshot through the shared in-process engine. */
118
- const screenshot = (options) => native.screenshot(options);
119
- var native_default = {
120
- NativeRuntime,
121
- native,
122
- screenshot
123
- };
124
-
125
- //#endregion
126
- export { NativeRuntime, native_default as default, native, screenshot };
127
- //# sourceMappingURL=native.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"native.js","names":["platformPackage.packageDir","platformPackage.packageName"],"sources":["../src/native.ts"],"sourcesContent":["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 './lib/platform.js';\nimport {toRequest} from './lib/request.js';\nimport type {\n NativeStartOptions,\n PurgeOptions,\n ScreenshotOptions,\n} from './types.js';\n\nexport type {\n NativeStartOptions,\n PurgeOptions,\n ScreenshotOptions,\n} from './types.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// The engine handle the addon hands back. It is opaque on purpose: everything\n// that can be done with it is a call on the binding below.\ntype Engine = unknown;\n\n// What native/binding.cc exports. See shot/shot_api.h for the C ABI under it.\ninterface 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// shot in this process, instead of in workers beside it.\n//\n// The difference from `runtime` is not the API, which is the same\n// screenshot(options), and not the request format, which is byte for byte the\n// same JSON. It is where blink is:\n//\n// runtime N worker processes, one screenshot each at a time, a crash is a\n// retry, memory is N copies of an engine\n// native one engine in this process, one screenshot at a time ever, a\n// crash takes the program with it, memory is one copy\n//\n// One at a time is not a limitation of this file. Blink is a process-wide\n// singleton -- it is initialised once and there is no path to a second one --\n// so an in-process engine is one renderer no matter how it is driven, and\n// worker_threads do not change that because they share the process. A caller\n// who wants four screenshots at once wants four processes, which is what the\n// pool is for.\n//\n// What it buys is that there is no process to start, nothing to find on disk,\n// no pipe, and no supervisor: a program that takes a handful of screenshots\n// and exits pays for one engine and talks to it directly.\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 bindingDir: string|null = null;\n\nfunction 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 bindingDir = path.dirname(candidate);\n return binding;\n }\n const expected = platformPackage.packageName();\n throw new Error(\n 'shotium: no native 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.\\n' :\n ` There is no build for ${process.platform}-${process.arch}.\\n`) +\n ' import(\"@shotkit/shotium\") uses worker processes instead and needs ' +\n 'no addon.');\n}\n\n/**\n * The engine, in this process, and the queue in front of it.\n *\n * Same options and same output as `runtime`, and a different set of trades.\n * There is no worker process, so there is nothing to start, nothing to find on\n * disk and no pipe: a screenshot costs about a third less than through the\n * pool, and the whole thing is one process instead of five.\n *\n * What it gives up is what a separate process was providing for free. One\n * renderer, because blink is a process-wide singleton and `worker_threads`\n * share the process, so requests are serialised however many callers there\n * are. And no crash isolation: a renderer that dies takes the host program\n * with it, where the pool would have retried.\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 NativeRuntime {\n private engine: Engine|null = null;\n private tail: Promise<unknown> = Promise.resolve();\n\n get running(): boolean {\n return this.engine !== null;\n }\n\n /**\n * Starts the engine. Safe to call twice; the second call is a no-op.\n *\n * `cacheDir` is the HTTP disk cache and `null` disables it, which is the\n * default here. `resourceDir` is where `shotium_data.pak` and\n * `shotium_strings.pak` are, and defaults to the directory the addon was\n * loaded from, which is where they ship.\n */\n start(options: NativeStartOptions = {}): this {\n if (this.engine) {\n return this;\n }\n const native = load();\n\n const engineOptions: Record<string, unknown> = {};\n if (options.cacheDir !== null && options.cacheDir !== undefined) {\n engineOptions.cacheDir = options.cacheDir;\n }\n if (options.userAgent !== undefined) {\n engineOptions.userAgent = options.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 = options.resourceDir || bindingDir;\n\n this.engine = native.create(JSON.stringify(engineOptions));\n return this;\n }\n\n /** Stops the engine, after whatever is queued. */\n async stop(): Promise<void> {\n if (!this.engine) {\n return;\n }\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 engine = this.engine;\n this.engine = null;\n await this.tail.catch(() => {});\n load().destroy(engine);\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 resident worker does this for itself on a timer because it can watch\n * its own request stream go quiet. Here the queue belongs to the caller, so\n * the caller is the one who knows a batch has ended.\n */\n purge({releaseWorkingSet = false}: PurgeOptions = {}): void {\n if (!this.engine) {\n return;\n }\n load().purge(this.engine, 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 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 const request = toRequest(options);\n if (!this.engine) {\n this.start();\n }\n const engine = this.engine;\n const native = 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(engine, JSON.stringify(request)));\n this.tail = result.catch(() => {});\n const image = await result;\n return request.path ? null : image;\n }\n}\n\n/** The shared in-process engine, started on first use. */\nconst native = new NativeRuntime();\n\n/** One screenshot through the shared in-process engine. */\nconst screenshot = (options: ScreenshotOptions): Promise<Buffer|null> =>\n native.screenshot(options);\n\nexport {native, screenshot};\n\nexport default {NativeRuntime, native, screenshot};\n"],"mappings":";;;;;;;;AAoBA,MAAM,UAAU,cAAc,YAAY,GAAG;AAG7C,MAAM,OAAO,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AA4CxD,SAAS,aAAuB;CAC9B,MAAM,QAAkB,CAAC;CACzB,MAAM,MAAMA,WAA2B;CACvC,IAAI,KACF,MAAM,KAAK,KAAK,KAAK,KAAK,cAAc,CAAC;CAE3C,MAAM,KACF,KAAK,KAAK,MAAM,MAAM,UAAU,SAAS,WAAW,cAAc,CAAC;CACvE,OAAO;AACT;AAEA,IAAI,UAA8B;AAClC,IAAI,aAA0B;AAE9B,SAAS,OAAsB;CAC7B,IAAI,SACF,OAAO;CAET,MAAM,QAAQ,WAAW;CACzB,KAAK,MAAM,aAAa,OAAO;EAC7B,IAAI,CAAC,GAAG,WAAW,SAAS,GAC1B;EAMF,UAAU,QAAQ,SAAS;EAC3B,aAAa,KAAK,QAAQ,SAAS;EACnC,OAAO;CACT;CACA,MAAM,WAAWC,YAA4B;CAC7C,MAAM,IAAI,MACN;oBACqB,MAAM,KAAK,QAAQ,EAAE,OACzC,WACI,iBAAiB,SAAS;IAE1B,2BAA2B,QAAQ,SAAS,GAAG,QAAQ,KAAK,QACjE,kFACW;AACjB;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,gBAAb,MAA2B;CACzB,AAAQ,SAAsB;CAC9B,AAAQ,OAAyB,QAAQ,QAAQ;CAEjD,IAAI,UAAmB;EACrB,OAAO,KAAK,WAAW;CACzB;;;;;;;;;CAUA,MAAM,UAA8B,CAAC,GAAS;EAC5C,IAAI,KAAK,QACP,OAAO;EAET,MAAM,SAAS,KAAK;EAEpB,MAAM,gBAAyC,CAAC;EAChD,IAAI,QAAQ,aAAa,QAAQ,QAAQ,aAAa,QACpD,cAAc,WAAW,QAAQ;EAEnC,IAAI,QAAQ,cAAc,QACxB,cAAc,YAAY,QAAQ;EAMpC,cAAc,cAAc,QAAQ,eAAe;EAEnD,KAAK,SAAS,OAAO,OAAO,KAAK,UAAU,aAAa,CAAC;EACzD,OAAO;CACT;;CAGA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAK,QACR;EAKF,MAAM,SAAS,KAAK;EACpB,KAAK,SAAS;EACd,MAAM,KAAK,KAAK,YAAY,CAAC,CAAC;EAC9B,KAAK,CAAC,CAAC,QAAQ,MAAM;CACvB;;;;;;;;;;;CAYA,MAAM,EAAC,oBAAoB,UAAuB,CAAC,GAAS;EAC1D,IAAI,CAAC,KAAK,QACR;EAEF,KAAK,CAAC,CAAC,MAAM,KAAK,QAAQ,iBAAiB;CAC7C;;;;;CAMA,MAAM,WAAW,SAAkD;EAGjE,MAAM,UAAU,UAAU,OAAO;EACjC,IAAI,CAAC,KAAK,QACR,KAAK,MAAM;EAEb,MAAM,SAAS,KAAK;EACpB,MAAM,SAAS,KAAK;EAIpB,MAAM,SAAS,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,WAC/B,OAAO,QAAQ,QAAQ,KAAK,UAAU,OAAO,CAAC,CAAC;EACzD,KAAK,OAAO,OAAO,YAAY,CAAC,CAAC;EACjC,MAAM,QAAQ,MAAM;EACpB,OAAO,QAAQ,OAAO,OAAO;CAC/B;AACF;;AAGA,MAAM,SAAS,IAAI,cAAc;;AAGjC,MAAM,cAAc,YAChB,OAAO,WAAW,OAAO;AAI7B,qBAAe;CAAC;CAAe;CAAQ;AAAU"}
@@ -1,32 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import path from "node:path";
3
-
4
- //#region src/lib/platform.ts
5
- const require = createRequire(import.meta.url);
6
- const PACKAGES = {
7
- "win32-x64": "@shotkit/shotium-win32-x64",
8
- "win32-arm64": "@shotkit/shotium-win32-arm64",
9
- "darwin-x64": "@shotkit/shotium-darwin-x64",
10
- "darwin-arm64": "@shotkit/shotium-darwin-arm64",
11
- "linux-x64": "@shotkit/shotium-linux-x64",
12
- "linux-arm64": "@shotkit/shotium-linux-arm64"
13
- };
14
- function packageName(platform = process.platform, arch = process.arch) {
15
- return PACKAGES[`${platform}-${arch}`] ?? null;
16
- }
17
- function packageDir() {
18
- const name = packageName();
19
- if (!name) return null;
20
- try {
21
- return path.dirname(require.resolve(`${name}/package.json`));
22
- } catch {
23
- return null;
24
- }
25
- }
26
- function binaryName() {
27
- return process.platform === "win32" ? "shotium.exe" : "shotium";
28
- }
29
-
30
- //#endregion
31
- export { packageDir as n, packageName as r, binaryName as t };
32
- //# sourceMappingURL=platform-DU8DYqmA.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"platform-DU8DYqmA.js","names":[],"sources":["../src/lib/platform.ts"],"sourcesContent":["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\n// What the engine executable is called, which is not what the platform calls\n// it: Windows wants the extension and nothing else does.\nfunction binaryName(): string {\n return process.platform === 'win32' ? 'shotium.exe' : 'shotium';\n}\n\nexport {PACKAGES, binaryName, packageDir, packageName};\n"],"mappings":";;;;AAKA,MAAM,UAAU,cAAc,YAAY,GAAG;AA8B7C,MAAM,WAA6C;CACjD,aAAa;CACb,eAAe;CACf,cAAc;CACd,gBAAgB;CAChB,aAAa;CACb,eAAe;AACjB;AAEA,SAAS,YACL,WAAmB,QAAQ,UAC3B,OAAe,QAAQ,MAAmB;CAC5C,OAAO,SAAS,GAAG,SAAS,GAAG,WAAW;AAC5C;AAQA,SAAS,aAA0B;CACjC,MAAM,OAAO,YAAY;CACzB,IAAI,CAAC,MACH,OAAO;CAET,IAAI;EACF,OAAO,KAAK,QAAQ,QAAQ,QAAQ,GAAG,KAAK,cAAc,CAAC;CAC7D,QAAQ;EACN,OAAO;CACT;AACF;AAIA,SAAS,aAAqB;CAC5B,OAAO,QAAQ,aAAa,UAAU,gBAAgB;AACxD"}