@shotkit/shotium 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -60
- package/dist/daemon_main.js +9 -7
- package/dist/daemon_main.js.map +1 -1
- package/dist/index.d.ts +396 -35
- package/dist/index.js +389 -41
- package/dist/index.js.map +1 -1
- package/dist/protocol-BTeWJDOa.js +474 -0
- package/dist/protocol-BTeWJDOa.js.map +1 -0
- package/native/binding.cc +184 -3
- package/package.json +7 -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 +246 -58
- package/src/lib/request.ts +10 -1
- package/src/types.ts +204 -4
- package/dist/engine-Xe7nH-1i.js +0 -267
- package/dist/engine-Xe7nH-1i.js.map +0 -1
package/src/lib/engine.ts
CHANGED
|
@@ -2,23 +2,80 @@ import * as binding from './binding.js';
|
|
|
2
2
|
import type {Engine as Handle} from './binding.js';
|
|
3
3
|
import {toRequest} from './request.js';
|
|
4
4
|
import type {WireRequest} from './request.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
CaptureStats,
|
|
7
|
+
ReleaseMemoryOptions,
|
|
8
|
+
ScreenshotOptions,
|
|
9
|
+
ScreenshotResult,
|
|
10
|
+
StartOptions,
|
|
11
|
+
StartResult,
|
|
12
|
+
} from '../types.js';
|
|
6
13
|
|
|
14
|
+
import type {ResolvedStartOptions} from './config.js';
|
|
7
15
|
import {resolveStartOptions} from './config.js';
|
|
8
16
|
|
|
9
|
-
//
|
|
17
|
+
// The engine this process has, held above every Engine object that uses it.
|
|
10
18
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// still alive. See shot/shot_api.h.
|
|
19
|
+
// Blink is initialised once and has no undo: it writes process-wide statics
|
|
20
|
+
// that shot_engine_destroy() cannot take back, and the C API refuses a second
|
|
21
|
+
// create for the lifetime of the process whether or not the first is still
|
|
22
|
+
// alive.
|
|
16
23
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
24
|
+
// That fact used to be exposed directly -- `stop()` destroyed the engine and
|
|
25
|
+
// every later `start()` threw. It was the wrong shape. `stop()` and `start()`
|
|
26
|
+
// are a caller saying "I am done for now" and "I want it again", and a library
|
|
27
|
+
// whose engine can be asked for exactly once turns an ordinary pair of calls
|
|
28
|
+
// into a thing that has to be rationed. It also made the disk cache
|
|
29
|
+
// nonsensical: the whole point of a cache is the *next* run, and the next run
|
|
30
|
+
// could not have the engine that reads it.
|
|
31
|
+
//
|
|
32
|
+
// So the handle lives here rather than on the instance. `stop()` stands the
|
|
33
|
+
// engine down -- the queue drains, the memory goes back, nothing more is
|
|
34
|
+
// accepted -- and `start()` picks the same one up again, as many times as a
|
|
35
|
+
// caller likes. The process is the engine's lifetime, which is what it always
|
|
36
|
+
// was; the difference is that the API no longer pretends to offer a shorter
|
|
37
|
+
// one.
|
|
38
|
+
let shared: Handle|null = null;
|
|
39
|
+
|
|
40
|
+
// What `shared` was created with. Kept because those options are fixed for the
|
|
41
|
+
// life of the process -- a later `start()` asking for a different cache
|
|
42
|
+
// directory cannot be given one, and is told so rather than handed an engine
|
|
43
|
+
// that quietly uses the first caller's.
|
|
44
|
+
let sharedOptions: ResolvedStartOptions|null = null;
|
|
45
|
+
|
|
46
|
+
// Whether the one create this process gets has been spent.
|
|
47
|
+
//
|
|
48
|
+
// Separate from `shared` being non-null because `dispose()` clears the handle
|
|
49
|
+
// and does not give the ability back: after a real teardown there is no engine
|
|
50
|
+
// and there cannot be another. Nothing in the public surface calls dispose()
|
|
51
|
+
// -- the daemon does, on its way out of a process it owns.
|
|
52
|
+
let spent = false;
|
|
53
|
+
|
|
54
|
+
/** The engine handle this process has, or null if it has none. */
|
|
55
|
+
function sharedHandle(): Handle|null {
|
|
56
|
+
return shared;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The options that are fixed at create time, and the report a mismatch gets.
|
|
60
|
+
//
|
|
61
|
+
// Only the ones the caller actually named are checked: `start()` with no
|
|
62
|
+
// arguments is a caller saying "whatever is there", which is exactly what
|
|
63
|
+
// adopting a running engine gives them. Naming an option that disagrees is
|
|
64
|
+
// different -- it is a request that cannot be honoured, and silently rendering
|
|
65
|
+
// with the other value is the failure this exists to prevent.
|
|
66
|
+
function conflictingOption(
|
|
67
|
+
options: StartOptions, current: ResolvedStartOptions): string|null {
|
|
68
|
+
const wanted = resolveStartOptions(options);
|
|
69
|
+
for (const key of ['cacheDir', 'cacheMaxBytes', 'userAgent', 'resourceDir'] as
|
|
70
|
+
const) {
|
|
71
|
+
if (options[key] === undefined || wanted[key] === current[key]) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
return `${key} is ${JSON.stringify(current[key])}, and this start() ` +
|
|
75
|
+
`asked for ${JSON.stringify(wanted[key])}`;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
22
79
|
|
|
23
80
|
/**
|
|
24
81
|
* Blink, in this process, and the queue in front of it.
|
|
@@ -36,45 +93,73 @@ let startedInThisProcess = false;
|
|
|
36
93
|
* gaining nothing, since the engine serialises them anyway.
|
|
37
94
|
*/
|
|
38
95
|
export class Engine {
|
|
39
|
-
|
|
40
|
-
|
|
96
|
+
// Whether *this* object considers itself started. The engine behind it may
|
|
97
|
+
// well be up for somebody else; `running` is about this lifecycle, not about
|
|
98
|
+
// whether the process has an engine.
|
|
99
|
+
private active = false;
|
|
41
100
|
private tail: Promise<unknown> = Promise.resolve();
|
|
42
101
|
|
|
43
102
|
get running(): boolean {
|
|
44
|
-
return this.
|
|
103
|
+
return this.active && shared !== null;
|
|
45
104
|
}
|
|
46
105
|
|
|
47
106
|
/**
|
|
48
|
-
*
|
|
49
|
-
* library code can call it defensively.
|
|
107
|
+
* The addon's engine handle, or null when this process has never had one.
|
|
50
108
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
109
|
+
* Deliberately not conditional on `running`. It is the cache that asks, and
|
|
110
|
+
* what the cache needs to know is whether a backend exists in this process
|
|
111
|
+
* -- because within one process a cache directory has one backend, so
|
|
112
|
+
* reading or clearing the directory the engine holds means borrowing it
|
|
113
|
+
* rather than opening a second one. A stood-down engine still holds its
|
|
114
|
+
* directory, so a caller who calls `stop()` and then `cache.getFiles()` is
|
|
115
|
+
* asking about a live backend and has to be routed to it. Nothing else
|
|
116
|
+
* should reach for this.
|
|
54
117
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
118
|
+
get nativeHandle(): Handle|null {
|
|
119
|
+
return sharedHandle();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Starts the engine, or picks the running one back up.
|
|
124
|
+
*
|
|
125
|
+
* Callable as often as a caller likes, in any order with `stop()`. The first
|
|
126
|
+
* call in a process builds the engine; every later one adopts it, which is
|
|
127
|
+
* the same engine and the same warm cache. Library code can call it
|
|
128
|
+
* defensively.
|
|
129
|
+
*
|
|
130
|
+
* The one thing that cannot be adopted is a different configuration. The
|
|
131
|
+
* options below are fixed when the engine is built and there is no second
|
|
132
|
+
* build, so naming one that disagrees with what is running throws rather
|
|
133
|
+
* than rendering with a value the caller did not ask for.
|
|
134
|
+
*/
|
|
135
|
+
start(options: StartOptions = {}): StartResult {
|
|
136
|
+
if (shared) {
|
|
137
|
+
const conflict = conflictingOption(options, sharedOptions!);
|
|
138
|
+
if (conflict) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'shotium: this process already has an engine, and its ' +
|
|
141
|
+
conflict + '. Blink is initialised once per process and cannot ' +
|
|
142
|
+
'be built again, so an engine\'s options are fixed for as long ' +
|
|
143
|
+
'as the process lives -- stop() does not undo them. Use the ' +
|
|
144
|
+
'engine that is up, or run another process.');
|
|
145
|
+
}
|
|
146
|
+
this.active = true;
|
|
147
|
+
return this.status();
|
|
64
148
|
}
|
|
65
|
-
if (
|
|
149
|
+
if (spent) {
|
|
66
150
|
throw new Error(
|
|
67
|
-
'shotium: an engine
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'another process.');
|
|
151
|
+
'shotium: this process had an engine and it was disposed of. ' +
|
|
152
|
+
'Blink is initialised once per process and cannot be built again. ' +
|
|
153
|
+
'Run another process.');
|
|
71
154
|
}
|
|
155
|
+
|
|
72
156
|
const native = binding.load();
|
|
73
157
|
const resolved = resolveStartOptions(options);
|
|
74
158
|
|
|
75
159
|
const engineOptions: Record<string, unknown> = {};
|
|
76
160
|
if (resolved.cacheDir !== null) {
|
|
77
161
|
engineOptions.cacheDir = resolved.cacheDir;
|
|
162
|
+
engineOptions.cacheMaxBytes = resolved.cacheMaxBytes;
|
|
78
163
|
}
|
|
79
164
|
if (resolved.userAgent !== undefined) {
|
|
80
165
|
engineOptions.userAgent = resolved.userAgent;
|
|
@@ -85,30 +170,84 @@ export class Engine {
|
|
|
85
170
|
// teaching the engine a second way to look. See shot_api.h.
|
|
86
171
|
engineOptions.resourceDir = resolved.resourceDir ?? binding.directory();
|
|
87
172
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
173
|
+
shared = native.create(JSON.stringify(engineOptions));
|
|
174
|
+
sharedOptions = resolved;
|
|
175
|
+
this.active = true;
|
|
176
|
+
return this.status();
|
|
91
177
|
}
|
|
92
178
|
|
|
93
179
|
/**
|
|
94
|
-
*
|
|
180
|
+
* What the engine came up as: whether this lifecycle is started, which cache
|
|
181
|
+
* directory the engine has, and whether it actually got it.
|
|
182
|
+
*
|
|
183
|
+
* The last of those is the one worth reading. A directory that cannot be
|
|
184
|
+
* created or written to -- no permission, no space, a path that is a file --
|
|
185
|
+
* fails invisibly: the engine renders exactly as well without a cache, only
|
|
186
|
+
* slower, and every capture pays the network again for a reason nothing
|
|
187
|
+
* reports. The engine opens the cache during `start()` so that this is
|
|
188
|
+
* answerable before the first screenshot rather than after it.
|
|
95
189
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
190
|
+
* The cache half is answered from the engine whenever this process has one,
|
|
191
|
+
* including after `stop()`. A stood-down engine still holds its directory,
|
|
192
|
+
* and reporting `null` for it would say the cache had gone away when what
|
|
193
|
+
* went away was the willingness to render.
|
|
194
|
+
*/
|
|
195
|
+
status(): StartResult {
|
|
196
|
+
if (!shared) {
|
|
197
|
+
return {running: false, cacheDir: null, cacheActive: false};
|
|
198
|
+
}
|
|
199
|
+
const reported =
|
|
200
|
+
JSON.parse(binding.load().status(shared)) as Omit<StartResult, 'running'>;
|
|
201
|
+
return {running: this.running, ...reported};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Stands the engine down, after whatever is queued.
|
|
206
|
+
*
|
|
207
|
+
* The queue drains, the memory the engine can rebuild goes back to the OS,
|
|
208
|
+
* and `running` becomes false. What does not happen is a teardown of Blink,
|
|
209
|
+
* because there is no such thing -- see the note at the top of this file --
|
|
210
|
+
* so the disk cache stays where it is and `start()` picks the same engine up
|
|
211
|
+
* again whenever the caller wants it.
|
|
212
|
+
*
|
|
213
|
+
* Which makes this exactly what it says: not a destructor, a caller saying
|
|
214
|
+
* they are done for now. A program that will want another screenshot in a
|
|
215
|
+
* moment can equally well stay started and call `releaseMemory()`; the two
|
|
216
|
+
* do the same work, and this one also stops accepting captures.
|
|
99
217
|
*/
|
|
100
218
|
async stop(): Promise<void> {
|
|
101
|
-
if (!this.
|
|
219
|
+
if (!this.active) {
|
|
102
220
|
return;
|
|
103
221
|
}
|
|
104
|
-
this.
|
|
105
|
-
// After the queue, not before:
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
222
|
+
this.active = false;
|
|
223
|
+
// After the queue, not before: a caller's last screenshot should resolve
|
|
224
|
+
// rather than race the stand-down, and the memory is not worth handing
|
|
225
|
+
// back until the thing still using it has finished.
|
|
226
|
+
await this.tail.catch(() => {});
|
|
227
|
+
if (shared) {
|
|
228
|
+
binding.load().purge(shared, /*releaseWorkingSet=*/ true);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The real teardown: joins the engine thread, unwinds the network stack, and
|
|
234
|
+
* lets the disk cache write its index.
|
|
235
|
+
*
|
|
236
|
+
* Final, and final for the process rather than for this object -- which is
|
|
237
|
+
* why it is not on the public surface. The daemon calls it as it exits a
|
|
238
|
+
* process it owns, where the index flush is worth having and nothing is
|
|
239
|
+
* going to ask for another screenshot. Everything else wants `stop()`.
|
|
240
|
+
*/
|
|
241
|
+
async dispose(): Promise<void> {
|
|
242
|
+
this.active = false;
|
|
110
243
|
await this.tail.catch(() => {});
|
|
111
|
-
|
|
244
|
+
const handle = shared;
|
|
245
|
+
shared = null;
|
|
246
|
+
sharedOptions = null;
|
|
247
|
+
if (handle) {
|
|
248
|
+
spent = true;
|
|
249
|
+
binding.load().destroy(handle);
|
|
250
|
+
}
|
|
112
251
|
}
|
|
113
252
|
|
|
114
253
|
/**
|
|
@@ -121,11 +260,11 @@ export class Engine {
|
|
|
121
260
|
* request stream go quiet. Here the queue belongs to the caller, so the
|
|
122
261
|
* caller is the one who knows a batch has ended.
|
|
123
262
|
*/
|
|
124
|
-
|
|
125
|
-
if (!
|
|
263
|
+
releaseMemory({releaseWorkingSet = false}: ReleaseMemoryOptions = {}): void {
|
|
264
|
+
if (!shared) {
|
|
126
265
|
return;
|
|
127
266
|
}
|
|
128
|
-
binding.load().purge(
|
|
267
|
+
binding.load().purge(shared, releaseWorkingSet);
|
|
129
268
|
}
|
|
130
269
|
|
|
131
270
|
/**
|
|
@@ -136,7 +275,7 @@ export class Engine {
|
|
|
136
275
|
// throws, and a caller who wrote `screenshot(bad).catch(...)` would get the
|
|
137
276
|
// throw past the catch and into the surrounding frame. The whole surface is
|
|
138
277
|
// promise-shaped, so a bad request is a rejection like everything else.
|
|
139
|
-
async screenshot(options: ScreenshotOptions): Promise<
|
|
278
|
+
async screenshot(options: ScreenshotOptions): Promise<ScreenshotResult> {
|
|
140
279
|
// Before anything else, and before the queue: a malformed request should
|
|
141
280
|
// be a rejection now rather than one that waits its turn.
|
|
142
281
|
return this.capture(toRequest(options));
|
|
@@ -150,11 +289,13 @@ export class Engine {
|
|
|
150
289
|
* ScreenshotOptions would mean the daemon validating a request it cannot see
|
|
151
290
|
* the original of, and rejecting fields a newer client legitimately sent.
|
|
152
291
|
*/
|
|
153
|
-
async capture(request: WireRequest): Promise<
|
|
154
|
-
|
|
292
|
+
async capture(request: WireRequest): Promise<ScreenshotResult> {
|
|
293
|
+
// Starts, or restarts, or adopts -- a screenshot after `stop()` is an
|
|
294
|
+
// ordinary thing to ask for and gets the engine back.
|
|
295
|
+
if (!this.running) {
|
|
155
296
|
this.start();
|
|
156
297
|
}
|
|
157
|
-
const handle =
|
|
298
|
+
const handle = shared!;
|
|
158
299
|
const native = binding.load();
|
|
159
300
|
|
|
160
301
|
// Chain onto the tail so that captures run one at a time. The catch keeps
|
|
@@ -162,7 +303,54 @@ export class Engine {
|
|
|
162
303
|
const result = this.tail.catch(() => {}).then(
|
|
163
304
|
() => native.capture(handle, JSON.stringify(request)));
|
|
164
305
|
this.tail = result.catch(() => {});
|
|
165
|
-
|
|
166
|
-
|
|
306
|
+
|
|
307
|
+
let captured;
|
|
308
|
+
try {
|
|
309
|
+
captured = await result;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
// The addon attaches the capture's statistics to the rejection as
|
|
312
|
+
// unparsed JSON, the same way it hands them back on success -- see
|
|
313
|
+
// NativeCapture. Parsing them here rather than leaving a string on the
|
|
314
|
+
// error is what makes `error.stats` the same CaptureStats a successful
|
|
315
|
+
// call returns, which is the whole point of attaching it: the failure is
|
|
316
|
+
// the case where the counters explain the most.
|
|
317
|
+
const withStats = error as Error&{stats?: string | CaptureStats};
|
|
318
|
+
if (typeof withStats.stats === 'string') {
|
|
319
|
+
withStats.stats = JSON.parse(withStats.stats) as CaptureStats;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
image: request.path ? null : captured.image,
|
|
326
|
+
stats: parseStats(captured.stats),
|
|
327
|
+
};
|
|
167
328
|
}
|
|
168
329
|
}
|
|
330
|
+
|
|
331
|
+
// A zeroed set of counters.
|
|
332
|
+
//
|
|
333
|
+
// Zeroes rather than undefined because the alternative is every caller writing
|
|
334
|
+
// `stats?.timing?.total ?? 0` around a field that is present for every capture
|
|
335
|
+
// that actually ran. The only case that produces none is a request rejected
|
|
336
|
+
// before it started, and that path throws rather than returning.
|
|
337
|
+
function emptyStats(): CaptureStats {
|
|
338
|
+
return {
|
|
339
|
+
requests: 0,
|
|
340
|
+
fromCache: 0,
|
|
341
|
+
failed: 0,
|
|
342
|
+
bytes: 0,
|
|
343
|
+
httpStatus: 0,
|
|
344
|
+
finalUrl: '',
|
|
345
|
+
timing: {fetch: 0, render: 0, encode: 0, total: 0},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// The addon hands statistics over as unparsed JSON -- see NativeCapture -- so
|
|
350
|
+
// this is where the string becomes an object. The daemon's client has them
|
|
351
|
+
// parsed already, from its own response header, and uses emptyStats directly.
|
|
352
|
+
function parseStats(json: string|undefined): CaptureStats {
|
|
353
|
+
return json ? JSON.parse(json) as CaptureStats : emptyStats();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {emptyStats, parseStats, sharedHandle};
|
package/src/lib/request.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
9
|
|
|
@@ -16,6 +21,8 @@ export interface WireRequest {
|
|
|
16
21
|
pageGotoParams?: PageGotoParams;
|
|
17
22
|
clip?: Clip;
|
|
18
23
|
allowFileAccess?: boolean;
|
|
24
|
+
cache?: CacheMode;
|
|
25
|
+
headers?: Record<string, string>;
|
|
19
26
|
width?: number;
|
|
20
27
|
height?: number;
|
|
21
28
|
}
|
|
@@ -40,6 +47,8 @@ const WIRE_FIELDS = new Set([
|
|
|
40
47
|
'clip',
|
|
41
48
|
'viewport',
|
|
42
49
|
'allowFileAccess',
|
|
50
|
+
'cache',
|
|
51
|
+
'headers',
|
|
43
52
|
]);
|
|
44
53
|
|
|
45
54
|
// One ScreenshotOptions, checked and flattened into what goes on the wire.
|
package/src/types.ts
CHANGED
|
@@ -33,6 +33,69 @@ 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
|
+
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
|
+
|
|
36
99
|
export interface ScreenshotOptions {
|
|
37
100
|
/** An http/https/file URL, or a local path. */
|
|
38
101
|
file: string;
|
|
@@ -68,15 +131,48 @@ export interface ScreenshotOptions {
|
|
|
68
131
|
* is rendered on.
|
|
69
132
|
*/
|
|
70
133
|
allowFileAccess?: boolean;
|
|
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>;
|
|
71
152
|
}
|
|
72
153
|
|
|
73
154
|
export interface StartOptions {
|
|
74
155
|
/**
|
|
75
|
-
* Root of the HTTP disk cache. `null` disables caching entirely
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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.
|
|
78
164
|
*/
|
|
79
165
|
cacheDir?: string|null;
|
|
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;
|
|
80
176
|
/** Overrides the built-in user agent string. */
|
|
81
177
|
userAgent?: string;
|
|
82
178
|
/**
|
|
@@ -86,6 +182,36 @@ export interface StartOptions {
|
|
|
86
182
|
resourceDir?: string;
|
|
87
183
|
}
|
|
88
184
|
|
|
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;
|
|
213
|
+
}
|
|
214
|
+
|
|
89
215
|
export interface DaemonOptions extends StartOptions {
|
|
90
216
|
/**
|
|
91
217
|
* Address the daemon by name instead of by configuration. Without it the
|
|
@@ -133,7 +259,16 @@ export interface DaemonStatus {
|
|
|
133
259
|
version: string;
|
|
134
260
|
}
|
|
135
261
|
|
|
136
|
-
|
|
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 {
|
|
137
272
|
/**
|
|
138
273
|
* Also ask the OS to take the engine's pages back. The next screenshot pays
|
|
139
274
|
* them back in soft page faults -- a few milliseconds -- so this is for when
|
|
@@ -141,3 +276,68 @@ export interface PurgeOptions {
|
|
|
141
276
|
*/
|
|
142
277
|
releaseWorkingSet?: boolean;
|
|
143
278
|
}
|
|
279
|
+
|
|
280
|
+
/** Which cache directory an operation is about. */
|
|
281
|
+
export interface CacheTarget {
|
|
282
|
+
/**
|
|
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.
|
|
295
|
+
*/
|
|
296
|
+
target?: 'current'|'all'|(string&{});
|
|
297
|
+
}
|
|
298
|
+
|
|
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 {
|
|
311
|
+
/**
|
|
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.
|
|
319
|
+
*/
|
|
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;
|
|
343
|
+
}
|