@shotkit/shotium 0.1.0 → 0.2.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/src/lib/daemon.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import {EventEmitter} from 'node:events';
2
2
  import fs from 'node:fs';
3
3
  import net from 'node:net';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
4
6
 
5
7
  import type {DaemonOptions, DaemonStatus} from '../types.js';
6
8
 
7
9
  import {resolveStartOptions} from './config.js';
8
10
  import type {ResolvedStartOptions} from './config.js';
9
11
  import {endpointFor} from './endpoint.js';
10
- import {Pool} from './pool.js';
12
+ import {Engine} from './engine.js';
11
13
  import {FrameReader, encodeFrame} from './protocol.js';
12
14
  import type {WireRequest} from './request.js';
13
15
 
@@ -25,11 +27,6 @@ const VERSION = (() => {
25
27
  }
26
28
  })();
27
29
 
28
- // How much longer than the page's own deadline the daemon waits before it
29
- // decides a worker is not going to answer at all. Same margin, same reasoning
30
- // as index.ts: the worker fails a slow page by itself and replies.
31
- const SUPERVISOR_MARGIN_MS = 10000;
32
- const DEFAULT_TIMEOUT_MS = 30000;
33
30
  const DEFAULT_IDLE_TIMEOUT_MS = 300000;
34
31
 
35
32
  // One message off the socket. `op` defaults to screenshot because that is what
@@ -51,30 +48,39 @@ interface DaemonReply {
51
48
  stopping?: boolean;
52
49
  }
53
50
 
54
- // A worker pool that outlives the process that asked for it.
51
+ // An engine that outlives the process that asked for it.
55
52
  //
56
- // The pool in index.ts is already resident, but only for as long as the Node
53
+ // The engine in index.ts is already resident, but only for as long as the Node
57
54
  // process holding it: a command-line invocation, a CI step, a serverless
58
- // handler and a `node -e` all pay for starting workers and then throw them
59
- // away. This is the same pool behind a socket, so the second caller -- in a
55
+ // handler and a `node -e` all pay for starting Blink and then throw it away.
56
+ // This is the same engine behind a socket, so the second caller -- in a
60
57
  // different process, minutes later -- pays a connect() and nothing else.
61
58
  //
62
- // The wire format is the worker's own, one level up: a request frame of JSON,
63
- // answered by a header frame and a payload frame. What it adds is `id`, so one
64
- // connection can have several requests in flight; the worker protocol cannot,
65
- // because a worker renders one document at a time, and multiplexing is exactly
66
- // what the pool in the middle is for.
59
+ // A request frame of JSON, answered by a header frame and a payload frame:
67
60
  //
68
61
  // -> [len][{"id":7,"op":"screenshot","request":{...}}]
69
62
  // <- [len][{"id":7,"ok":true,"bytes":97756}] [len][<PNG>]
70
63
  //
71
- // Events: ready, request, response, idle-exit, error, plus the pool's own.
64
+ // `id` is on the wire so that a client may have several requests outstanding
65
+ // on one connection. That is a convenience for the client, not concurrency:
66
+ // there is one renderer here, because Blink is a process-wide singleton, so
67
+ // the requests queue and come back in the order the engine finished them.
68
+ // Wanting two at once means wanting two daemons, addressed by `name`.
69
+ //
70
+ // Nothing supervises a capture. The pool this replaced could time a worker out
71
+ // and kill it; an in-process engine has no such seam -- there is no way to
72
+ // abandon a render without abandoning the process. A page's own deadline
73
+ // (`pageGotoParams.timeout`) is what bounds it, and the engine answers slow
74
+ // pages by itself. `timeout` and `retry` on the wire are accepted and ignored,
75
+ // so that an older client still talks to this.
76
+ //
77
+ // Events: ready, warm, request, response, idle-exit, error, close.
72
78
  class Daemon extends EventEmitter {
73
79
  private readonly options: ResolvedStartOptions;
74
80
  private readonly endpointPath: string;
75
81
  private readonly idleTimeoutMs: number;
76
82
  private readonly prewarmOnStart: boolean;
77
- private pool: Pool|null = null;
83
+ private readonly engine = new Engine();
78
84
  private server: net.Server|null = null;
79
85
  private sockets = new Set<net.Socket>();
80
86
  private inFlight = 0;
@@ -106,24 +112,21 @@ class Daemon extends EventEmitter {
106
112
  return this.warmed;
107
113
  }
108
114
 
109
- // Brings the pool up and starts listening. The pipe existing *is* the
115
+ // Brings the engine up and starts listening. The pipe existing *is* the
110
116
  // readiness signal -- a client's connect() either succeeds or the daemon is
111
- // not up -- so nothing is bound until the pool has been asked to start.
117
+ // not up -- so nothing is bound until the engine has started.
118
+ //
119
+ // Starting it here rather than on the first request is deliberate: a machine
120
+ // with no engine for its platform should fail while the caller is still
121
+ // watching, not answer a connect() and then reject every request on it.
112
122
  async listen(): Promise<this> {
113
- const pool = new Pool(this.options);
114
- this.pool = pool;
115
- for (const event of ['exit', 'crash', 'timeout', 'worker-restart',
116
- 'worker-error', 'stderr']) {
117
- pool.on(event, (payload) => this.emit(event, payload));
118
- }
119
- pool.start();
123
+ this.engine.start(this.options);
120
124
 
121
125
  this.server = net.createServer((socket) => this.accept(socket));
122
126
  this.server.on('error', (error) => this.emit('error', error));
123
127
  await this.bind();
124
128
  this.armIdleTimer();
125
- this.emit('ready',
126
- {endpoint: this.endpointPath, workers: this.options.workers});
129
+ this.emit('ready', {endpoint: this.endpointPath});
127
130
  if (this.prewarmOnStart) {
128
131
  await this.prewarm();
129
132
  }
@@ -178,8 +181,8 @@ class Daemon extends EventEmitter {
178
181
  //
179
182
  // On Windows it is a named pipe, and node exposes no way to give one an ACL:
180
183
  // the default lets any account on the machine open it. A daemon on a shared
181
- // Windows host is therefore as trusted as the machine's users are -- render
182
- // in-process, or with a binary that has no file access, if that is not
184
+ // Windows host is therefore as trusted as the machine's users are -- use the
185
+ // engine in your own process, where nothing is listening, if that is not
183
186
  // acceptable.
184
187
  private restrict(): void {
185
188
  if (process.platform === 'win32') {
@@ -192,23 +195,37 @@ class Daemon extends EventEmitter {
192
195
  }
193
196
  }
194
197
 
195
- // Renders one throwaway document per worker so that the first real request
196
- // does not pay for whatever each process initialises lazily. The pool hands
197
- // one request to each free worker, and there are exactly as many requests as
198
- // workers, so every process is touched.
198
+ // Renders one throwaway document so that the first real request does not pay
199
+ // for whatever the engine initialises lazily. One is enough: there is one
200
+ // renderer, and it is the same one every request lands on.
201
+ //
202
+ // A temporary file, not a `data:` URL. This used to send
203
+ // `data:text/html,...`, which the renderer rejects -- shot_capture.cc takes
204
+ // file, http and https and nothing else -- so every prewarm failed into the
205
+ // catch below and the step had never once done anything. The failure was
206
+ // invisible because a prewarm that does not work looks exactly like one that
207
+ // does, only slower on the first request.
199
208
  //
200
- // `data:` rather than a file, because a daemon started without
201
- // --allow-file-access would otherwise be prewarmed by a request it refuses.
209
+ // The document names no subresources, so it renders identically whether or
210
+ // not this daemon allows file access -- which is what the `data:` URL was
211
+ // reaching for. A top-level file: URL always loads; `allowFileAccess` gates
212
+ // what the document may then pull in.
202
213
  async prewarm(): Promise<void> {
203
- const blank = 'data:text/html,<!doctype html><title>shotium</title>';
204
- await Promise.all(Array.from({length: this.options.workers}, () => {
205
- return this.pool!
206
- .submit({file: blank, width: 16, height: 16},
207
- {timeout: DEFAULT_TIMEOUT_MS + SUPERVISOR_MARGIN_MS, retry: 1})
208
- .catch(() => null);
209
- }));
210
- this.warmed = true;
211
- this.emit('warm', {workers: this.options.workers});
214
+ const blank = path.join(
215
+ os.tmpdir(), `shotium-prewarm-${process.pid}.html`);
216
+ try {
217
+ fs.writeFileSync(
218
+ blank, '<!doctype html><title>shotium</title><p>shotium');
219
+ await this.engine.capture({file: blank, width: 16, height: 16});
220
+ this.warmed = true;
221
+ } catch (error) {
222
+ // Not fatal: a daemon that could not prewarm still serves. But it is not
223
+ // warm, and status() should not claim it is.
224
+ this.emit('error', error);
225
+ } finally {
226
+ fs.rmSync(blank, {force: true});
227
+ }
228
+ this.emit('warm', {warm: this.warmed});
212
229
  }
213
230
 
214
231
  status(): DaemonStatus {
@@ -216,10 +233,9 @@ class Daemon extends EventEmitter {
216
233
  ok: true,
217
234
  pid: process.pid,
218
235
  endpoint: this.endpointPath,
219
- binary: this.options.binary,
220
- workers: this.options.workers,
221
236
  cacheDir: this.options.cacheDir,
222
- args: this.options.args,
237
+ userAgent: this.options.userAgent,
238
+ resourceDir: this.options.resourceDir,
223
239
  warm: this.warmed,
224
240
  uptimeMs: Date.now() - this.startedAt,
225
241
  connections: this.sockets.size,
@@ -285,26 +301,22 @@ class Daemon extends EventEmitter {
285
301
  }
286
302
 
287
303
  const request = message.request || ({} as WireRequest);
288
- const timeout = (typeof message.timeout === 'number' ? message.timeout :
289
- DEFAULT_TIMEOUT_MS) +
290
- SUPERVISOR_MARGIN_MS;
291
- const retry = typeof message.retry === 'number' ? message.retry : 0;
292
304
 
293
305
  this.inFlight += 1;
294
306
  this.armIdleTimer();
295
307
  this.emit('request', {id, file: request.file});
296
- this.pool!.submit(request, {timeout, retry})
297
- .then((result) => {
308
+ this.engine.capture(request)
309
+ .then((image) => {
298
310
  this.served += 1;
299
311
  this.reply(
300
312
  socket,
301
313
  {
302
314
  id,
303
315
  ok: true,
304
- bytes: result.image ? result.image.length : 0,
305
- path: result.header ? result.header.path : undefined,
316
+ bytes: image ? image.length : 0,
317
+ path: request.path,
306
318
  },
307
- result.image);
319
+ image);
308
320
  })
309
321
  .catch((error: Error) => {
310
322
  this.reply(
@@ -362,7 +374,7 @@ class Daemon extends EventEmitter {
362
374
  }
363
375
  this.sockets.clear();
364
376
  await new Promise<void>((resolve) => this.server!.close(() => resolve()));
365
- await this.pool!.stop();
377
+ await this.engine.stop();
366
378
  this.emit('close', {});
367
379
  }
368
380
  }
@@ -5,21 +5,27 @@ import path from 'node:path';
5
5
  // What endpointFor() needs to know: a resolved configuration, plus the two
6
6
  // ways of overriding the address it would derive from one.
7
7
  export interface EndpointOptions {
8
- binary?: string;
9
- workers?: number;
10
8
  cacheDir?: string|null;
11
- args?: string[];
9
+ userAgent?: string;
10
+ resourceDir?: string;
12
11
  name?: string;
13
12
  endpoint?: string;
14
13
  }
15
14
 
16
15
  // Where a daemon listens, derived from what it was asked to be.
17
16
  //
18
- // The address is a hash of the configuration -- binary, worker count, cache
19
- // root, extra flags -- rather than a fixed name, because attaching to whatever
20
- // daemon happens to be up would mean rendering with someone else's binary and
21
- // someone else's flags. Two configurations are two daemons; the same
22
- // configuration, from any process, is one.
17
+ // The address is a hash of the configuration -- cache root, user agent,
18
+ // resource directory -- rather than a fixed name, because attaching to
19
+ // whatever daemon happens to be up would mean rendering with someone else's
20
+ // settings. Two configurations are two daemons; the same configuration, from
21
+ // any process, is one.
22
+ //
23
+ // Every field of EndpointOptions is optional, so nothing here fails to compile
24
+ // when a field is dropped from the configuration -- it just stops being part
25
+ // of the identity, and every caller collapses onto one address. That happened
26
+ // once, when the worker pool went away and this was left hashing three fields
27
+ // that no longer existed. If a field is added to StartOptions and it changes
28
+ // what the engine renders, it belongs in the array below.
23
29
  //
24
30
  // A caller who wants a daemon by name instead of by configuration passes
25
31
  // `name`, which replaces the hash. That is the escape hatch for a service that
@@ -30,10 +36,11 @@ function endpointKey(options: EndpointOptions): string {
30
36
  return String(options.name);
31
37
  }
32
38
  const identity = JSON.stringify([
33
- path.resolve(options.binary || ''),
34
- options.workers,
35
- options.cacheDir === null ? null : path.resolve(options.cacheDir || ''),
36
- options.args || [],
39
+ options.cacheDir === null || options.cacheDir === undefined ?
40
+ null :
41
+ path.resolve(options.cacheDir),
42
+ options.userAgent ?? null,
43
+ options.resourceDir ? path.resolve(options.resourceDir) : null,
37
44
  ]);
38
45
  return crypto.createHash('sha256').update(identity).digest('hex').slice(0, 16);
39
46
  }
@@ -0,0 +1,168 @@
1
+ import * as binding from './binding.js';
2
+ import type {Engine as Handle} from './binding.js';
3
+ import {toRequest} from './request.js';
4
+ import type {WireRequest} from './request.js';
5
+ import type {PurgeOptions, ScreenshotOptions, StartOptions} from '../types.js';
6
+
7
+ import {resolveStartOptions} from './config.js';
8
+
9
+ // One per process, ever. Not one at a time -- one.
10
+ //
11
+ // This is not a rule of this file, it is what Blink is: initialising it writes
12
+ // process-wide statics it has no path to undo, so shot_engine_destroy() gives
13
+ // back what it can and the process still cannot make another. The C API
14
+ // returns SHOT_ERR_STATE for a second create whether or not the first is
15
+ // still alive. See shot/shot_api.h.
16
+ //
17
+ // So `stop()` is final for the process, and this flag exists to say that in
18
+ // words at the call site. Without it a caller who stops and starts again gets
19
+ // SHOT_ERR_STATE out of the addon -- a true error, arriving one layer too deep
20
+ // to explain that the answer is a second process rather than a retry.
21
+ let startedInThisProcess = false;
22
+
23
+ /**
24
+ * Blink, in this process, and the queue in front of it.
25
+ *
26
+ * There is one renderer and there is no way to have two. Blink is a
27
+ * process-wide singleton: it is initialised once, there is no path to a second
28
+ * one, and `worker_threads` do not change that because they share the process.
29
+ * So captures are serialised however many callers there are, and a program
30
+ * that wants four at once wants four processes.
31
+ *
32
+ * The queue is not about fairness. Each capture occupies a libuv thread pool
33
+ * thread for as long as the render takes, and there are four of those by
34
+ * default, shared with fs and dns -- so letting four screenshots go at once
35
+ * would stall the host's file reads for a fifth of a second at a time while
36
+ * gaining nothing, since the engine serialises them anyway.
37
+ */
38
+ export class Engine {
39
+ private handle: Handle|null = null;
40
+ private stopped = false;
41
+ private tail: Promise<unknown> = Promise.resolve();
42
+
43
+ get running(): boolean {
44
+ return this.handle !== null;
45
+ }
46
+
47
+ /**
48
+ * Starts the engine. Safe to call twice; the second call is a no-op, so that
49
+ * library code can call it defensively.
50
+ *
51
+ * Not safe to call after `stop()`, and not because of anything here: Blink
52
+ * starts once per process and cannot be restarted. Another engine means
53
+ * another process.
54
+ */
55
+ start(options: StartOptions = {}): this {
56
+ if (this.handle) {
57
+ return this;
58
+ }
59
+ if (this.stopped) {
60
+ throw new Error(
61
+ 'shotium: this engine was stopped, and Blink cannot be started ' +
62
+ 'again in a process that has already run it. Start another ' +
63
+ 'process, or keep the engine up between screenshots.');
64
+ }
65
+ if (startedInThisProcess) {
66
+ throw new Error(
67
+ 'shotium: an engine has already run in this process. Blink is a ' +
68
+ 'process-wide singleton -- there is one per process, ever -- so a ' +
69
+ 'second Runtime cannot have one. Use the shared `runtime`, or run ' +
70
+ 'another process.');
71
+ }
72
+ const native = binding.load();
73
+ const resolved = resolveStartOptions(options);
74
+
75
+ const engineOptions: Record<string, unknown> = {};
76
+ if (resolved.cacheDir !== null) {
77
+ engineOptions.cacheDir = resolved.cacheDir;
78
+ }
79
+ if (resolved.userAgent !== undefined) {
80
+ engineOptions.userAgent = resolved.userAgent;
81
+ }
82
+ // The packs sit beside the library, and the library cannot find itself on
83
+ // Linux -- the path the engine resolves for "this module" goes through
84
+ // /proc/self/exe, which names node. Saying it here is cheaper than
85
+ // teaching the engine a second way to look. See shot_api.h.
86
+ engineOptions.resourceDir = resolved.resourceDir ?? binding.directory();
87
+
88
+ this.handle = native.create(JSON.stringify(engineOptions));
89
+ startedInThisProcess = true;
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Stops the engine, after whatever is queued.
95
+ *
96
+ * Final for this process: see the note above. A program that will want
97
+ * another screenshot later should leave the engine up and call `purge()`
98
+ * instead, which hands back the memory without giving up the engine.
99
+ */
100
+ async stop(): Promise<void> {
101
+ if (!this.handle) {
102
+ return;
103
+ }
104
+ this.stopped = true;
105
+ // After the queue, not before: destroy() waits for a capture in flight
106
+ // anyway, and doing it in order means a caller's last screenshot resolves
107
+ // rather than racing the shutdown.
108
+ const handle = this.handle;
109
+ this.handle = null;
110
+ await this.tail.catch(() => {});
111
+ binding.load().destroy(handle);
112
+ }
113
+
114
+ /**
115
+ * Hands back what the engine is holding but can rebuild.
116
+ * `releaseWorkingSet` additionally asks the OS for the pages, which the next
117
+ * screenshot pays back in soft faults -- worth it when there may not be a
118
+ * next one soon.
119
+ *
120
+ * The daemon does this for itself on a timer because it can watch its own
121
+ * request stream go quiet. Here the queue belongs to the caller, so the
122
+ * caller is the one who knows a batch has ended.
123
+ */
124
+ purge({releaseWorkingSet = false}: PurgeOptions = {}): void {
125
+ if (!this.handle) {
126
+ return;
127
+ }
128
+ binding.load().purge(this.handle, releaseWorkingSet);
129
+ }
130
+
131
+ /**
132
+ * Renders one screenshot. Resolves to the encoded image, or to `null` when
133
+ * `path` was given and the engine wrote the file itself.
134
+ */
135
+ // `async` and not a plain function returning capture()'s promise: toRequest()
136
+ // throws, and a caller who wrote `screenshot(bad).catch(...)` would get the
137
+ // throw past the catch and into the surrounding frame. The whole surface is
138
+ // promise-shaped, so a bad request is a rejection like everything else.
139
+ async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {
140
+ // Before anything else, and before the queue: a malformed request should
141
+ // be a rejection now rather than one that waits its turn.
142
+ return this.capture(toRequest(options));
143
+ }
144
+
145
+ /**
146
+ * The same, for a request that is already in wire form.
147
+ *
148
+ * The daemon reads these off a socket, where they arrived having been
149
+ * validated by the client that sent them. Re-deriving one from
150
+ * ScreenshotOptions would mean the daemon validating a request it cannot see
151
+ * the original of, and rejecting fields a newer client legitimately sent.
152
+ */
153
+ async capture(request: WireRequest): Promise<Buffer|null> {
154
+ if (!this.handle) {
155
+ this.start();
156
+ }
157
+ const handle = this.handle;
158
+ const native = binding.load();
159
+
160
+ // Chain onto the tail so that captures run one at a time. The catch keeps
161
+ // one failure from poisoning everything queued behind it.
162
+ const result = this.tail.catch(() => {}).then(
163
+ () => native.capture(handle, JSON.stringify(request)));
164
+ this.tail = result.catch(() => {});
165
+ const image = await result;
166
+ return request.path ? null : image;
167
+ }
168
+ }
@@ -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,9 @@
1
1
  import type {Clip, PageGotoParams, ScreenshotOptions} from '../types.js';
2
2
 
3
3
  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
4
 
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.
5
+ // What actually goes down the pipe: ScreenshotOptions with the viewport
6
+ // flattened -- see toRequest below for why.
12
7
  export interface WireRequest {
13
8
  file: string;
14
9
  type?: 'png'|'jpeg'|'webp';
@@ -49,8 +44,8 @@ const WIRE_FIELDS = new Set([
49
44
 
50
45
  // One ScreenshotOptions, checked and flattened into what goes on the wire.
51
46
  //
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
47
+ // It lives here rather than in index.ts because the engine in this process and
48
+ // the daemon both send it: a request that is valid through one entry point and
54
49
  // rejected through the other would be a difference nobody asked for.
55
50
  function toRequest(options: ScreenshotOptions): WireRequest {
56
51
  if (!options || typeof options !== 'object') {
@@ -65,11 +60,6 @@ function toRequest(options: ScreenshotOptions): WireRequest {
65
60
  if (value === undefined) {
66
61
  continue;
67
62
  }
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
63
  if (!WIRE_FIELDS.has(key)) {
74
64
  throw new TypeError(`shotium: unknown option "${key}"`);
75
65
  }
@@ -101,7 +91,6 @@ function timeoutFor(options: ScreenshotOptions): number {
101
91
 
102
92
  export {
103
93
  DEFAULT_TIMEOUT_MS,
104
- SUPERVISOR_MARGIN_MS,
105
94
  WIRE_FIELDS,
106
95
  timeoutFor,
107
96
  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 {
@@ -70,35 +68,29 @@ export interface ScreenshotOptions {
70
68
  * is rendered on.
71
69
  */
72
70
  allowFileAccess?: boolean;
73
- /** How many times to re-send after a crash or a timeout. Default 0. */
74
- retry?: number;
75
71
  }
76
72
 
77
73
  export interface StartOptions {
78
74
  /**
79
- * Path to `shotium.exe`. Default `$SHOTIUM_BINARY`, then the platform
80
- * package for this machine, then `./bin/shotium.exe`.
75
+ * Root of the HTTP disk cache. `null` disables caching entirely, which is
76
+ * the default: a program holding the engine is often short-lived, and a
77
+ * cache it never reads twice is a directory it leaves behind.
81
78
  */
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
79
  cacheDir?: string|null;
87
- /** Extra flags passed to every worker. */
88
- args?: string[];
89
- }
90
-
91
- export interface WorkerEvent {
92
- worker: number;
93
- code?: number|null;
94
- signal?: NodeJS.Signals|null;
80
+ /** Overrides the built-in user agent string. */
81
+ userAgent?: string;
82
+ /**
83
+ * Where `shotium_data.pak` and `shotium_strings.pak` are. Defaults to the
84
+ * directory the engine was loaded from, which is where they ship.
85
+ */
86
+ resourceDir?: string;
95
87
  }
96
88
 
97
89
  export interface DaemonOptions extends StartOptions {
98
90
  /**
99
91
  * 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
92
+ * endpoint is a hash of `cacheDir`, `userAgent` and `resourceDir`, so a
93
+ * client never attaches to a daemon that renders with something other than
102
94
  * what it asked for.
103
95
  */
104
96
  name?: string;
@@ -110,9 +102,8 @@ export interface DaemonOptions extends StartOptions {
110
102
  */
111
103
  idleTimeoutMs?: number;
112
104
  /**
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.
105
+ * Render one throwaway document at startup, so the first real request does
106
+ * not pay for whatever the engine initialises lazily. Default true.
116
107
  */
117
108
  prewarm?: boolean;
118
109
  /** Fail instead of starting a daemon when none is listening. */
@@ -129,11 +120,10 @@ export interface DaemonStatus {
129
120
  spawned?: boolean;
130
121
  pid: number;
131
122
  endpoint: string;
132
- binary: string;
133
- workers: number;
134
123
  cacheDir: string|null;
135
- args: string[];
136
- /** Every worker has rendered at least once. */
124
+ userAgent?: string;
125
+ resourceDir?: string;
126
+ /** The engine has rendered at least once. */
137
127
  warm: boolean;
138
128
  uptimeMs: number;
139
129
  connections: number;
@@ -143,22 +133,6 @@ export interface DaemonStatus {
143
133
  version: string;
144
134
  }
145
135
 
146
- export interface NativeStartOptions {
147
- /**
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.
151
- */
152
- cacheDir?: string|null;
153
- /** Overrides the built-in user agent string. */
154
- userAgent?: string;
155
- /**
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.
158
- */
159
- resourceDir?: string;
160
- }
161
-
162
136
  export interface PurgeOptions {
163
137
  /**
164
138
  * Also ask the OS to take the engine's pages back. The next screenshot pays